diff --git a/Agent.md b/Agent.md index 68c621b..8dc9357 100644 --- a/Agent.md +++ b/Agent.md @@ -93,7 +93,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg ``` -Python: `uv run pytest tests/ -v` (597) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (599) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (93: 22 daemon_client + 22 app-commands + 24 renderer smoke + 15 i18n + 7 integration + 3 commands) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) diff --git a/README.cn.md b/README.cn.md index e489e81..8203255 100644 --- a/README.cn.md +++ b/README.cn.md @@ -274,7 +274,7 @@ EMRG 不只是追赶——它自己追上来。 git clone https://github.com/argszero/emrg.git cd emrg uv sync # 安装依赖 -uv run pytest tests/ -v # 跑测试(当前 597 项) +uv run pytest tests/ -v # 跑测试(当前 599 项) uv run python -m emrg # 启动 TUI # CI 含 actionlint workflow 门禁(#444):workflow 解析错误在 PR 即失败 diff --git a/README.md b/README.md index c7cf7e0..1e9d68e 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ EMRG doesn't just keep up — it catches up on its own. git clone https://github.com/argszero/emrg.git cd emrg uv sync # install deps -uv run pytest tests/ -v # run tests (currently 597 items) +uv run pytest tests/ -v # run tests (currently 599 items) uv run python -m emrg # launch TUI # CI includes actionlint workflow gate (#444): workflow parse errors fail PR CI diff --git a/emrg/server/git_utils.py b/emrg/server/git_utils.py index 6df4b04..1140853 100644 --- a/emrg/server/git_utils.py +++ b/emrg/server/git_utils.py @@ -172,15 +172,25 @@ def _cache_tool_paths(git: str, gh: str) -> None: Also persists the EMRG repo URL (``repo``) so the evolution workspace self-heal (rant 2026-08-06T20:42:05) can clone on demand without hardcoding — packaged installs have no git remote to detect. + + The write is atomic (temp file + os.replace) so concurrent readers + never observe a partially-written file; the read is guarded like + ``_cached_tool_path`` so a corrupt/partial cache (e.g. a crashed or + concurrent writer) degrades to an empty dict instead of raising + JSONDecodeError (observed as a flaky test_daemon failure when the + live daemon rewrote install-info.json mid-suite). """ try: data = {} if INSTALL_INFO.exists(): - data = json.loads(INSTALL_INFO.read_text(encoding="utf-8")) + try: + data = json.loads(INSTALL_INFO.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError, AttributeError): + data = {} data.update({"git_path": git, "gh_path": gh, "repo": "https://github.com/argszero/emrg.git"}) - INSTALL_INFO.write_text( - json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8" - ) + tmp = INSTALL_INFO.with_name(INSTALL_INFO.name + ".tmp") + tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + os.replace(tmp, INSTALL_INFO) except OSError: pass diff --git a/tests/test_git_utils.py b/tests/test_git_utils.py index ebdc1d0..dd5f650 100644 --- a/tests/test_git_utils.py +++ b/tests/test_git_utils.py @@ -115,6 +115,46 @@ def test_cache_tool_paths_preserves_existing_fields(tmp_path, monkeypatch): assert data["repo"] == "https://github.com/argszero/emrg.git" +def test_cache_tool_paths_tolerates_corrupt_cache(tmp_path, monkeypatch): + """A corrupt/partial install-info.json must not raise — degrades to {}. + + Regression for the flaky test_daemon::test_build_prompt_with_project + (json.decoder.JSONDecodeError): the live daemon rewrites this shared + file non-atomically; a concurrent reader could catch a partial write. + """ + from emrg.server import git_utils as mod + import json as _json + + info = tmp_path / "install-info.json" + info.write_text('{"git_path": "/partial', encoding="utf-8") # truncated JSON + monkeypatch.setattr(mod, "INSTALL_INFO", info) + + mod._cache_tool_paths("/usr/bin/git", "/usr/bin/gh") # must not raise + + data = _json.loads(info.read_text(encoding="utf-8")) + assert data["git_path"] == "/usr/bin/git" + assert data["repo"] == "https://github.com/argszero/emrg.git" + + +def test_cache_tool_paths_atomic_write_no_temp_leftover(tmp_path, monkeypatch): + """Write is atomic: target is valid JSON and no .tmp file remains.""" + from emrg.server import git_utils as mod + import json as _json + + info = tmp_path / "install-info.json" + info.write_text( + _json.dumps({"custom": "old"}), encoding="utf-8" + ) + monkeypatch.setattr(mod, "INSTALL_INFO", info) + + mod._cache_tool_paths("/usr/bin/git", "/usr/bin/gh") + + assert not (tmp_path / "install-info.json.tmp").exists() + data = _json.loads(info.read_text(encoding="utf-8")) + assert data["git_path"] == "/usr/bin/git" + assert data["custom"] == "old" + + # ── no_prompt_env / parse_gh_auth_user (rant 2026-08-07T10:17:27) ──