From 6808cb47924ee46a259e1fc490b74cc583dc0274 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Sun, 13 Sep 2026 08:38:20 +0800 Subject: [PATCH] emrg: report an empty --all as a state, not as a usage error With --all passed explicitly and nothing unmerged (a merge that resolved cleanly) the tool printed 'error: no paths given (pass files, or --all for every unmerged path)' and exited 2: it asked for the flag the caller had just passed, and reported the good outcome as a malformed invocation. Measured in cyc20260913-082711 at the moment this mattered most - a merge that resolved CLEANLY and produced a tree that fails the doc-count guard (issue #1158, PR #1166). There is no conflict marker to notice, so the tool's silence had to be explained rather than reported as an error. --all answered with an empty list is now rc 0 with a message that says the merge is clean or already resolved, warns that a clean merge is not evidence of a healthy tree, and points at check-merge-sequence.py - the tool that measures that, and the one that caught this case. The bare no-argument invocation stays a usage error (rc 2), and the named-file 'no conflict blocks' case is unchanged at rc 2 (documented and pinned). Tests pin both directions plus a rot guard: the script the message points at must exist, so renaming it fails the suite instead of sending a reader after a file that is not there. Mutation-checked: restoring the old behaviour fails 1 test, dropping the usage error fails 2, removing the pointer fails 1. --- Agent.md | 2 +- scripts/classify-conflict.py | 18 ++++++++++++ tests/test_classify_conflict.py | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/Agent.md b/Agent.md index d23917ca..4f8d3ad6 100644 --- a/Agent.md +++ b/Agent.md @@ -119,7 +119,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` (1562) — import check: `uv run python -c "from emrg.client.app import run_client" +Python: `uv run pytest tests/ -v` (1564) — import check: `uv run python -c "from emrg.client.app import run_client" GUI: `cd emrg/gui && npm test` (100: 44 daemon_client + 20 conn-manager + 7 integration + 7 nav-policy + 7 gui-state + 6 build-config + 4 boot-contract + 3 preload-api + 2 theme-guard) — syntax: `node --check main.js preload.js daemon_client.js` Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (514: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 13 markdown + 21 transcript + 11 TranscriptView + 15 history + 31 composer + 41 Composer + 6 LinkDialog + 16 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 27 workspaceView + 10 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 8 openSession + 6 WelcomeDialog + 9 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 18 daemonBridge + 7 DaemonBridgeProvider + 30 Shell + 15 DialogHost + 20 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown) + `npm run build` → `renderer/dist/` 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 上下文) diff --git a/scripts/classify-conflict.py b/scripts/classify-conflict.py index 8442d953..6b92e78b 100644 --- a/scripts/classify-conflict.py +++ b/scripts/classify-conflict.py @@ -52,6 +52,8 @@ Exit codes: 0 every block classified, none needs a human decision + (also: `--all` and git reports nothing unmerged - a clean merge is a + state, not a usage error; see the note in `main()`) 1 at least one block is `overlapping` (human must decide) 2 usage error / no conflict blocks found """ @@ -634,6 +636,22 @@ def main(argv: list[str] | None = None) -> int: paths = [p for p in paths if not (p in seen or seen.add(p))] if not paths: + if args.all: + # `--all` was *answered*, not misused: git reports nothing unmerged. + # This is rc 0 rather than rc 2 because a clean merge is a state, not a + # malformed invocation - and the message must say so, because the old + # one ("pass files, or --all for every unmerged path") told the caller + # to pass the flag they had just passed, at the one moment when the + # silence is the interesting signal: `cyc20260913-082711` hit this with + # a merge that resolved *cleanly* and produced a tree that fails the + # doc-count guard (issue #1158), so "no conflicts" must not be read as + # "the tree is fine" - hence the pointer to the tool that measures that. + print( + "no unmerged paths: the merge is clean or already resolved - " + "nothing to classify. A clean merge is not evidence of a healthy " + "tree; check the resulting tree with scripts/check-merge-sequence.py" + ) + return 0 print( "error: no paths given (pass files, or --all for every unmerged path)", file=sys.stderr, diff --git a/tests/test_classify_conflict.py b/tests/test_classify_conflict.py index 77d26560..7f3dca38 100644 --- a/tests/test_classify_conflict.py +++ b/tests/test_classify_conflict.py @@ -461,6 +461,56 @@ class TestCli: def test_no_paths_is_a_usage_error(self, mod, capsys) -> None: assert mod.main([]) == 2 + def test_all_with_nothing_unmerged_is_a_state_not_a_usage_error( + self, mod, capsys, monkeypatch + ) -> None: + """`--all` answered with an empty list is rc 0, not the usage error (cyc20260913-082711). + + Measured before this: with `--all` passed explicitly and no unmerged paths + (a merge that resolved cleanly), the tool printed "error: no paths given + (pass files, or --all for every unmerged path)" and exited 2 - telling the + caller to pass the flag they had just passed, and reporting a clean merge + as a malformed invocation. Hit in practice at the moment a clean merge had + produced a tree that fails the doc-count guard, i.e. exactly when the + silence needed an explanation rather than a usage complaint. + """ + monkeypatch.setattr(mod, "_unmerged_paths", lambda: []) + rc = mod.main(["--all"]) + captured = capsys.readouterr() + assert rc == 0, "a clean merge is a state, not a usage error" + assert "nothing to classify" in captured.out + assert "no paths given" not in captured.out + captured.err, ( + "the caller did pass --all; the message must not ask for it again" + ) + assert captured.err == "", "this is not an error, so nothing goes to stderr" + + # The pointer must be to a real script: a hint at a renamed or deleted tool + # is worse than no hint, and it is read at the one moment the reader has just + # merged something and wants to know whether the resulting tree is healthy. + referenced = [t for t in captured.out.split() if t.endswith(".py")] + assert referenced, f"the message no longer points at a tool: {captured.out!r}" + for name in referenced: + assert (REPO_ROOT / name).is_file(), ( + f"the message points at {name}, which does not exist in the repo" + ) + + def test_all_still_classifies_when_paths_are_unmerged( + self, mod, tmp_path, capsys, monkeypatch + ) -> None: + """The other direction: the new early return must not swallow the normal path.""" + f = tmp_path / "x.py" + f.write_text( + "<<<<<<< HEAD\ndef ours_only():\n pass\n=======\n" + "def theirs_only():\n pass\n>>>>>>> origin/master\n", + encoding="utf-8", + ) + monkeypatch.setattr(mod, "_unmerged_paths", lambda: [str(f)]) + rc = mod.main(["--all"]) + out = capsys.readouterr().out + assert rc == 0 + assert "disjoint" in out.lower() or "KEEP BOTH" in out + assert "nothing to classify" not in out + def test_missing_file_is_an_error(self, mod, tmp_path) -> None: assert mod.main([str(tmp_path / "nope.txt")]) == 2