From 4e977e2178996574b466ac74702214a2d99def33 Mon Sep 17 00:00:00 2001 From: NopeCharon <115794254+NopeCharon@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:22:25 +0530 Subject: [PATCH 1/2] fix(work): split attempt diff lines only at newlines parse_unified_diff iterated over source.splitlines(). str.splitlines() also breaks at form feed, vertical tab, \x1c-\x1e, \x85, U+2028 and U+2029, none of which end a line in Git's patch output. Such a character inside a source line split the line in two: the remainder was either dropped, truncating the shown text, or, when it began with "-", "+" or a space, rendered as a phantom removed/added/context line. That changed the additions/deletions counts and shifted every later line number in the hunk. Form feeds are common page separators in Python, C and Lisp sources and U+2028 appears in JavaScript and JSON strings. Split on "\n" and remove a trailing "\r", which keeps the previous handling of CRLF content lines and CRLF-framed patches. Fixes #281 --- server/unified_diff.py | 5 ++++- tests/test_unified_diff_canvas.py | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/server/unified_diff.py b/server/unified_diff.py index fb969974..91929e40 100644 --- a/server/unified_diff.py +++ b/server/unified_diff.py @@ -50,7 +50,10 @@ def append_file(old_path: str = "", new_path: str = "") -> dict[str, Any] | None files.append(item) return item - for raw_line in source.splitlines(): + # Patch lines end at "\n" (or "\r\n"). str.splitlines() would also break + # content at a form feed, U+2028 or another separator inside the line. + for line in source.split("\n"): + raw_line = line.removesuffix("\r") # A hunk header declares how many old and new lines follow. Until they # are consumed, "--- x" and "+++ x" are removed/added content such as # an SQL or Lua comment, not file headers. diff --git a/tests/test_unified_diff_canvas.py b/tests/test_unified_diff_canvas.py index 2edd3f30..bd267547 100644 --- a/tests/test_unified_diff_canvas.py +++ b/tests/test_unified_diff_canvas.py @@ -183,6 +183,31 @@ def test_parse_unified_diff_caps_files_without_merging_hunks() -> None: assert parsed["files"][0]["deletions"] == 1 +def test_parse_unified_diff_splits_hunk_lines_only_at_newlines() -> None: + parsed = parse_unified_diff( + "diff --git a/app.py b/app.py\n" + "--- a/app.py\n" + "+++ b/app.py\n" + "@@ -1,4 +1,4 @@\n" + " def a():\n" + "- s = 'x\f-y'\n" + "+ s = 'x\u2028+y'\n" + " \f\n" + " windows\r\n" + ) + + file = parsed["files"][0] + assert file["additions"] == 1 + assert file["deletions"] == 1 + assert file["hunks"][0]["lines"] == [ + {"kind": "context", "oldLine": 1, "newLine": 1, "text": "def a():"}, + {"kind": "remove", "oldLine": 2, "text": " s = 'x\f-y'"}, + {"kind": "add", "newLine": 2, "text": " s = 'x\u2028+y'"}, + {"kind": "context", "oldLine": 3, "newLine": 3, "text": "\f"}, + {"kind": "context", "oldLine": 4, "newLine": 4, "text": "windows"}, + ] + + if __name__ == "__main__": test_parse_unified_diff_preserves_dual_line_numbers() test_parse_unified_diff_keeps_untracked_files_without_hunks() @@ -190,4 +215,5 @@ def test_parse_unified_diff_caps_files_without_merging_hunks() -> None: test_parse_unified_diff_keeps_marker_like_content_inside_hunks() test_diff_canvas_payload_keeps_report_for_local_mode_switch() test_parse_unified_diff_caps_files_without_merging_hunks() + test_parse_unified_diff_splits_hunk_lines_only_at_newlines() print("unified diff canvas tests passed") From 34a88a650be7732ea31b7a9871427dc8eb5d4b92 Mon Sep 17 00:00:00 2001 From: Lucas1479 Date: Wed, 4 Nov 2026 01:22:07 +1300 Subject: [PATCH 2/2] fix(work): preserve new-file patch framing and trailing content --- server/work_artifact_registry.py | 7 +- tests/test_work_artifact_registry.py | 96 +++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/server/work_artifact_registry.py b/server/work_artifact_registry.py index 6c699680..9f28df4d 100644 --- a/server/work_artifact_registry.py +++ b/server/work_artifact_registry.py @@ -475,7 +475,7 @@ async def collect_git_delta( part.rstrip("\n") for part in (committed_patch, working_patch, untracked_patch) if part - ).strip(), + ), "committed_patch": committed_patch, "working_patch": working_patch, "untracked_patch": untracked_patch, @@ -692,7 +692,8 @@ def _untracked_text_patch( display_path = str(relative_path).replace("\\", "/") old_marker = "/dev/null" new_marker = f"b/{display_path}" - lines = text.splitlines() + # Match Git line framing without consuming separators inside source lines. + lines = text.removesuffix("\n").split("\n") if text else [] patch_lines = [ f"diff --git {_quoted_patch_path(f'a/{display_path}')} {_quoted_patch_path(new_marker)}", "new file mode 100644", @@ -702,7 +703,7 @@ def _untracked_text_patch( if lines: patch_lines.append(f"@@ -0,0 +1,{len(lines)} @@") patch_lines.extend(f"+{line}" for line in lines) - if text and not text.endswith(("\n", "\r")): + if not text.endswith("\n"): patch_lines.append("\\ No newline at end of file") patches.append("\n".join(patch_lines)) return "\n".join(patches), omissions diff --git a/tests/test_work_artifact_registry.py b/tests/test_work_artifact_registry.py index 03671668..8527818d 100644 --- a/tests/test_work_artifact_registry.py +++ b/tests/test_work_artifact_registry.py @@ -15,7 +15,8 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from agent_host.work_ledger_store import WorkLedgerStore -from server.work_artifact_registry import WorkArtifactRegistry +from server.unified_diff import parse_unified_diff +from server.work_artifact_registry import WorkArtifactRegistry, _untracked_text_patch def _git(cwd: Path, *args: str) -> str: @@ -385,6 +386,99 @@ async def run() -> None: asyncio.run(run()) +@pytest.mark.parametrize( + ("content", "expected_lines", "missing_newline"), + [ + ("page\f-break\nx\u2028+y\nlast\n", ["page\f-break", "x\u2028+y", "last"], False), + ("a\v-b\x1c+c\x1d d\x1e\\e\x85 f\u2029g\n", ["a\v-b\x1c+c\x1d d\x1e\\e\x85 f\u2029g"], False), + ("", [], False), + ("\n", [""], False), + ("first\n\n", ["first", ""], False), + ("first\r\n\r\nlast\r\n", ["first", "", "last"], False), + ("first\nlast", ["first", "last"], True), + ("first\rlast\r", ["first\rlast"], True), + ], + ids=[ + "embedded-separators", "control-characters", "empty", "blank", + "trailing-blank", "crlf", "no-final-newline", "bare-cr", + ], +) +def test_untracked_patch_roundtrip_preserves_git_lines( + tmp_path: Path, content: str, expected_lines: list[str], missing_newline: bool, +) -> None: + (tmp_path / "new file.txt").write_bytes(content.encode("utf-8")) + + patch, omissions = _untracked_text_patch(tmp_path, ["new file.txt"]) + parsed = parse_unified_diff( + patch, changed_files=["new file.txt"], untracked=["new file.txt"], + ) + + assert omissions == [] + assert parsed["fileCount"] == 1 + assert parsed["additions"] == len(expected_lines) + assert parsed["deletions"] == 0 + file = parsed["files"][0] + assert file["path"] == "new file.txt" + assert file["status"] == "added" + if not expected_lines: + assert file["hunks"] == [] + return + hunk, = file["hunks"] + assert hunk["oldCount"] == 0 + assert hunk["newCount"] == len(expected_lines) + expected = [ + {"kind": "add", "newLine": number, "text": text} + for number, text in enumerate(expected_lines, 1) + ] + if missing_newline: + expected.append({"kind": "meta", "text": "\\ No newline at end of file"}) + assert hunk["lines"] == expected + + +@pytest.mark.parametrize("storage", ["git-untracked", "git-tracked", "filesystem-new"]) +def test_attempt_diff_preserves_newlines_and_trailing_content(tmp_path: Path, storage: str) -> None: + async def run() -> None: + root = tmp_path / "workspace" + root.mkdir() + target = root / "new file.txt" + if storage.startswith("git-"): + _git(root, "init") + _git(root, "config", "user.email", "amadeus-test@example.invalid") + _git(root, "config", "user.name", "Amadeus Test") + (root / "README.md").write_bytes(b"baseline\n") + if storage == "git-tracked": + target.write_bytes(b"old\n") + _git(root, "add", ".") + _git(root, "commit", "-m", "baseline") + with WorkLedgerStore(tmp_path / "ledger.sqlite3") as store: + project = store.create_or_get_project(root) + item = store.create_work_item(project.project_id, title="Generate file") + attempt = store.create_attempt(item.work_item_id, provider="codex", task="Generate file") + registry = WorkArtifactRegistry(store) + await registry.capture_baseline(attempt, item) + target.write_bytes("page\f-break\nx\u2028+y\nlast\f \n".encode("utf-8")) + current_attempt = store.get_attempt(attempt.attempt_id) + assert current_attempt is not None + delta = await registry.finalize_attempt(current_attempt, item) + + assert delta["available"] is True + parsed = parse_unified_diff(delta["patch"], changed_files=delta["changed_files"]) + assert parsed["fileCount"] == 1 + assert parsed["additions"] == 3 + assert parsed["deletions"] == (1 if storage == "git-tracked" else 0) + file = parsed["files"][0] + assert file["path"] == "new file.txt" + assert [ + line for hunk in file["hunks"] for line in hunk["lines"] if line["kind"] == "add" + ] == [ + {"kind": "add", "newLine": 1, "text": "page\f-break"}, + {"kind": "add", "newLine": 2, "text": "x\u2028+y"}, + {"kind": "add", "newLine": 3, "text": "last\f "}, + ] + + asyncio.run(run()) + + def _main() -> None: test_committed_delta_survives_and_baseline_dirty_is_not_stolen() test_changed_preexisting_dirty_path_is_ambiguous_not_claimed_exact()