From 119094201cbd36f4508ca09f30b73142cef748cd Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Wed, 12 Aug 2026 19:26:16 +0800 Subject: [PATCH] emrg: write install-info cache once per process in resolve_git_gh --- Agent.md | 2 +- emrg/server/git_utils.py | 14 +++++++++++++- tests/test_git_utils.py | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Agent.md b/Agent.md index 53271c70..25cffee1 100644 --- a/Agent.md +++ b/Agent.md @@ -118,7 +118,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` (746) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (747) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (229: 44 daemon_client + 19 conn-manager + 22 app-commands + 107 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — 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/emrg/server/git_utils.py b/emrg/server/git_utils.py index 77e69962..94255e2e 100644 --- a/emrg/server/git_utils.py +++ b/emrg/server/git_utils.py @@ -23,6 +23,12 @@ # restarted without PATH git and cycles were silently skipped for 18 min). _GIT_MISSING_WARNED = False +# In-process memo of the last (git, gh) paths written to install-info.json — +# resolve_git_gh() is called on every git_cmd(), and each call used to do an +# atomic tmp+os.replace disk write even when nothing changed. Tool paths are +# stable within a process lifetime, so write once and skip the rest. +_LAST_CACHED_PATHS: tuple[str, str] | None = None + # ── Non-interactive subprocess environment (rant 2026-08-07T10:17:27) ── # @@ -228,6 +234,7 @@ def resolve_git_gh() -> tuple[str, str]: Returns (git_path, gh_path). Missing executables yield '' (callers decide how to degrade). """ + global _LAST_CACHED_PATHS git = _cached_tool_path("git") gh = _cached_tool_path("gh") if git and Path(git).exists(): @@ -240,7 +247,12 @@ def resolve_git_gh() -> tuple[str, str]: gh = _tool_in_install("gh") or (shutil.which("gh") or "") if git: - _cache_tool_paths(git, gh) + # Write the cache only when the resolved pair changed since the last + # write (or nothing cached yet) — avoids an atomic install-info.json + # write on every git_cmd() call. Paths are stable per process. + if _LAST_CACHED_PATHS != (git, gh): + _cache_tool_paths(git, gh) + _LAST_CACHED_PATHS = (git, gh) else: # git is the failure mode that silently disables evolution (2026-08-12 # incident) — warn regardless of whether gh resolved. Also skip the diff --git a/tests/test_git_utils.py b/tests/test_git_utils.py index aef94b6f..20a766d7 100644 --- a/tests/test_git_utils.py +++ b/tests/test_git_utils.py @@ -349,3 +349,27 @@ def fake_cache(git: str, gh: str) -> None: warns = [r for r in caplog.records if r.levelno >= logging.WARNING] assert len(warns) == 1 assert "git executable not found" in warns[0].getMessage() + + +def test_resolve_git_gh_writes_cache_once_per_process(monkeypatch): + """Cache write happens once per process, not on every git_cmd() call. + + resolve_git_gh() runs on every git_cmd(); before this fix each call did an + atomic install-info.json write even when the resolved paths were unchanged. + Tool paths are stable within a process lifetime — write once, skip the rest. + """ + from emrg.server import git_utils as gu + + calls = [] + monkeypatch.setattr(gu, "_cached_tool_path", lambda tool: None) + monkeypatch.setattr(gu, "_tool_in_install", lambda tool: None) + monkeypatch.setattr(gu.shutil, "which", lambda tool: "/usr/bin/git" if tool == "git" else "/usr/bin/gh") + monkeypatch.setattr(gu, "_cache_tool_paths", lambda g, h: calls.append((g, h))) + monkeypatch.setattr(gu, "_LAST_CACHED_PATHS", None) + + gu.resolve_git_gh() + gu.resolve_git_gh() # unchanged paths → no second write + gu.resolve_git_gh() + + assert len(calls) == 1, f"expected exactly 1 cache write, got {len(calls)}: {calls}" + assert calls[0] == ("/usr/bin/git", "/usr/bin/gh")