feat(robomp): added repo-scoped issue search to issue triage flow
- Added `search_issues` support to the GitHub backend and client, including `state_reason` and `is_pull_request` in issue summaries. - Added the `gh_search_issues` host tool with repo-prefixed query handling, non-empty/restricted `repo:` validation, default and bounded `limit` values, and inbound issue filtering. - Added proxy integration for issue search with a new `/gh/v1/search_issues` endpoint and matching proxy-client method/response parsing. - Updated triage prompts to perform pre-`classify_issue` duplicate and already-fixed checks via search, and added tests for search query formatting, validation, and state-aware match rendering.
This commit is contained in:
@@ -861,6 +861,82 @@ def test_classify_issue_wontfix_takes_comment_only_path(db: Database, tmp_path:
|
||||
assert row is not None and row.classification == "wontfix"
|
||||
|
||||
|
||||
def test_gh_search_issues_scopes_repo_and_renders_matches(db: Database, tmp_path: Path) -> None:
|
||||
"""Search auto-prefixes the repo scope, surfaces PR/state_reason so triage can
|
||||
spot prior fixes and not-planned precedents, and filters the inbound issue."""
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
captured["q"] = request.url.params["q"]
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"total_count": 3,
|
||||
"items": [
|
||||
{
|
||||
"number": 42, # the inbound issue itself — must be filtered
|
||||
"title": "boom",
|
||||
"state": "open",
|
||||
"user": {"login": "alice"},
|
||||
"labels": [],
|
||||
"comments": 0,
|
||||
"updated_at": "2026-07-01T00:00:00Z",
|
||||
"created_at": "2026-07-01T00:00:00Z",
|
||||
"html_url": "https://example/42",
|
||||
},
|
||||
{
|
||||
"number": 30,
|
||||
"title": "same crash on resize",
|
||||
"state": "closed",
|
||||
"state_reason": "not_planned",
|
||||
"user": {"login": "bob"},
|
||||
"labels": [{"name": "wontfix"}],
|
||||
"comments": 3,
|
||||
"updated_at": "2026-06-01T00:00:00Z",
|
||||
"created_at": "2026-05-01T00:00:00Z",
|
||||
"html_url": "https://example/30",
|
||||
},
|
||||
{
|
||||
"number": 31,
|
||||
"title": "fix: resize crash",
|
||||
"state": "closed",
|
||||
"state_reason": "completed",
|
||||
"user": {"login": "bot"},
|
||||
"labels": [],
|
||||
"comments": 1,
|
||||
"updated_at": "2026-06-02T00:00:00Z",
|
||||
"created_at": "2026-06-02T00:00:00Z",
|
||||
"html_url": "https://example/pull/31",
|
||||
"pull_request": {"url": "https://example/pull/31"},
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
bindings, loop, t = _bindings(db, tmp_path, httpx.MockTransport(handler))
|
||||
try:
|
||||
tool = next(x for x in build(bindings) if x.name == "gh_search_issues")
|
||||
result = tool.execute({"query": "resize crash"}, _ctx())
|
||||
finally:
|
||||
_stop_loop(loop, t)
|
||||
assert captured["q"] == "repo:octo/widget resize crash"
|
||||
assert "#42" not in result # inbound issue filtered out
|
||||
assert "#30 (issue, closed (not_planned))" in result
|
||||
assert "#31 (PR, closed (completed))" in result
|
||||
|
||||
|
||||
def test_gh_search_issues_rejects_repo_qualifier_and_empty_query(db: Database, tmp_path: Path) -> None:
|
||||
bindings, loop, t = _bindings(db, tmp_path, httpx.MockTransport(lambda r: httpx.Response(500)))
|
||||
try:
|
||||
tool = next(x for x in build(bindings) if x.name == "gh_search_issues")
|
||||
with pytest.raises(RpcCommandError):
|
||||
tool.execute({"query": "repo:evil/elsewhere secrets"}, _ctx())
|
||||
with pytest.raises(RpcCommandError):
|
||||
tool.execute({"query": " "}, _ctx())
|
||||
finally:
|
||||
_stop_loop(loop, t)
|
||||
|
||||
|
||||
def test_classify_issue_rejects_bug_without_priority(db: Database, tmp_path: Path) -> None:
|
||||
bindings, loop, t = _bindings(db, tmp_path, httpx.MockTransport(lambda r: httpx.Response(500)))
|
||||
try:
|
||||
|
||||
@@ -251,6 +251,29 @@ def round_trip_app(proxy_settings: Settings):
|
||||
}
|
||||
],
|
||||
)
|
||||
if path == "/search/issues" and req.method == "GET":
|
||||
assert req.url.params["q"].startswith("repo:octo/widget ")
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"total_count": 1,
|
||||
"items": [
|
||||
{
|
||||
"number": 9,
|
||||
"title": "fixed it",
|
||||
"state": "closed",
|
||||
"state_reason": "completed",
|
||||
"user": {"login": "bob"},
|
||||
"labels": [{"name": "bug"}],
|
||||
"comments": 2,
|
||||
"updated_at": "2026-02-01T00:00:00Z",
|
||||
"created_at": "2026-01-15T00:00:00Z",
|
||||
"html_url": "https://example/9",
|
||||
"pull_request": {"url": "https://example/pull/9"},
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
if path == "/repos/octo/widget/issues/1/comments" and req.method == "GET":
|
||||
return httpx.Response(
|
||||
200,
|
||||
@@ -365,6 +388,10 @@ async def test_round_trip_all_endpoints(round_trip_app) -> None:
|
||||
issues = await client.list_issues("octo/widget")
|
||||
assert len(issues) == 1 and isinstance(issues[0], IssueSummary)
|
||||
|
||||
found = await client.search_issues("octo/widget", "colon selector is:pr")
|
||||
assert len(found) == 1 and isinstance(found[0], IssueSummary)
|
||||
assert found[0].is_pull_request and found[0].state_reason == "completed"
|
||||
|
||||
comments = await client.list_comments("octo/widget", 1)
|
||||
assert len(comments) == 1 and isinstance(comments[0], CommentInfo)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user