From 33df7f1e072a47f4948ebc7c743484d4a047cd48 Mon Sep 17 00:00:00 2001 From: NopeCharon <115794254+NopeCharon@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:41:01 +0530 Subject: [PATCH] fix(work): read attempt diff paths literally, not as Git pathspecs collect_git_delta builds the committed and working attempt patches with `git diff ... -- `. Git interprets those arguments as pathspecs, so glob characters and pathspec magic in a filename changed which files were diffed: - a changed `[id].tsx` also matched `i.tsx` and `d.tsx`, so a pre-existing user edit that the baseline filter had correctly listed under excluded_baseline_paths was still written into the attempt patch and shown in the attempt diff canvas as part of the Work; - a changed file whose name starts with `:` was read as pathspec magic, matched nothing, and its changes were missing from the patch. The paths always come from Git's own status and name-only output, so they are literal repository paths. run_git, which already pins core.quotepath=false for machine consumers, now also passes --literal-pathspecs. None of its callers passes a pattern. Adds a regression test in which an attempt edits `[id].tsx` while a pre-existing dirty `i.tsx` stays untouched; the attempt patch must list only `[id].tsx`. Fixes #300 --- server/local_git.py | 4 +++ tests/test_work_artifact_registry.py | 43 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/server/local_git.py b/server/local_git.py index d72310de..d7052aa8 100644 --- a/server/local_git.py +++ b/server/local_git.py @@ -34,10 +34,14 @@ async def run_git(cwd: str, args: list[str]) -> dict[str, Any]: kwargs["creationflags"] = CREATE_NO_WINDOW # Git quotes non-ASCII pathnames by default. Machine consumers must see # repository paths, not display-oriented C-style escape text. + # Paths passed after "--" are repository paths taken from Git's own + # output, never patterns: a name such as "[id].tsx" must not glob-match + # "i.tsx", and a leading ":" must not be read as pathspec magic. proc = await asyncio.create_subprocess_exec( "git", "-c", "core.quotepath=false", + "--literal-pathspecs", *args, **kwargs, ) diff --git a/tests/test_work_artifact_registry.py b/tests/test_work_artifact_registry.py index e1d171b0..56f8f2fc 100644 --- a/tests/test_work_artifact_registry.py +++ b/tests/test_work_artifact_registry.py @@ -210,6 +210,49 @@ async def run() -> None: asyncio.run(run()) +def test_attempt_patch_reads_changed_paths_literally_not_as_globs() -> None: + async def run() -> None: + with tempfile.TemporaryDirectory(prefix="work_artifact_literal_paths_") as temp: + root = Path(temp) / "project" + root.mkdir() + _git(root, "init") + _git(root, "config", "user.email", "amadeus-test@example.invalid") + _git(root, "config", "user.name", "Amadeus Test") + route = root / "[id].tsx" + route.write_text("route\n", encoding="utf-8") + (root / "i.tsx").write_text("clean\n", encoding="utf-8") + _git(root, "add", ".") + _git(root, "commit", "-m", "baseline") + + # As a glob, "[id].tsx" also matches this pre-existing user edit. + (root / "i.tsx").write_text("dirty before run\n", encoding="utf-8") + store = WorkLedgerStore(Path(temp) / "ledger.sqlite3") + project = store.create_or_get_project(root) + item = store.create_work_item(project.project_id, title="Edit route") + attempt = store.create_attempt(item.work_item_id, provider="codex", task="Edit route") + registry = WorkArtifactRegistry(store) + await registry.capture_baseline(attempt, item) + route.write_text("route changed\n", encoding="utf-8") + + current_attempt = store.get_attempt(attempt.attempt_id) + assert current_attempt is not None + delta = await registry.finalize_attempt(current_attempt, item) + store.close() + + assert delta["changed_files"] == ["[id].tsx"] + assert delta["excluded_baseline_paths"] == ["i.tsx"] + parsed = parse_unified_diff( + delta["patch"], + changed_files=delta["changed_files"], + untracked=delta["untracked"], + ) + assert [(entry["path"], entry["additions"], entry["deletions"]) for entry in parsed["files"]] == [ + ("[id].tsx", 1, 1), + ] + + asyncio.run(run()) + + def test_non_ascii_entry_revision_is_registered_as_the_real_path() -> None: async def run() -> None: with tempfile.TemporaryDirectory(prefix="work_artifact_unicode_") as temp: