Files
Claude 7a510a926d refactor: organize pilot packages
Group review, feedback, evaluation, observability, and entrypoint code into packages. Keep thin top-level compatibility shims for existing scripts and imports, and mirror the structure in the tests.
2026-09-01 00:59:51 +00:00

339 lines
12 KiB
Python

"""Unit tests for pragent pilot diff_compress. No network."""
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
sys.path.insert(0, os.path.join(ROOT, "pilot"))
import diff_compress # noqa: E402
from diff_compress import compress_diff, extract_finding_bullets # noqa: E402
# ---------------------------------------------------------------------------
# compress_diff
# ---------------------------------------------------------------------------
_DIFF = """\
diff --git a/src/a.py b/src/a.py
index 1..2 100644
--- a/src/a.py
+++ b/src/a.py
@@ -1,20 +1,21 @@
ctx1
-removed
+added
ctx2
ctx3
ctx4
ctx5
ctx6
ctx7
ctx8
ctx9
ctx10
ctx11
ctx12
ctx13
ctx14
ctx15
ctx16
+extra
ctx17
@@ -20,3 +21,4 @@
tail1
tail2
+tail3
tail4
diff --git a/binary.bin b/binary.bin
new file mode 100644
index 0..1
Binary files differ
"""
def test_compress_diff_default_context_two():
text, orig, kept = compress_diff(_DIFF, context=2)
# +/- lines preserved
assert "+added" in text
assert "-removed" in text
assert "+extra" in text
assert "+tail3" in text
# 2 context lines around +/- kept, the rest collapsed
assert "ctx2" in text and "ctx3" in text
assert "ctx4" not in text # outside the +/- window
# Binary files pass through
assert "Binary files differ" in text
# File headers preserved
assert "diff --git a/src/a.py b/src/a.py" in text
assert orig > kept
def test_compress_diff_context_zero_strips_context():
text, orig, kept = compress_diff(_DIFF, context=0)
assert "+added" in text and "-removed" in text and "+extra" in text
# Context lines dropped (only +/- survive)
assert " ctx1" not in text
assert "ctx2" not in text
assert orig > kept
def test_compress_diff_negative_disables_compression():
text, orig, kept = compress_diff(_DIFF, context=-1)
assert text == _DIFF
assert orig == kept
def test_compress_diff_collapsed_gap_splits_into_two_hunks():
# Two +/- lines separated by 14 context lines, context=2. The dropped
# middle is expressed by SPLITTING the hunk in two, each with a recomputed
# `@@` header — not by a pseudo-marker line. `parse_diff_anchors` reads
# `@@` headers to reset its line counter, so anything that looks like a
# header but isn't one silently misanchors every following comment.
middle = "\n".join(f" m{i}" for i in range(14)) + "\n" # trailing \n!
diff = (
"diff --git a/x.py b/x.py\n"
"--- a/x.py\n"
"+++ b/x.py\n"
"@@ -1,21 +1,23 @@\n"
+ " c1\n c2\n" # ctx near +a (kept with context=2)
+ "+a\n"
+ middle
+ "+b\n"
+ " c1\n c2\n" # ctx near +b (kept with context=2)
)
text, _, _ = compress_diff(diff, context=2)
assert "+a" in text and "+b" in text
for m in ("m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "m10", "m11"):
assert f" {m}\n" not in text # the gap itself is gone
# Two hunks, and every emitted header is a real unified-diff header.
headers = [ln for ln in text.splitlines() if ln.startswith("@@")]
assert len(headers) == 2
assert all(re.match(r"^@@ -\d+,\d+ \+\d+,\d+ @@", h) for h in headers)
def test_compress_diff_strips_no_newline_marker():
diff = (
"diff --git a/x.py b/x.py\n"
"--- a/x.py\n"
"+++ b/x.py\n"
"@@ -1,2 +1,2 @@\n"
" a\n"
"-b\n"
"\\ No newline at end of file\n"
"+c\n"
"\\ No newline at end of file\n"
)
text, _, _ = compress_diff(diff, context=2)
assert "\\ No newline" not in text
assert "-b" in text and "+c" in text
def test_compress_diff_empty_and_none():
text, orig, kept = compress_diff("", context=2)
assert text == ""
assert orig == 0 and kept == 0
text, orig, kept = compress_diff(None, context=2) # type: ignore[arg-context]
assert text == ""
assert orig == 0 and kept == 0
def test_compress_diff_pure_context_hunk_drops_body():
# A hunk that's *only* context lines (rare but legal — `git diff` emits
# these when the post-image differs only in whitespace outside the visible
# hunk) collapses entirely: file headers stay, the empty hunk header
# itself drops. The reviewer doesn't need to re-read unchanged code.
diff = (
"diff --git a/x.py b/x.py\n"
"--- a/x.py\n"
"+++ b/x.py\n"
"@@ -1,3 +1,3 @@\n"
" a\n"
" b\n"
" c\n"
)
text, _, _ = compress_diff(diff, context=2)
assert text == "diff --git a/x.py b/x.py\n--- a/x.py\n+++ b/x.py\n"
assert "@@ -1,3" not in text # empty hunk header dropped
def test_compress_diff_wide_window_keeps_more_context():
narrow, _, _ = compress_diff(_DIFF, context=0)
wide, _, wide_kept = compress_diff(_DIFF, context=10)
assert wide_kept > len(narrow)
# ---------------------------------------------------------------------------
# extract_finding_bullets
# ---------------------------------------------------------------------------
_BODY = """\
🤖 **AI Review** · pragent pilot · glm-5.2:cloud · `abcdef12`
Adds the salvavoid void-death item-rescue module. Risk is moderate on the
PlayerDeathEvent item/inventory path. New findings (not in prior review):
orphaned chest left in world on rescue failure, missing module-enabled check.
- **[HIGH]** `src/main/java/dev/marcospaulo/canalhandia/VoidProtection.java:162` — drop duplication race. fix: use ItemMeta to write inventory once. (ref: https://example.com)
- **[MEDIUM]** `src/main/java/dev/marcospaulo/canalhandia/VoidProtection.java:67` — O(n^2) spiral. fix: cap radius. (https://example.com/spiral)
- **[LOW]** `src/main/java/dev/marcospaulo/canalhandia/VoidProtection.java:3` — package-info javadoc missing.
_4 inline comment(s) posted below._
<!-- pragent:sha=abcdef1234567890 -->
"""
def test_extract_finding_bullets_basic():
bs = extract_finding_bullets(_BODY)
assert len(bs) == 3
assert any("HIGH" in b and "VoidProtection.java:162" in b for b in bs)
assert any("MEDIUM" in b for b in bs)
assert any("LOW" in b for b in bs)
def test_extract_finding_bullets_drops_prose():
bs = extract_finding_bullets(_BODY)
joined = "\n".join(bs)
# The summary prose is dropped.
assert "Adds the salvavoid" not in joined
assert "PlayerDeathEvent item/inventory path" not in joined
# The inline-comment footer is dropped.
assert "inline comment(s) posted below" not in joined
# The sha marker is dropped.
assert "pragent:sha=" not in joined
def test_extract_finding_bullets_accepts_lowercase_summary_bullets():
# `summary_bullets` renders `- **[HIGH]**` (bold); older reviews used
# `- [high]` (plain). Both should match.
text = (
"- [critical] `a.py:1` — bug. fix: fix it.\n"
"- **[HIGH]** `b.go:9` — race.\n"
)
bs = extract_finding_bullets(text)
assert len(bs) == 2
assert "CRITICAL" in bs[0].upper() or "critical" in bs[0]
assert "HIGH" in bs[1]
def test_extract_finding_bullets_empty_and_prose_only():
assert extract_finding_bullets("") == []
assert extract_finding_bullets(" \n \n") == []
assert extract_finding_bullets("Just some prose, no bullets here.") == []
assert extract_finding_bullets("- This is a regular bullet, not a finding.") == []
def test_extract_finding_bullets_keeps_indented_subbullets():
# A finding may carry continuation lines below it (rare in pragent output
# but legal). We only pull the matching line itself — sub-bullets stay
# with their parent as prose.
text = (
"- **[HIGH]** `a.py:1` — bug.\n"
" sub-bullet continuation that the reviewer wrote\n"
"- **[LOW]** `b.go:2` — nit.\n"
)
bs = extract_finding_bullets(text)
assert len(bs) == 2
assert all("sub-bullet continuation" not in b for b in bs)
def test_compress_diff_preserves_anchors_for_post_change_lines():
# Sanity: a finding anchored on a context line that compress_diff keeps
# must still be a valid anchor after compression. We re-run the parser the
# ai_review core uses, so a regression here surfaces as misanchored
# inline comments in production.
import ai_review
diff = (
"diff --git a/x.py b/x.py\n"
"--- a/x.py\n"
"+++ b/x.py\n"
"@@ -10,4 +10,5 @@\n"
" ctx_a\n"
" ctx_b\n"
"+new\n"
" ctx_c\n"
" ctx_d\n"
)
text, _, _ = compress_diff(diff, context=1)
anchors = ai_review.parse_diff_anchors(text)
assert 12 in anchors["x.py"] # +new
# ctx_a is within 1 line of +new at line 12, so kept.
assert 11 in anchors["x.py"]
def test_compress_diff_keeps_post_change_line_numbers_exact():
# The regression that motivated the hunk-header rewrite: dropping context
# lines without renumbering shifted every anchor. Here `+new` really is
# line 10 of the post-change file; compression must not move it.
raw = (
"diff --git a/x.py b/x.py\n"
"--- a/x.py\n"
"+++ b/x.py\n"
"@@ -1,12 +1,12 @@\n"
+ "".join(f" l{i}\n" for i in range(1, 10))
+ "-old\n"
+ "+new\n"
+ " l11\n"
)
import ai_review
raw_anchors = ai_review.parse_diff_anchors(raw)["x.py"]
assert 10 in raw_anchors # +new
text, _, _ = compress_diff(raw, context=1)
comp_anchors = ai_review.parse_diff_anchors(text)["x.py"]
# Compression only ever drops anchors; it never invents or moves one.
assert comp_anchors <= raw_anchors
assert 10 in comp_anchors # +new still anchors to its real line
def test_compress_diff_content_line_starting_with_dashes_is_not_a_header():
# A removed YAML document separator renders as `----`; an added one as
# `+++new`. Treating those as file headers truncated the hunk body and
# dropped the `@@` header with it.
diff = (
"diff --git a/x.yml b/x.yml\n"
"--- a/x.yml\n"
"+++ b/x.yml\n"
"@@ -1,4 +1,4 @@\n"
" a: 1\n"
" b: 2\n"
"----\n"
"+++new\n"
" c: 3\n"
)
text, _, _ = compress_diff(diff, context=1)
assert "----" in text and "+++new" in text
# The hunk header survives, so the body is still anchorable.
headers = [ln for ln in text.splitlines() if _is_hunk_header(ln)]
assert len(headers) == 1
import ai_review
assert ai_review.parse_diff_anchors(text)["x.yml"] == {2, 3, 4}
def _is_hunk_header(line: str) -> bool:
return bool(re.match(r"^@@ -\d+,\d+ \+\d+,\d+ @@", line))
def test_extract_finding_bullets_matches_current_renderer_output():
# The prior-review dedupe is only worth anything if it can read the
# bullets pragent itself posts. `summary_bullets` renders an emoji badge
# between the `-` and the `[SEV]` tag, which the original regex rejected.
import ai_review
findings = [
{"path": "a.py", "line": 10, "severity": "high",
"problem": "boom", "fix": "guard it", "suggestion": "", "reference": ""},
{"path": "b.go", "line": 0, "severity": "low",
"problem": "nit", "fix": "", "suggestion": "", "reference": ""},
]
body = ai_review.format_review_body(
ai_review.summary_bullets(findings), "m", "abc123",
findings_for_table=findings,
)
bullets = extract_finding_bullets(body)
assert len(bullets) == 2
assert any("a.py:10" in b and "boom" in b for b in bullets)
# `**Fix:**` continuation lines are prose, not findings.
assert all("**Fix:**" not in b for b in bullets)
assert ai_review.compact_prior_reviews([body]) != []