diff --git a/backend/packages/harness/deerflow/workspace_changes/diff.py b/backend/packages/harness/deerflow/workspace_changes/diff.py index ef9d53067..a28c875f6 100644 --- a/backend/packages/harness/deerflow/workspace_changes/diff.py +++ b/backend/packages/harness/deerflow/workspace_changes/diff.py @@ -198,12 +198,12 @@ def _snapshot_text(file: FileSnapshot | None) -> str | None: def _count_diff_lines(lines: list[str]) -> tuple[int, int]: additions = 0 deletions = 0 - for line in lines: - # Unified-diff file headers are "+++ " / "--- " with a trailing space; - # a bare "+++"/"---" prefix would also swallow real content lines whose - # text begins with those sequences (e.g. an added line "+++foo"). - if line.startswith("+++ ") or line.startswith("--- "): - continue + # difflib.unified_diff always emits the two file headers ("--- a/…" then + # "+++ b/…") first; skip them by position. A prefix test can't tell them + # apart from a hunk-body line whose content starts with "-- "/"++ " (e.g. a + # deleted SQL comment "-- get users" becomes the diff line "--- get users"), + # which would then be dropped from the count. + for line in lines[2:]: if line.startswith("+"): additions += 1 elif line.startswith("-"): diff --git a/backend/tests/test_workspace_changes.py b/backend/tests/test_workspace_changes.py index 7648686b6..2ec7d868d 100644 --- a/backend/tests/test_workspace_changes.py +++ b/backend/tests/test_workspace_changes.py @@ -182,6 +182,33 @@ def test_count_diff_lines_ignores_only_real_headers(): assert deletions == 2 +def test_count_diff_lines_counts_content_starting_with_dashes_or_pluses(): + """Hunk-body lines whose content starts with '-- '/'++ ' must be counted. + + difflib prefixes a deleted line "-- get users" to "--- get users"; the old + prefix skip mistook that for a file header and dropped it, undercounting + deletions in the user-visible +N/-M summary. + """ + import difflib + + from deerflow.workspace_changes.diff import _count_diff_lines + + lines = list( + difflib.unified_diff( + ["SELECT 1", "-- get users"], + ["SELECT 1", "SELECT 2"], + fromfile="a/x.sql", + tofile="b/x.sql", + lineterm="", + ) + ) + + additions, deletions = _count_diff_lines(lines) + + assert additions == 1 + assert deletions == 1 + + def test_scan_workspace_roots_skips_excluded_directories(tmp_path): roots = _roots(tmp_path) workspace = roots[0].host_path