diff --git a/Agent.md b/Agent.md index 84ae476c..9f3cef23 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.token; python -m emrg ``` -Python: `uv run pytest tests/ -v` (1078) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (1079) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (259: 45 daemon_client + 20 conn-manager + 22 app-commands + 129 renderer smoke + 15 i18n + 8 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + 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/scheduler.py b/emrg/server/scheduler.py index 65680a69..32b1b949 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -351,7 +351,12 @@ def _is_dirty_tree_sync(source_dir: str) -> bool: """ import subprocess as _sp # noqa: PLC0415 — local import keeps the module invariant - if not os.path.isdir(os.path.join(source_dir, ".git")): + # .git may be a directory (regular repo) or a file (linked worktree — + # "gitdir: ..." pointer). Treat both as git repos; only a bare/non-git + # dir fails open (cycle 20260825-194513: isdir-only check silently + # bypassed the guard in linked-worktree setups). + git_marker = os.path.join(source_dir, ".git") + if not os.path.isdir(git_marker) and not os.path.isfile(git_marker): return False try: out = _sp.run( diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index cf02c6f0..785635aa 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -2496,6 +2496,30 @@ def test_is_dirty_tree_detects_uncommitted_changes(): assert TaskHandler._is_dirty_tree_sync(d) is False +def test_is_dirty_tree_linked_worktree(): + """Cycle 20260825-194513: a linked git worktree has `.git` as a FILE, not + a dir — the probe must detect dirt there too (isdir-only check silently + failed open, bypassing the read-only guard in worktree setups).""" + with tempfile.TemporaryDirectory() as d: + subprocess.run(["git", "init", d], capture_output=True, timeout=10) + subprocess.run(["git", "-C", d, "config", "user.email", "t@t.t"], + capture_output=True, timeout=10) + subprocess.run(["git", "-C", d, "config", "user.name", "t"], + capture_output=True, timeout=10) + (Path(d) / "base.txt").write_text("base", encoding="utf-8") + subprocess.run(["git", "-C", d, "add", "base.txt"], capture_output=True, timeout=10) + subprocess.run(["git", "-C", d, "commit", "-m", "base"], + capture_output=True, timeout=10) + wt = Path(d) / "wt" + subprocess.run(["git", "-C", d, "worktree", "add", str(wt), "HEAD"], + capture_output=True, timeout=10) + # .git is a file in the linked worktree + assert (wt / ".git").is_file(), "linked worktree .git should be a file" + assert TaskHandler._is_dirty_tree_sync(str(wt)) is False # clean worktree + (wt / "dirty.txt").write_text("dirty", encoding="utf-8") + assert TaskHandler._is_dirty_tree_sync(str(wt)) is True # untracked → dirty + + def test_dirty_tree_forces_read_only_structural_guard(): """Community issue #979: a dirty source tree forces the cycle's effective sandbox to read-only regardless of configuration — topology over rules."""