fix(review): correct diff-compression line numbers, prior-review dedupe, triage skip

Four defects, all found reviewing PR #9 (two of them by pragent-bot's own
review of that PR, which the anchoring bug then misplaced):

* compress_diff dropped context lines but copied the original `@@` hunk
  header verbatim, so the header no longer described the lines beneath it.
  parse_diff_anchors then walked stale headers and produced anchor sets
  shifted by the number of elided lines, misplacing inline comments or
  demoting them to bullets. Each surviving run of lines is now re-emitted as
  its own hunk with a recomputed `@@ -a,b +c,d @@`, so the output stays a
  valid unified diff whose numbers describe the real post-change file. The
  pseudo-marker `@@ … N context line(s) omitted … @@` is gone; it parsed as
  a hunk header and reset the anchor counter to 0. Anchoring additionally
  runs on the raw diff now, so the prompt window can never shrink the
  anchorable set.

* compress_diff's `_FILE_HEADER` regex matched diff *body* lines: a removed
  YAML `---` separator or an added `++` line was read as a file header,
  truncating the hunk and dropping its `@@` header with it. Body detection
  is now prefix-based, with a full-shape hunk-header regex.

* extract_finding_bullets could not match the bullets pragent itself posts:
  summary_bullets renders an emoji severity badge between the `-` and the
  `[SEV]` tag, which the regex rejected, so compact_prior_reviews always
  returned [] and every re-review repeated its previous findings.

* triage returning `{"lenses":[]}` — documented in .opencode/agents/triage.md
  as "no lens has surface, skip the fan-out" — ran every lens instead, since
  _intersect_with_triage mapped an empty selection to "all" and the call site
  had a second `or reviewers` fallback. `[]` and None are now distinct
  outcomes: `[]` skips, None fails open. A roster naming only unknown lens
  ids now fails open rather than silencing the review. The skip path returns
  a well-formed empty-findings response instead of "", which had landed in
  ai_review's unparseable-output branch and posted "AI review produced no
  parseable output" — a malfunction message for a normal verdict.

Also: non-URL references (a CVE id, a doc title) rendered as
`[CVE-2024-1234](CVE-2024-1234)`, a broken relative link in Gitea — now
plain text. PRAGENT_DIFF_CONTEXT and friends parse through _int_env, so a
typo logs and falls back instead of killing a review mid-flight. Removed
format_usage_section, dead since the collapsible usage block replaced it and
carrying a duplicate copy of the price-target logic.

Tests: 290 -> 301. New coverage for hunk-header fidelity before/after
compression, header-shaped content lines, the bullet round-trip against the
real renderer, and triage's three outcomes (previously untested).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B11e8TZZxJyzHW7jj7KWUN
This commit is contained in:
Marcos
2026-08-20 23:05:03 +00:00
parent 78bcf6a9a0
commit 2b1cf750b7
7 changed files with 526 additions and 235 deletions
+87 -2
View File
@@ -750,10 +750,14 @@ def test_intersect_with_triage_preserves_order():
assert [r.id for r in out] == ["security", "docs"]
def test_intersect_with_triage_none_returns_all():
def test_intersect_with_triage_none_fails_open_but_empty_selects_nothing():
# The two must NOT be conflated: None is "triage gave no verdict, run
# everything"; [] is "triage says no lens has surface", which the caller
# short-circuits on. Returning all lenses for [] made a skip verdict run
# every lens instead.
reviewers = [oc.ReviewerSpec(id="security"), oc.ReviewerSpec(id="docs")]
assert oc._intersect_with_triage(reviewers, None) == reviewers
assert oc._intersect_with_triage(reviewers, []) == reviewers
assert oc._intersect_with_triage(reviewers, []) == []
def test_merge_usage_sums_tokens():
@@ -772,3 +776,84 @@ def test_merge_usage_skips_none():
merged = oc.merge_usage([a, None, None])
assert merged["input"] == 100
assert merged["steps"] == 3
# ---------------------------------------------------------------------------
# triage(): the empty-list verdict must survive as its own outcome
# ---------------------------------------------------------------------------
def _stub_triage_env(monkeypatch, agent_output: str):
"""Make `triage()` runnable in-process: no opencode binary, no HOME setup."""
class _Proc:
stdout = "irrelevant — parse_opencode_events is stubbed"
stderr = ""
returncode = 0
monkeypatch.setattr(oc, "_opencode_bin", lambda: "/bin/true")
monkeypatch.setattr(oc, "_shared_home", lambda: "/tmp")
monkeypatch.setattr(oc, "_warm_opencode", lambda home, model: None)
monkeypatch.setattr(oc, "_build_env", lambda home: {})
monkeypatch.setattr(oc.subprocess, "run", lambda *a, **k: _Proc())
monkeypatch.setattr(oc, "parse_opencode_events", lambda raw: (agent_output, None))
_TRIAGE_CFG = {"enabled": True, "model": "", "max_lenses": 5}
def test_triage_empty_list_is_a_skip_verdict(monkeypatch):
_stub_triage_env(monkeypatch, '{"lenses":[]}')
reviewers = [oc.ReviewerSpec(id="security"), oc.ReviewerSpec(id="docs")]
out = oc.triage("/tmp", _TRIAGE_CFG, reviewers, "m", "/tmp")
# [] — NOT None. None would fail open and run every lens.
assert out == []
assert out is not None
def test_triage_unknown_lens_ids_fail_open(monkeypatch):
# A hallucinated roster is a bad answer, not a verdict of "nothing to
# review" — it must fail open rather than silence the whole review.
_stub_triage_env(monkeypatch, '{"lenses":["not-a-lens","also-fake"]}')
reviewers = [oc.ReviewerSpec(id="security"), oc.ReviewerSpec(id="docs")]
assert oc.triage("/tmp", _TRIAGE_CFG, reviewers, "m", "/tmp") is None
def test_triage_valid_subset_selected(monkeypatch):
_stub_triage_env(monkeypatch, '{"lenses":["docs","nope"]}')
reviewers = [oc.ReviewerSpec(id="security"), oc.ReviewerSpec(id="docs")]
assert oc.triage("/tmp", _TRIAGE_CFG, reviewers, "m", "/tmp") == ["docs"]
def test_triage_disabled_fails_open(monkeypatch):
_stub_triage_env(monkeypatch, '{"lenses":[]}')
reviewers = [oc.ReviewerSpec(id="security")]
cfg = {"enabled": False, "model": "", "max_lenses": 5}
assert oc.triage("/tmp", cfg, reviewers, "m", "/tmp") is None
def test_triage_malformed_output_fails_open(monkeypatch):
_stub_triage_env(monkeypatch, "the agent wrote prose instead of JSON")
reviewers = [oc.ReviewerSpec(id="security")]
assert oc.triage("/tmp", _TRIAGE_CFG, reviewers, "m", "/tmp") is None
def test_no_surface_response_parses_as_an_empty_review():
# The skip path must return the same shape every other path returns.
# A bare "" landed in ai_review's unparseable-output branch and posted
# "AI review produced no parseable output" — a malfunction, not a verdict.
import ai_review
text, usage = oc._no_surface_response("o/r", "9", "abc12345", 3)
assert usage is None
summary, findings, _changes, _risks = ai_review.parse_review_output(text)
assert findings == []
assert summary # non-empty, so ai_review does NOT take the salvage branch
assert "no review surface" in summary.lower()
assert "3 configured lens" in summary
def test_no_surface_response_zero_lenses_wording():
import ai_review
text, _ = oc._no_surface_response("o/r", "9", "abc12345", 0)
summary, findings, _c, _r = ai_review.parse_review_output(text)
assert findings == []
assert "after path filtering" in summary