From 96fe6a4bbe3db36ecccc9ef611985d8ce7c8e101 Mon Sep 17 00:00:00 2001 From: NopeCharon <115794254+NopeCharon@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:00:54 +0530 Subject: [PATCH] fix(work): count changes past the line cap in the attempt diff canvas parse_unified_diff caps the rendered hunk lines at max_lines and marks the result as truncated. The additions/deletions counters were incremented after that cap check, so once the cap was reached every later changed line was dropped from the counts as well as from the rendered lines. A file whose changes started after the cap was listed in the attempt diff canvas as +0 -0, the file crossing the cap showed only its first changes, and the canvas summary under-reported the attempt's totals. Count added and removed lines before the cap check, so the cap bounds only which line items are stored. lineCount still reports the rendered lines, and truncated still marks the clipped hunks. Add a regression test where the cap falls inside the first file: both files keep their real counts, and the rendered lines stop at the cap. Fixes #298 --- server/unified_diff.py | 9 ++++++-- tests/test_unified_diff_canvas.py | 38 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/server/unified_diff.py b/server/unified_diff.py index 9e21f1e0..adacd839 100644 --- a/server/unified_diff.py +++ b/server/unified_diff.py @@ -152,6 +152,13 @@ def append_file(old_path: str = "", new_path: str = "") -> dict[str, Any] | None old_remaining = max(0, old_remaining - 1) if prefix in {" ", "+"}: new_remaining = max(0, new_remaining - 1) + # The line cap bounds the rendered hunk lines only. File and patch + # totals still count every change, so a file after the cap is not + # listed as +0 / -0. + if prefix == "+": + current_file["additions"] += 1 + elif prefix == "-": + current_file["deletions"] += 1 if parsed_lines >= max_lines: truncated = True continue @@ -161,12 +168,10 @@ def append_file(old_path: str = "", new_path: str = "") -> dict[str, Any] | None kind = "add" line_item = {"kind": kind, "newLine": new_line, "text": text} new_line += 1 - current_file["additions"] += 1 elif prefix == "-": kind = "remove" line_item = {"kind": kind, "oldLine": old_line, "text": text} old_line += 1 - current_file["deletions"] += 1 elif prefix == " ": kind = "context" line_item = { diff --git a/tests/test_unified_diff_canvas.py b/tests/test_unified_diff_canvas.py index 2a1f5e23..bc0970a4 100644 --- a/tests/test_unified_diff_canvas.py +++ b/tests/test_unified_diff_canvas.py @@ -216,6 +216,43 @@ def test_parse_unified_diff_caps_files_without_merging_hunks() -> None: assert parsed["files"][0]["deletions"] == 1 +def test_parse_unified_diff_counts_changes_past_the_line_cap() -> None: + patch = "\n".join( + [ + "diff --git a/data.txt b/data.txt", + "--- a/data.txt", + "+++ b/data.txt", + "@@ -1,2 +1,3 @@", + " keep", + "-old", + "+new", + "+extra", + "diff --git a/app.py b/app.py", + "--- a/app.py", + "+++ b/app.py", + "@@ -1 +1 @@", + "-before", + "+after", + ] + ) + parsed = parse_unified_diff(patch, max_lines=2) + + assert parsed["truncated"] is True + assert parsed["lineCount"] == 2 + assert [(item["path"], item["additions"], item["deletions"]) for item in parsed["files"]] == [ + ("data.txt", 2, 1), + ("app.py", 1, 1), + ] + assert parsed["additions"] == 3 + assert parsed["deletions"] == 2 + data, app = parsed["files"] + assert data["hunks"][0]["lines"] == [ + {"kind": "context", "oldLine": 1, "newLine": 1, "text": "keep"}, + {"kind": "remove", "oldLine": 2, "text": "old"}, + ] + assert app["hunks"][0]["lines"] == [] + + 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" @@ -330,6 +367,7 @@ def test_parse_unified_diff_reads_copy_headers() -> None: test_parse_unified_diff_keeps_spaced_paths_without_marker_lines() test_diff_canvas_payload_keeps_report_for_local_mode_switch() test_parse_unified_diff_caps_files_without_merging_hunks() + test_parse_unified_diff_counts_changes_past_the_line_cap() test_parse_unified_diff_splits_hunk_lines_only_at_newlines() test_parse_unified_diff_reads_empty_lines_inside_hunks_as_blank_context() test_parse_unified_diff_reads_copy_headers()