mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-03 20:13:21 +00:00
feat(open-swe): post review summary to Slack on first review (#1258)
When a Slack user kicks off a PR review with `@open-swe review <pr-url>`, the reviewer agent now posts a one-line summary back to the Slack thread when it finishes — either "No issues found" or "found N potential issue(s)" with a link to the GitHub review. The reviewer agent has no Slack tools by design, so the summary is sent host-side from `publish_review` after the GitHub review POST succeeds. The Slack channel/thread_ts is persisted on reviewer thread metadata at trigger time and read back on publish. Re-reviews triggered by push events stay silent in Slack to avoid noise on the original thread.
This commit is contained in:
parent
88a55057ea
commit
892347041f
5 changed files with 297 additions and 2 deletions
|
|
@ -71,6 +71,13 @@ class ReviewerPRMeta(TypedDict, total=False):
|
||||||
base_ref: str
|
base_ref: str
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewerSlackThread(TypedDict, total=False):
|
||||||
|
"""Slack thread that initiated this review — used to post a completion reply."""
|
||||||
|
|
||||||
|
channel_id: str
|
||||||
|
thread_ts: str
|
||||||
|
|
||||||
|
|
||||||
def new_finding_id() -> str:
|
def new_finding_id() -> str:
|
||||||
"""Return a stable, short, URL-friendly finding id (``f_<hex>``)."""
|
"""Return a stable, short, URL-friendly finding id (``f_<hex>``)."""
|
||||||
return f"f_{uuid.uuid4().hex[:10]}"
|
return f"f_{uuid.uuid4().hex[:10]}"
|
||||||
|
|
@ -206,6 +213,7 @@ async def set_reviewer_thread_metadata(
|
||||||
last_reviewed_sha: str | None = None,
|
last_reviewed_sha: str | None = None,
|
||||||
watch: bool | None = None,
|
watch: bool | None = None,
|
||||||
findings: list[Finding] | None = None,
|
findings: list[Finding] | None = None,
|
||||||
|
slack_thread: ReviewerSlackThread | None = None,
|
||||||
extra: dict[str, Any] | None = None,
|
extra: dict[str, Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Persist reviewer-thread-level metadata.
|
"""Persist reviewer-thread-level metadata.
|
||||||
|
|
@ -224,6 +232,8 @@ async def set_reviewer_thread_metadata(
|
||||||
metadata["watch"] = watch
|
metadata["watch"] = watch
|
||||||
if findings is not None:
|
if findings is not None:
|
||||||
metadata["findings"] = findings
|
metadata["findings"] = findings
|
||||||
|
if slack_thread is not None:
|
||||||
|
metadata["slack_thread"] = slack_thread
|
||||||
if extra:
|
if extra:
|
||||||
metadata.update(extra)
|
metadata.update(extra)
|
||||||
await client.threads.update(thread_id=thread_id, metadata=metadata)
|
await client.threads.update(thread_id=thread_id, metadata=metadata)
|
||||||
|
|
@ -245,6 +255,19 @@ def get_thread_pr_meta(metadata: dict[str, Any]) -> ReviewerPRMeta | None:
|
||||||
return cast(ReviewerPRMeta, pr)
|
return cast(ReviewerPRMeta, pr)
|
||||||
|
|
||||||
|
|
||||||
|
def get_thread_slack_ref(metadata: dict[str, Any]) -> ReviewerSlackThread | None:
|
||||||
|
slack_thread = metadata.get("slack_thread")
|
||||||
|
if not isinstance(slack_thread, dict):
|
||||||
|
return None
|
||||||
|
channel_id = slack_thread.get("channel_id")
|
||||||
|
thread_ts = slack_thread.get("thread_ts")
|
||||||
|
if not isinstance(channel_id, str) or not isinstance(thread_ts, str):
|
||||||
|
return None
|
||||||
|
if not channel_id or not thread_ts:
|
||||||
|
return None
|
||||||
|
return cast(ReviewerSlackThread, slack_thread)
|
||||||
|
|
||||||
|
|
||||||
def filter_findings_for_publish(
|
def filter_findings_for_publish(
|
||||||
findings: list[Finding],
|
findings: list[Finding],
|
||||||
*,
|
*,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ from ..reviewer_findings import (
|
||||||
Severity,
|
Severity,
|
||||||
filter_findings_for_publish,
|
filter_findings_for_publish,
|
||||||
get_thread_id_from_runtime,
|
get_thread_id_from_runtime,
|
||||||
|
get_thread_metadata,
|
||||||
|
get_thread_slack_ref,
|
||||||
replace_findings,
|
replace_findings,
|
||||||
set_reviewer_thread_metadata,
|
set_reviewer_thread_metadata,
|
||||||
)
|
)
|
||||||
|
|
@ -26,6 +28,7 @@ from ..reviewer_publish import (
|
||||||
resolve_review_thread,
|
resolve_review_thread,
|
||||||
)
|
)
|
||||||
from ..utils.github_token import get_github_token
|
from ..utils.github_token import get_github_token
|
||||||
|
from ..utils.slack import post_slack_thread_reply
|
||||||
|
|
||||||
|
|
||||||
def publish_review(
|
def publish_review(
|
||||||
|
|
@ -70,6 +73,7 @@ def publish_review(
|
||||||
repo_config = configurable.get("repo") if isinstance(configurable, dict) else None
|
repo_config = configurable.get("repo") if isinstance(configurable, dict) else None
|
||||||
pr_number = configurable.get("pr_number") if isinstance(configurable, dict) else None
|
pr_number = configurable.get("pr_number") if isinstance(configurable, dict) else None
|
||||||
head_sha = configurable.get("head_sha") if isinstance(configurable, dict) else None
|
head_sha = configurable.get("head_sha") if isinstance(configurable, dict) else None
|
||||||
|
is_re_review = bool(configurable.get("re_review")) if isinstance(configurable, dict) else False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not isinstance(repo_config, dict)
|
not isinstance(repo_config, dict)
|
||||||
|
|
@ -95,6 +99,7 @@ def publish_review(
|
||||||
token=token,
|
token=token,
|
||||||
severity_threshold=_cast_severity(severity_threshold),
|
severity_threshold=_cast_severity(severity_threshold),
|
||||||
cap=cap,
|
cap=cap,
|
||||||
|
is_re_review=is_re_review,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -112,6 +117,7 @@ async def _publish_review_async(
|
||||||
token: str,
|
token: str,
|
||||||
severity_threshold: Severity,
|
severity_threshold: Severity,
|
||||||
cap: int,
|
cap: int,
|
||||||
|
is_re_review: bool,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
thread_id = get_thread_id_from_runtime()
|
thread_id = get_thread_id_from_runtime()
|
||||||
findings = await list_findings_async(thread_id)
|
findings = await list_findings_async(thread_id)
|
||||||
|
|
@ -178,6 +184,16 @@ async def _publish_review_async(
|
||||||
findings=await list_findings_async(thread_id),
|
findings=await list_findings_async(thread_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not is_re_review:
|
||||||
|
await _maybe_post_slack_completion_reply(
|
||||||
|
thread_id=thread_id,
|
||||||
|
owner=owner,
|
||||||
|
repo=repo,
|
||||||
|
pr_number=pr_number,
|
||||||
|
review_id=review_id,
|
||||||
|
surfaced_count=len(inline_comments),
|
||||||
|
)
|
||||||
|
|
||||||
await set_reviewer_thread_metadata(thread_id, last_reviewed_sha=head_sha)
|
await set_reviewer_thread_metadata(thread_id, last_reviewed_sha=head_sha)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -189,6 +205,40 @@ async def _publish_review_async(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _maybe_post_slack_completion_reply(
|
||||||
|
*,
|
||||||
|
thread_id: str,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
pr_number: int,
|
||||||
|
review_id: int | None,
|
||||||
|
surfaced_count: int,
|
||||||
|
) -> None:
|
||||||
|
"""Post a one-line completion summary to the Slack thread that started this review.
|
||||||
|
|
||||||
|
Only fires for first reviews (gated by the caller). No-op if the reviewer
|
||||||
|
thread has no ``slack_thread`` metadata — i.e. the review wasn't started
|
||||||
|
from Slack.
|
||||||
|
"""
|
||||||
|
metadata = await get_thread_metadata(thread_id)
|
||||||
|
slack_ref = get_thread_slack_ref(metadata)
|
||||||
|
if slack_ref is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if surfaced_count == 0:
|
||||||
|
headline = "*Open SWE Review*: No issues found."
|
||||||
|
else:
|
||||||
|
issue_word = "issue" if surfaced_count == 1 else "issues"
|
||||||
|
headline = f"*Open SWE Review* found {surfaced_count} potential {issue_word}."
|
||||||
|
|
||||||
|
review_url = f"https://github.com/{owner}/{repo}/pull/{pr_number}"
|
||||||
|
if isinstance(review_id, int):
|
||||||
|
review_url = f"{review_url}#pullrequestreview-{review_id}"
|
||||||
|
text = f"{headline} <{review_url}|View review>"
|
||||||
|
|
||||||
|
await post_slack_thread_reply(slack_ref["channel_id"], slack_ref["thread_ts"], text)
|
||||||
|
|
||||||
|
|
||||||
async def _store_comment_ids_on_findings(
|
async def _store_comment_ids_on_findings(
|
||||||
*,
|
*,
|
||||||
thread_id: str,
|
thread_id: str,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from langgraph_sdk.client import LangGraphClient
|
||||||
from .reviewer_findings import (
|
from .reviewer_findings import (
|
||||||
REVIEWER_THREAD_KIND,
|
REVIEWER_THREAD_KIND,
|
||||||
ReviewerPRMeta,
|
ReviewerPRMeta,
|
||||||
|
ReviewerSlackThread,
|
||||||
set_reviewer_thread_metadata,
|
set_reviewer_thread_metadata,
|
||||||
)
|
)
|
||||||
from .utils.auth import (
|
from .utils.auth import (
|
||||||
|
|
@ -913,7 +914,12 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
|
||||||
async def process_slack_pr_review_request(
|
async def process_slack_pr_review_request(
|
||||||
pr_ref: GitHubPrRef, channel_id: str, thread_ts: str
|
pr_ref: GitHubPrRef, channel_id: str, thread_ts: str
|
||||||
) -> None:
|
) -> None:
|
||||||
result = await trigger_pr_review_from_ref(pr_ref, source="slack")
|
result = await trigger_pr_review_from_ref(
|
||||||
|
pr_ref,
|
||||||
|
source="slack",
|
||||||
|
slack_channel_id=channel_id,
|
||||||
|
slack_thread_ts=thread_ts,
|
||||||
|
)
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
thread_id = result.get("thread_id")
|
thread_id = result.get("thread_id")
|
||||||
if isinstance(thread_id, str) and thread_id:
|
if isinstance(thread_id, str) and thread_id:
|
||||||
|
|
@ -1383,6 +1389,8 @@ async def trigger_pr_review_from_ref(
|
||||||
source: str,
|
source: str,
|
||||||
github_login: str = "",
|
github_login: str = "",
|
||||||
github_user_id: int | None = None,
|
github_user_id: int | None = None,
|
||||||
|
slack_channel_id: str = "",
|
||||||
|
slack_thread_ts: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
repo_config = {"owner": pr_ref.owner, "name": pr_ref.repo}
|
repo_config = {"owner": pr_ref.owner, "name": pr_ref.repo}
|
||||||
if not _is_repo_allowed_for_reviewer(repo_config):
|
if not _is_repo_allowed_for_reviewer(repo_config):
|
||||||
|
|
@ -1428,7 +1436,15 @@ async def trigger_pr_review_from_ref(
|
||||||
"head_ref": branch_name,
|
"head_ref": branch_name,
|
||||||
"base_ref": base_ref,
|
"base_ref": base_ref,
|
||||||
}
|
}
|
||||||
await set_reviewer_thread_metadata(thread_id, pr=pr_meta, watch=True)
|
slack_thread_meta: ReviewerSlackThread | None = None
|
||||||
|
if slack_channel_id and slack_thread_ts:
|
||||||
|
slack_thread_meta = {
|
||||||
|
"channel_id": slack_channel_id,
|
||||||
|
"thread_ts": slack_thread_ts,
|
||||||
|
}
|
||||||
|
await set_reviewer_thread_metadata(
|
||||||
|
thread_id, pr=pr_meta, watch=True, slack_thread=slack_thread_meta
|
||||||
|
)
|
||||||
|
|
||||||
prompt = build_github_pr_review_prompt(repo_config, pr_ref.number, pr_url, base_sha, head_sha)
|
prompt = build_github_pr_review_prompt(repo_config, pr_ref.number, pr_url, base_sha, head_sha)
|
||||||
configurable = _build_reviewer_configurable(
|
configurable = _build_reviewer_configurable(
|
||||||
|
|
|
||||||
|
|
@ -594,9 +594,13 @@ def test_process_slack_pr_review_request_posts_trace_reply(monkeypatch) -> None:
|
||||||
source: str,
|
source: str,
|
||||||
github_login: str = "",
|
github_login: str = "",
|
||||||
github_user_id: int | None = None,
|
github_user_id: int | None = None,
|
||||||
|
slack_channel_id: str = "",
|
||||||
|
slack_thread_ts: str = "",
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
captured["pr_ref"] = pr_ref
|
captured["pr_ref"] = pr_ref
|
||||||
captured["source"] = source
|
captured["source"] = source
|
||||||
|
captured["slack_channel_id"] = slack_channel_id
|
||||||
|
captured["slack_thread_ts"] = slack_thread_ts
|
||||||
return {"success": True, "thread_id": "reviewer-thread-id", "pr_url": pr_ref.url}
|
return {"success": True, "thread_id": "reviewer-thread-id", "pr_url": pr_ref.url}
|
||||||
|
|
||||||
async def fake_post_slack_trace_reply(
|
async def fake_post_slack_trace_reply(
|
||||||
|
|
@ -626,6 +630,8 @@ def test_process_slack_pr_review_request_posts_trace_reply(monkeypatch) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
assert captured["source"] == "slack"
|
assert captured["source"] == "slack"
|
||||||
|
assert captured["slack_channel_id"] == "C123"
|
||||||
|
assert captured["slack_thread_ts"] == "1700000000.000100"
|
||||||
assert captured["trace_reply"] == {
|
assert captured["trace_reply"] == {
|
||||||
"channel_id": "C123",
|
"channel_id": "C123",
|
||||||
"thread_ts": "1700000000.000100",
|
"thread_ts": "1700000000.000100",
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,10 @@ async def test_publish_review_skips_findings_already_published() -> None:
|
||||||
return_value=0,
|
return_value=0,
|
||||||
),
|
),
|
||||||
patch("agent.tools.publish_review.set_reviewer_thread_metadata", set_metadata),
|
patch("agent.tools.publish_review.set_reviewer_thread_metadata", set_metadata),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review._maybe_post_slack_completion_reply",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
),
|
||||||
):
|
):
|
||||||
result = await _publish_review_async(
|
result = await _publish_review_async(
|
||||||
owner="o",
|
owner="o",
|
||||||
|
|
@ -152,6 +156,7 @@ async def test_publish_review_skips_findings_already_published() -> None:
|
||||||
token="t",
|
token="t",
|
||||||
severity_threshold="medium",
|
severity_threshold="medium",
|
||||||
cap=15,
|
cap=15,
|
||||||
|
is_re_review=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
|
|
@ -182,6 +187,10 @@ async def test_publish_review_posts_summary_when_no_findings() -> None:
|
||||||
return_value=0,
|
return_value=0,
|
||||||
),
|
),
|
||||||
patch("agent.tools.publish_review.set_reviewer_thread_metadata", set_metadata),
|
patch("agent.tools.publish_review.set_reviewer_thread_metadata", set_metadata),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review._maybe_post_slack_completion_reply",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
),
|
||||||
):
|
):
|
||||||
result = await _publish_review_async(
|
result = await _publish_review_async(
|
||||||
owner="o",
|
owner="o",
|
||||||
|
|
@ -191,6 +200,7 @@ async def test_publish_review_posts_summary_when_no_findings() -> None:
|
||||||
token="t",
|
token="t",
|
||||||
severity_threshold="medium",
|
severity_threshold="medium",
|
||||||
cap=15,
|
cap=15,
|
||||||
|
is_re_review=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
|
|
@ -201,3 +211,193 @@ async def test_publish_review_posts_summary_when_no_findings() -> None:
|
||||||
posted_inline = post_review.await_args.kwargs["inline_comments"]
|
posted_inline = post_review.await_args.kwargs["inline_comments"]
|
||||||
assert posted_inline == []
|
assert posted_inline == []
|
||||||
assert "No issues found" in posted_body
|
assert "No issues found" in posted_body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_publish_review_posts_slack_reply_on_first_review_with_slack_ref() -> None:
|
||||||
|
"""A first review with a slack_thread metadata ref posts a one-line summary."""
|
||||||
|
from agent.tools.publish_review import _publish_review_async
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"kind": "reviewer",
|
||||||
|
"slack_thread": {"channel_id": "C1", "thread_ts": "1234.5"},
|
||||||
|
}
|
||||||
|
slack_post = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
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=[])),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review.post_pull_request_review",
|
||||||
|
AsyncMock(return_value={"id": 42}),
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.fetch_review_comments", AsyncMock(return_value=[])),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review._resolve_threads_for_resolved_findings",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=0,
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.set_reviewer_thread_metadata", new_callable=AsyncMock),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review.get_thread_metadata",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=metadata,
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.post_slack_thread_reply", slack_post),
|
||||||
|
):
|
||||||
|
await _publish_review_async(
|
||||||
|
owner="o",
|
||||||
|
repo="r",
|
||||||
|
pr_number=7,
|
||||||
|
head_sha="sha",
|
||||||
|
token="t",
|
||||||
|
severity_threshold="medium",
|
||||||
|
cap=15,
|
||||||
|
is_re_review=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
slack_post.assert_awaited_once()
|
||||||
|
args = slack_post.await_args.args
|
||||||
|
assert args[0] == "C1"
|
||||||
|
assert args[1] == "1234.5"
|
||||||
|
assert "No issues found" in args[2]
|
||||||
|
assert "https://github.com/o/r/pull/7#pullrequestreview-42" in args[2]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_publish_review_uses_plural_findings_in_slack_reply() -> None:
|
||||||
|
"""Surfaced count > 1 should pluralize 'issues' in the slack summary."""
|
||||||
|
from agent.tools.publish_review import _publish_review_async
|
||||||
|
|
||||||
|
findings = [
|
||||||
|
_f(id="f1", file="a.py", start_line=1, end_line=1),
|
||||||
|
_f(id="f2", file="b.py", start_line=2, end_line=2),
|
||||||
|
]
|
||||||
|
metadata = {
|
||||||
|
"kind": "reviewer",
|
||||||
|
"slack_thread": {"channel_id": "C1", "thread_ts": "1234.5"},
|
||||||
|
}
|
||||||
|
slack_post = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
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",
|
||||||
|
AsyncMock(return_value={"id": 99}),
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.fetch_review_comments", AsyncMock(return_value=[])),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review._resolve_threads_for_resolved_findings",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=0,
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.set_reviewer_thread_metadata", new_callable=AsyncMock),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review.get_thread_metadata",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=metadata,
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.post_slack_thread_reply", slack_post),
|
||||||
|
):
|
||||||
|
await _publish_review_async(
|
||||||
|
owner="o",
|
||||||
|
repo="r",
|
||||||
|
pr_number=7,
|
||||||
|
head_sha="sha",
|
||||||
|
token="t",
|
||||||
|
severity_threshold="medium",
|
||||||
|
cap=15,
|
||||||
|
is_re_review=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
slack_post.assert_awaited_once()
|
||||||
|
text = slack_post.await_args.args[2]
|
||||||
|
assert "found 2 potential issues" in text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_publish_review_skips_slack_reply_on_re_review() -> None:
|
||||||
|
"""Re-reviews must NOT post to Slack even when slack_thread metadata is set."""
|
||||||
|
from agent.tools.publish_review import _publish_review_async
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"kind": "reviewer",
|
||||||
|
"slack_thread": {"channel_id": "C1", "thread_ts": "1234.5"},
|
||||||
|
}
|
||||||
|
slack_post = AsyncMock(return_value=True)
|
||||||
|
get_metadata = AsyncMock(return_value=metadata)
|
||||||
|
|
||||||
|
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=[])),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review.post_pull_request_review",
|
||||||
|
AsyncMock(return_value={"id": 1}),
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.fetch_review_comments", AsyncMock(return_value=[])),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review._resolve_threads_for_resolved_findings",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=0,
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.set_reviewer_thread_metadata", new_callable=AsyncMock),
|
||||||
|
patch("agent.tools.publish_review.get_thread_metadata", get_metadata),
|
||||||
|
patch("agent.tools.publish_review.post_slack_thread_reply", slack_post),
|
||||||
|
):
|
||||||
|
await _publish_review_async(
|
||||||
|
owner="o",
|
||||||
|
repo="r",
|
||||||
|
pr_number=7,
|
||||||
|
head_sha="sha",
|
||||||
|
token="t",
|
||||||
|
severity_threshold="medium",
|
||||||
|
cap=15,
|
||||||
|
is_re_review=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
slack_post.assert_not_awaited()
|
||||||
|
# Re-review path should also avoid even fetching the slack metadata.
|
||||||
|
get_metadata.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_publish_review_skips_slack_reply_when_no_slack_ref() -> None:
|
||||||
|
"""A review started from GitHub (no slack_thread metadata) must not post to Slack."""
|
||||||
|
from agent.tools.publish_review import _publish_review_async
|
||||||
|
|
||||||
|
slack_post = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
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=[])),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review.post_pull_request_review",
|
||||||
|
AsyncMock(return_value={"id": 1}),
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.fetch_review_comments", AsyncMock(return_value=[])),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review._resolve_threads_for_resolved_findings",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=0,
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.set_reviewer_thread_metadata", new_callable=AsyncMock),
|
||||||
|
patch(
|
||||||
|
"agent.tools.publish_review.get_thread_metadata",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value={"kind": "reviewer"},
|
||||||
|
),
|
||||||
|
patch("agent.tools.publish_review.post_slack_thread_reply", slack_post),
|
||||||
|
):
|
||||||
|
await _publish_review_async(
|
||||||
|
owner="o",
|
||||||
|
repo="r",
|
||||||
|
pr_number=7,
|
||||||
|
head_sha="sha",
|
||||||
|
token="t",
|
||||||
|
severity_threshold="medium",
|
||||||
|
cap=15,
|
||||||
|
is_re_review=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
slack_post.assert_not_awaited()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue