mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-03 20:13:21 +00:00
feat: surface out-of-diff findings in a collapsed dropdown (#1427)
* fix: stop reviewer retrying out-of-diff findings add_finding rejects findings anchored outside the PR diff, but the agent retried the same finding 2-3x with adjacent line ranges before giving up, burning a model turn each. Add a reviewer-prompt recovery block telling the agent the rejection is authoritative (drop or re-anchor to a + line, don't retry adjacent), and enrich the rejection payload with nearby in-diff line ranges for the file so a single re-anchor needs no guessing. * feat: surface out-of-diff findings in a collapsed dropdown Instead of rejecting findings anchored outside the PR diff, accept them (marked in_diff=false) and surface them in a collapsed <details> section of the review summary, Devin-style. Inline comments stay reserved for in-diff findings; out-of-diff are severity-gated and capped the same way. Re-review normally suppresses the empty summary, but now makes an exception when there are new out-of-diff findings to surface. Surfaced out-of-diff findings carry a github_review_id so they aren't reposted on later pushes. Supersedes the earlier 'drop/re-anchor out-of-diff' prompt guidance. --------- Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
This commit is contained in:
parent
d2b3cb01c0
commit
f64ab2bcd7
7 changed files with 216 additions and 29 deletions
|
|
@ -100,6 +100,13 @@ If `publish_review` returns `unresolvable_findings`, do NOT retry with the
|
||||||
same args — call `update_finding(status="resolved", note="...")` on those ids, or fix
|
same args — call `update_finding(status="resolved", note="...")` on those ids, or fix
|
||||||
their file/line via `update_finding`, then call `publish_review` again.
|
their file/line via `update_finding`, then call `publish_review` again.
|
||||||
|
|
||||||
|
If `add_finding` returns `in_diff: false`, the finding was accepted but anchored
|
||||||
|
outside the PR diff (e.g. a caller broken by a changed signature, in a file the
|
||||||
|
PR doesn't touch). It will be surfaced in a collapsed "out-of-diff findings"
|
||||||
|
section of the review summary instead of as an inline comment. This is expected —
|
||||||
|
do NOT re-anchor, retry, or drop it. Only file such findings when they clear the
|
||||||
|
bar below (a proven regression, not speculation about pre-existing code).
|
||||||
|
|
||||||
Re-review: for each open finding, `update_finding(id, status="resolved", note="...")`
|
Re-review: for each open finding, `update_finding(id, status="resolved", note="...")`
|
||||||
if fixed (include a brief explanation of the fix in `note`), `update_finding` with
|
if fixed (include a brief explanation of the fix in `note`), `update_finding` with
|
||||||
new fields + `note` if changed, otherwise do nothing. Add net-new findings with
|
new fields + `note` if changed, otherwise do nothing. Add net-new findings with
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ class Finding(TypedDict, total=False):
|
||||||
start_line: int | None
|
start_line: int | None
|
||||||
end_line: int | None
|
end_line: int | None
|
||||||
side: DiffSide
|
side: DiffSide
|
||||||
|
in_diff: bool
|
||||||
description: str
|
description: str
|
||||||
suggestion: str | None
|
suggestion: str | None
|
||||||
status: FindingStatus
|
status: FindingStatus
|
||||||
|
|
@ -183,6 +184,7 @@ def new_finding(
|
||||||
suggestion: str | None = None,
|
suggestion: str | None = None,
|
||||||
diff_hunk: str | None = None,
|
diff_hunk: str | None = None,
|
||||||
finding_id: str | None = None,
|
finding_id: str | None = None,
|
||||||
|
in_diff: bool = True,
|
||||||
) -> Finding:
|
) -> Finding:
|
||||||
"""Construct a fully-populated ``Finding`` ready to persist."""
|
"""Construct a fully-populated ``Finding`` ready to persist."""
|
||||||
resolved_id = finding_id or new_finding_id()
|
resolved_id = finding_id or new_finding_id()
|
||||||
|
|
@ -212,6 +214,7 @@ def new_finding(
|
||||||
"start_line": start_line,
|
"start_line": start_line,
|
||||||
"end_line": end_line,
|
"end_line": end_line,
|
||||||
"side": side,
|
"side": side,
|
||||||
|
"in_diff": in_diff,
|
||||||
"description": description,
|
"description": description,
|
||||||
"suggestion": suggestion,
|
"suggestion": suggestion,
|
||||||
"status": "open",
|
"status": "open",
|
||||||
|
|
|
||||||
|
|
@ -230,18 +230,59 @@ def review_summary_marker(pr_number: int) -> str:
|
||||||
return f"<!-- open-swe-reviewer pr={pr_number} -->"
|
return f"<!-- open-swe-reviewer pr={pr_number} -->"
|
||||||
|
|
||||||
|
|
||||||
def render_review_body(*, pr_number: int, surfaced_count: int, trace_url: str | None = None) -> str:
|
def render_out_of_diff_section(findings: list[Finding]) -> str:
|
||||||
|
"""Render findings anchored outside the PR diff as a collapsed dropdown.
|
||||||
|
|
||||||
|
These can't be posted as inline comments (GitHub rejects off-diff lines), so
|
||||||
|
they live in the review summary body inside a ``<details>`` block — visible
|
||||||
|
on demand without adding noise to the changed-line review.
|
||||||
|
"""
|
||||||
|
count = len(findings)
|
||||||
|
noun = "finding" if count == 1 else "findings"
|
||||||
|
items: list[str] = []
|
||||||
|
for f in findings:
|
||||||
|
title, detail = _split_title_and_detail(
|
||||||
|
(f.get("description") or "").strip(), f.get("title")
|
||||||
|
)
|
||||||
|
location = f.get("file") or "?"
|
||||||
|
line_ref = _format_line_reference(f.get("start_line"), f.get("end_line"))
|
||||||
|
if line_ref:
|
||||||
|
location += f" {line_ref.strip('*()')}".replace("Refers to ", "")
|
||||||
|
item = f"- {_severity_emoji(f.get('severity') or 'medium')} **{title}** — `{location}`"
|
||||||
|
if detail:
|
||||||
|
item += f"\n {detail}"
|
||||||
|
items.append(item)
|
||||||
|
return (
|
||||||
|
f"<details>\n<summary>🔍 {count} out-of-diff {noun}</summary>\n\n"
|
||||||
|
"These relate to code outside this PR's changed lines.\n\n"
|
||||||
|
+ "\n".join(items)
|
||||||
|
+ "\n</details>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_review_body(
|
||||||
|
*,
|
||||||
|
pr_number: int,
|
||||||
|
surfaced_count: int,
|
||||||
|
trace_url: str | None = None,
|
||||||
|
out_of_diff_findings: list[Finding] | None = None,
|
||||||
|
) -> str:
|
||||||
"""Compose the top-level review body."""
|
"""Compose the top-level review body."""
|
||||||
if surfaced_count == 0:
|
out_of_diff_findings = out_of_diff_findings or []
|
||||||
|
if surfaced_count == 0 and not out_of_diff_findings:
|
||||||
headline = (
|
headline = (
|
||||||
"## ✅ Open SWE Review: No issues found\n\n"
|
"## ✅ Open SWE Review: No issues found\n\n"
|
||||||
"Open SWE reviewed this PR and found no potential bugs to report."
|
"Open SWE reviewed this PR and found no potential bugs to report."
|
||||||
)
|
)
|
||||||
|
elif surfaced_count == 0:
|
||||||
|
headline = "**Open SWE Review** found no issues in the changed lines."
|
||||||
else:
|
else:
|
||||||
issue_word = "issue" if surfaced_count == 1 else "issues"
|
issue_word = "issue" if surfaced_count == 1 else "issues"
|
||||||
headline = f"**Open SWE Review** found {surfaced_count} potential {issue_word}."
|
headline = f"**Open SWE Review** found {surfaced_count} potential {issue_word}."
|
||||||
|
|
||||||
parts = [headline]
|
parts = [headline]
|
||||||
|
if out_of_diff_findings:
|
||||||
|
parts.append(render_out_of_diff_section(out_of_diff_findings))
|
||||||
if trace_url:
|
if trace_url:
|
||||||
parts.append(f"[View Open SWE trace]({trace_url})")
|
parts.append(f"[View Open SWE trace]({trace_url})")
|
||||||
parts.append(review_summary_marker(pr_number))
|
parts.append(review_summary_marker(pr_number))
|
||||||
|
|
|
||||||
|
|
@ -115,16 +115,9 @@ def add_finding(
|
||||||
diff_line_set = configurable.get("diff_line_set") if isinstance(configurable, dict) else None
|
diff_line_set = configurable.get("diff_line_set") if isinstance(configurable, dict) else None
|
||||||
diff_text = configurable.get("diff_text", "") if isinstance(configurable, dict) else ""
|
diff_text = configurable.get("diff_text", "") if isinstance(configurable, dict) else ""
|
||||||
|
|
||||||
if isinstance(diff_line_set, dict) and not is_range_in_diff(
|
in_diff = not isinstance(diff_line_set, dict) or is_range_in_diff(
|
||||||
diff_line_set, file, start_line, end_line, side=_cast_side(side)
|
diff_line_set, file, start_line, end_line, side=_cast_side(side)
|
||||||
):
|
)
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": (
|
|
||||||
f"Finding range {file}:{start_line}-{end_line} is not part of the PR diff. "
|
|
||||||
"Only review changes the PR introduces; do not flag pre-existing code."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
diff_hunk: str | None = None
|
diff_hunk: str | None = None
|
||||||
if isinstance(diff_text, str) and diff_text:
|
if isinstance(diff_text, str) and diff_text:
|
||||||
|
|
@ -150,10 +143,18 @@ def add_finding(
|
||||||
side=_cast_side(side),
|
side=_cast_side(side),
|
||||||
suggestion=clipped_suggestion,
|
suggestion=clipped_suggestion,
|
||||||
diff_hunk=diff_hunk,
|
diff_hunk=diff_hunk,
|
||||||
|
in_diff=in_diff,
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.run(append_finding(thread_id, finding))
|
asyncio.run(append_finding(thread_id, finding))
|
||||||
result: dict[str, Any] = {"success": True, "finding_id": finding["id"]}
|
result: dict[str, Any] = {"success": True, "finding_id": finding["id"]}
|
||||||
|
if not in_diff:
|
||||||
|
result["in_diff"] = False
|
||||||
|
result["note"] = (
|
||||||
|
"Anchored outside the PR diff. This will be surfaced in the collapsed "
|
||||||
|
"out-of-diff section of the review summary, not as an inline comment. "
|
||||||
|
"Do not re-anchor or retry."
|
||||||
|
)
|
||||||
if suggestion_dropped:
|
if suggestion_dropped:
|
||||||
result["suggestion_dropped"] = True
|
result["suggestion_dropped"] = True
|
||||||
result["warning"] = (
|
result["warning"] = (
|
||||||
|
|
|
||||||
|
|
@ -166,8 +166,15 @@ async def _publish_review_eval_dry_run_async(
|
||||||
findings = await list_findings_async(thread_id)
|
findings = await list_findings_async(thread_id)
|
||||||
unpublished_findings = [f for f in findings if not _has_publication_identity(f)]
|
unpublished_findings = [f for f in findings if not _has_publication_identity(f)]
|
||||||
open_unpublished = [f for f in unpublished_findings if f.get("status", "open") == "open"]
|
open_unpublished = [f for f in unpublished_findings if f.get("status", "open") == "open"]
|
||||||
|
in_diff_unpublished = [f for f in unpublished_findings if f.get("in_diff", True)]
|
||||||
|
out_of_diff_unpublished = [f for f in unpublished_findings if not f.get("in_diff", True)]
|
||||||
eligible = filter_findings_for_publish(
|
eligible = filter_findings_for_publish(
|
||||||
unpublished_findings,
|
in_diff_unpublished,
|
||||||
|
severity_threshold=severity_threshold,
|
||||||
|
cap=cap,
|
||||||
|
)
|
||||||
|
eligible_out_of_diff = filter_findings_for_publish(
|
||||||
|
out_of_diff_unpublished,
|
||||||
severity_threshold=severity_threshold,
|
severity_threshold=severity_threshold,
|
||||||
cap=cap,
|
cap=cap,
|
||||||
)
|
)
|
||||||
|
|
@ -184,7 +191,10 @@ async def _publish_review_eval_dry_run_async(
|
||||||
"dry_run": True,
|
"dry_run": True,
|
||||||
"review_id": None,
|
"review_id": None,
|
||||||
"surfaced_count": len(inline_comments),
|
"surfaced_count": len(inline_comments),
|
||||||
"hidden_count": max(len(open_unpublished) - len(inline_comments), 0),
|
"out_of_diff_count": len(eligible_out_of_diff),
|
||||||
|
"hidden_count": max(
|
||||||
|
len(open_unpublished) - len(inline_comments) - len(eligible_out_of_diff), 0
|
||||||
|
),
|
||||||
"resolved_thread_count": 0,
|
"resolved_thread_count": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,8 +239,21 @@ async def _publish_review_async(
|
||||||
f for f in unpublished_findings if f.get("first_seen_sha") == head_sha
|
f for f in unpublished_findings if f.get("first_seen_sha") == head_sha
|
||||||
]
|
]
|
||||||
open_unpublished = [f for f in unpublished_findings if f.get("status", "open") == "open"]
|
open_unpublished = [f for f in unpublished_findings if f.get("status", "open") == "open"]
|
||||||
|
# In-diff findings become inline comments. Out-of-diff findings can't anchor
|
||||||
|
# to an inline comment (GitHub rejects off-diff lines), so they're surfaced
|
||||||
|
# in a collapsed dropdown in the review body. Already-surfaced out-of-diff
|
||||||
|
# findings carry a github_review_id, so they're not re-posted on re-review.
|
||||||
|
in_diff_unpublished = [f for f in unpublished_findings if f.get("in_diff", True)]
|
||||||
|
out_of_diff_unpublished = [
|
||||||
|
f
|
||||||
|
for f in unpublished_findings
|
||||||
|
if not f.get("in_diff", True) and not isinstance(f.get("github_review_id"), int)
|
||||||
|
]
|
||||||
eligible = filter_findings_for_publish(
|
eligible = filter_findings_for_publish(
|
||||||
unpublished_findings, severity_threshold=severity_threshold, cap=cap
|
in_diff_unpublished, severity_threshold=severity_threshold, cap=cap
|
||||||
|
)
|
||||||
|
eligible_out_of_diff = filter_findings_for_publish(
|
||||||
|
out_of_diff_unpublished, severity_threshold=severity_threshold, cap=cap
|
||||||
)
|
)
|
||||||
|
|
||||||
inline_comments: list[dict[str, Any]] = []
|
inline_comments: list[dict[str, Any]] = []
|
||||||
|
|
@ -252,9 +275,15 @@ async def _publish_review_async(
|
||||||
# SWE review summary) instead. Still resolve threads for findings that just
|
# SWE review summary) instead. Still resolve threads for findings that just
|
||||||
# moved to resolved, and advance last_reviewed_sha so subsequent pushes
|
# moved to resolved, and advance last_reviewed_sha so subsequent pushes
|
||||||
# don't redo the same diff.
|
# don't redo the same diff.
|
||||||
if not inline_comments and (
|
if (
|
||||||
is_re_review
|
not inline_comments
|
||||||
or await open_swe_review_exists(owner=owner, repo=repo, pr_number=pr_number, token=token)
|
and not eligible_out_of_diff
|
||||||
|
and (
|
||||||
|
is_re_review
|
||||||
|
or await open_swe_review_exists(
|
||||||
|
owner=owner, repo=repo, pr_number=pr_number, token=token
|
||||||
|
)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
resolved_thread_count = await _resolve_threads_for_resolved_findings(
|
resolved_thread_count = await _resolve_threads_for_resolved_findings(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
|
|
@ -277,6 +306,7 @@ async def _publish_review_async(
|
||||||
pr_number=pr_number,
|
pr_number=pr_number,
|
||||||
surfaced_count=len(inline_comments),
|
surfaced_count=len(inline_comments),
|
||||||
trace_url=review_trace_url,
|
trace_url=review_trace_url,
|
||||||
|
out_of_diff_findings=eligible_out_of_diff,
|
||||||
)
|
)
|
||||||
|
|
||||||
review_response = await post_pull_request_review(
|
review_response = await post_pull_request_review(
|
||||||
|
|
@ -310,6 +340,7 @@ async def _publish_review_async(
|
||||||
pr_number=pr_number,
|
pr_number=pr_number,
|
||||||
surfaced_count=len(retry_inline),
|
surfaced_count=len(retry_inline),
|
||||||
trace_url=review_trace_url,
|
trace_url=review_trace_url,
|
||||||
|
out_of_diff_findings=eligible_out_of_diff,
|
||||||
)
|
)
|
||||||
retry_response = await post_pull_request_review(
|
retry_response = await post_pull_request_review(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
|
|
@ -368,6 +399,15 @@ async def _publish_review_async(
|
||||||
}
|
}
|
||||||
review_id = review_response.get("id") if isinstance(review_response, dict) else None
|
review_id = review_response.get("id") if isinstance(review_response, dict) else None
|
||||||
|
|
||||||
|
if review_id is not None and eligible_out_of_diff:
|
||||||
|
# Mark surfaced out-of-diff findings so re-review doesn't repost them.
|
||||||
|
await _store_review_id_on_findings(
|
||||||
|
thread_id=thread_id,
|
||||||
|
findings=findings,
|
||||||
|
eligible_with_payload=[(dict(f), {}) for f in eligible_out_of_diff],
|
||||||
|
review_id=review_id,
|
||||||
|
)
|
||||||
|
|
||||||
if review_id is not None and inline_comments:
|
if review_id is not None and inline_comments:
|
||||||
await _store_review_id_on_findings(
|
await _store_review_id_on_findings(
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
|
|
@ -435,7 +475,10 @@ async def _publish_review_async(
|
||||||
"success": True,
|
"success": True,
|
||||||
"review_id": review_id,
|
"review_id": review_id,
|
||||||
"surfaced_count": len(inline_comments),
|
"surfaced_count": len(inline_comments),
|
||||||
"hidden_count": max(len(open_unpublished) - len(inline_comments), 0),
|
"out_of_diff_count": len(eligible_out_of_diff),
|
||||||
|
"hidden_count": max(
|
||||||
|
len(open_unpublished) - len(inline_comments) - len(eligible_out_of_diff), 0
|
||||||
|
),
|
||||||
"resolved_thread_count": resolved_thread_count,
|
"resolved_thread_count": resolved_thread_count,
|
||||||
}
|
}
|
||||||
if unresolvable_findings:
|
if unresolvable_findings:
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,33 @@ def test_render_review_body_no_findings_message() -> None:
|
||||||
body = render_review_body(pr_number=99, surfaced_count=0)
|
body = render_review_body(pr_number=99, surfaced_count=0)
|
||||||
assert "## ✅ Open SWE Review: No issues found" in body
|
assert "## ✅ Open SWE Review: No issues found" in body
|
||||||
assert "Open SWE reviewed this PR and found no potential bugs to report." in body
|
assert "Open SWE reviewed this PR and found no potential bugs to report." in body
|
||||||
assert "<!-- open-swe-reviewer pr=99 -->" in body
|
|
||||||
|
|
||||||
|
def test_render_review_body_with_only_out_of_diff_findings() -> None:
|
||||||
|
body = render_review_body(
|
||||||
|
pr_number=7,
|
||||||
|
surfaced_count=0,
|
||||||
|
out_of_diff_findings=[
|
||||||
|
_f(title="Caller passes stale arg", description="boom", file="x/caller.py")
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert "No issues found" not in body
|
||||||
|
assert "found no issues in the changed lines" in body
|
||||||
|
assert "<details>" in body
|
||||||
|
assert "1 out-of-diff finding</summary>" in body
|
||||||
|
assert "**Caller passes stale arg**" in body
|
||||||
|
assert "`x/caller.py" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_review_body_combines_inline_and_out_of_diff() -> None:
|
||||||
|
body = render_review_body(
|
||||||
|
pr_number=7,
|
||||||
|
surfaced_count=2,
|
||||||
|
out_of_diff_findings=[_f(title="A"), _f(title="B")],
|
||||||
|
)
|
||||||
|
assert "found 2 potential issues." in body
|
||||||
|
assert "2 out-of-diff findings</summary>" in body
|
||||||
|
assert "<!-- open-swe-reviewer pr=7 -->" in body
|
||||||
|
|
||||||
|
|
||||||
def test_render_review_body_includes_trace_link_when_provided() -> None:
|
def test_render_review_body_includes_trace_link_when_provided() -> None:
|
||||||
|
|
@ -539,6 +565,57 @@ async def test_publish_review_skips_post_on_re_review_with_no_new_findings() ->
|
||||||
assert result["skipped_empty_re_review"] is True
|
assert result["skipped_empty_re_review"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_publish_review_surfaces_out_of_diff_finding_on_re_review() -> None:
|
||||||
|
"""A new out-of-diff finding has no inline comment, but the re-review
|
||||||
|
empty-summary suppression must not swallow it — it should post a summary
|
||||||
|
review carrying the collapsed out-of-diff dropdown."""
|
||||||
|
from agent.tools.publish_review import _publish_review_async
|
||||||
|
|
||||||
|
findings = [
|
||||||
|
_f(
|
||||||
|
id="f_ood",
|
||||||
|
file="caller.py",
|
||||||
|
in_diff=False,
|
||||||
|
first_seen_sha="newsha",
|
||||||
|
github_review_comment_id=None,
|
||||||
|
github_review_id=None,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
post_review = AsyncMock(return_value={"id": 555})
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("agent.tools.publish_review.get_thread_id_from_runtime", return_value="tid"),
|
||||||
|
patch("agent.tools.publish_review.list_findings_async", AsyncMock(return_value=findings)),
|
||||||
|
patch("agent.tools.publish_review.post_pull_request_review", post_review),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review._resolve_threads_for_resolved_findings",
|
||||||
|
AsyncMock(return_value=0),
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.set_reviewer_thread_metadata", AsyncMock()),
|
||||||
|
patch("agent.tools.publish_review._maybe_post_slack_completion_reply", AsyncMock()),
|
||||||
|
patch("agent.tools.publish_review._resolve_review_trace_url", AsyncMock(return_value=None)),
|
||||||
|
):
|
||||||
|
result = await _publish_review_async(
|
||||||
|
owner="o",
|
||||||
|
repo="r",
|
||||||
|
pr_number=7,
|
||||||
|
head_sha="newsha",
|
||||||
|
token="t",
|
||||||
|
severity_threshold="medium",
|
||||||
|
cap=15,
|
||||||
|
is_re_review=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
post_review.assert_awaited_once()
|
||||||
|
assert "out-of-diff" in post_review.await_args.kwargs["body"]
|
||||||
|
assert post_review.await_args.kwargs["inline_comments"] == []
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["surfaced_count"] == 0
|
||||||
|
assert result["out_of_diff_count"] == 1
|
||||||
|
assert "skipped_empty_re_review" not in result
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_publish_review_skips_duplicate_empty_summary_when_open_swe_already_reviewed() -> (
|
async def test_publish_review_skips_duplicate_empty_summary_when_open_swe_already_reviewed() -> (
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,17 @@ def test_add_finding_rejects_empty_title() -> None:
|
||||||
assert "title" in result["error"].lower()
|
assert "title" in result["error"].lower()
|
||||||
|
|
||||||
|
|
||||||
def test_add_finding_rejects_out_of_diff_lines() -> None:
|
def test_add_finding_accepts_out_of_diff_lines_marked_not_in_diff() -> None:
|
||||||
with patch("agent.tools.add_finding.get_config", return_value=_config()):
|
captured: list[Any] = []
|
||||||
|
|
||||||
|
async def fake_append(_thread_id: str, finding: Any) -> None:
|
||||||
|
captured.append(finding)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("agent.tools.add_finding.get_config", return_value=_config()),
|
||||||
|
patch("agent.tools.add_finding.get_thread_id_from_runtime", return_value="tid-1"),
|
||||||
|
patch("agent.tools.add_finding.append_finding", side_effect=fake_append),
|
||||||
|
):
|
||||||
result = add_finding(
|
result = add_finding(
|
||||||
severity="high",
|
severity="high",
|
||||||
confidence="high",
|
confidence="high",
|
||||||
|
|
@ -100,8 +109,10 @@ def test_add_finding_rejects_out_of_diff_lines() -> None:
|
||||||
start_line=99,
|
start_line=99,
|
||||||
end_line=99,
|
end_line=99,
|
||||||
)
|
)
|
||||||
assert result["success"] is False
|
assert result["success"] is True
|
||||||
assert "not part of the PR diff" in result["error"]
|
assert result["in_diff"] is False
|
||||||
|
assert "out-of-diff section" in result["note"]
|
||||||
|
assert captured[0]["in_diff"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_add_finding_accepts_left_side_anchor_on_old_line() -> None:
|
def test_add_finding_accepts_left_side_anchor_on_old_line() -> None:
|
||||||
|
|
@ -139,9 +150,9 @@ def test_add_finding_accepts_left_side_anchor_on_old_line() -> None:
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_add_finding_rejects_left_anchor_outside_old_side_set() -> None:
|
def test_add_finding_left_anchor_outside_old_side_set_marked_not_in_diff() -> None:
|
||||||
"""A LEFT anchor on a line that's not in the old-side hunk must be
|
"""A LEFT anchor on a line that's not in the old-side hunk is accepted but
|
||||||
rejected — same guard, just on the correct side."""
|
marked out-of-diff — same guard, just on the correct side."""
|
||||||
config = {
|
config = {
|
||||||
"configurable": {
|
"configurable": {
|
||||||
"thread_id": "tid-1",
|
"thread_id": "tid-1",
|
||||||
|
|
@ -153,7 +164,11 @@ def test_add_finding_rejects_left_anchor_outside_old_side_set() -> None:
|
||||||
},
|
},
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
}
|
}
|
||||||
with patch("agent.tools.add_finding.get_config", return_value=config):
|
with (
|
||||||
|
patch("agent.tools.add_finding.get_config", return_value=config),
|
||||||
|
patch("agent.tools.add_finding.get_thread_id_from_runtime", return_value="tid-1"),
|
||||||
|
patch("agent.tools.add_finding.append_finding", new_callable=AsyncMock),
|
||||||
|
):
|
||||||
result = add_finding(
|
result = add_finding(
|
||||||
severity="high",
|
severity="high",
|
||||||
confidence="high",
|
confidence="high",
|
||||||
|
|
@ -165,8 +180,8 @@ def test_add_finding_rejects_left_anchor_outside_old_side_set() -> None:
|
||||||
end_line=99,
|
end_line=99,
|
||||||
side="LEFT",
|
side="LEFT",
|
||||||
)
|
)
|
||||||
assert result["success"] is False
|
assert result["success"] is True
|
||||||
assert "not part of the PR diff" in result["error"]
|
assert result["in_diff"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_add_finding_rejects_invalid_confidence() -> None:
|
def test_add_finding_rejects_invalid_confidence() -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue