Files
tracer-cloud--opensre/tests/tools/test_github_workflow_tools.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

521 lines
18 KiB
Python

"""Tests for semantic GitHub workflow tools."""
from __future__ import annotations
from typing import Any
from unittest.mock import patch
from core.execution import (
BeforeToolCallResult,
ToolExecutionHooks,
ToolExecutionRequest,
execute_tool_calls,
)
from core.llm.types import ToolCall
from core.tool_framework.registered_tool import RegisteredTool
from integrations.github.tools.work_status import (
execute_github_issue_mutation,
list_github_security_alerts,
list_github_work_items,
propose_github_issue_mutation_from_slack,
summarize_github_pr_status,
)
from integrations.github.tools.workflow import (
GitHubApiError,
GitHubRestClient,
build_work_status_report,
)
from tests.tools.conftest import BaseToolContract
from tools.work_status_report_tool import generate_work_status_report
def _registered_tool(tool: Any) -> Any:
return tool.__opensre_registered_tool__
class TestListGitHubWorkItemsContract(BaseToolContract):
def get_tool_under_test(self):
return _registered_tool(list_github_work_items)
class TestSummarizeGitHubPrStatusContract(BaseToolContract):
def get_tool_under_test(self):
return _registered_tool(summarize_github_pr_status)
class TestListGitHubSecurityAlertsContract(BaseToolContract):
def get_tool_under_test(self):
return _registered_tool(list_github_security_alerts)
class TestProposeGitHubIssueMutationFromSlackContract(BaseToolContract):
def get_tool_under_test(self):
return _registered_tool(propose_github_issue_mutation_from_slack)
class TestExecuteGitHubIssueMutationContract(BaseToolContract):
def get_tool_under_test(self):
return _registered_tool(execute_github_issue_mutation)
def test_list_github_work_items_classifies_taken_and_up_for_grabs() -> None:
issues = [
{
"number": 1,
"title": "Assigned bug",
"state": "open",
"html_url": "https://github.com/o/r/issues/1",
"user": {"login": "alice"},
"assignees": [{"login": "bob"}],
"labels": [{"name": "bug"}],
"updated_at": "2026-06-28T10:00:00Z",
},
{
"number": 2,
"title": "Starter task",
"state": "open",
"html_url": "https://github.com/o/r/issues/2",
"user": {"login": "carol"},
"assignees": [],
"labels": [{"name": "help wanted"}],
"updated_at": "2026-06-28T11:00:00Z",
},
{"number": 3, "pull_request": {}, "title": "PR returned from issues endpoint"},
]
with patch.object(GitHubRestClient, "paginate", return_value=issues):
result = list_github_work_items(owner="o", repo="r", github_token="tok")
assert result["available"] is True
assert result["counts"] == {"total": 2, "taken": 1, "up_for_grabs": 1, "unassigned": 0}
assert [item["work_status"] for item in result["items"]] == ["taken", "up_for_grabs"]
def test_summarize_github_pr_status_uses_detail_mergeability_not_list_nulls() -> None:
list_pr = {
"number": 10,
"title": "Ready PR",
"draft": False,
"html_url": "https://github.com/o/r/pull/10",
"user": {"login": "alice"},
"head": {"sha": "abc", "ref": "feature"},
"mergeable": None,
"mergeable_state": "unknown",
"updated_at": "2026-06-28T10:00:00Z",
}
detail_pr = {**list_pr, "mergeable": True, "mergeable_state": "clean"}
def fake_request(self: GitHubRestClient, method: str, path: str, **_kwargs: Any) -> Any:
if path == "/repos/o/r/pulls/10":
return detail_pr
if path == "/repos/o/r/commits/abc/check-runs":
return {
"check_runs": [{"name": "test", "conclusion": "success", "status": "completed"}]
}
raise AssertionError((method, path))
with (
patch.object(GitHubRestClient, "paginate", return_value=[list_pr]),
patch.object(GitHubRestClient, "request", fake_request),
):
result = summarize_github_pr_status(owner="o", repo="r", github_token="tok")
assert result["counts"]["mergeable"] == 1
assert result["pull_requests"][0]["mergeability"] == "mergeable"
def test_summarize_github_pr_status_reports_unknown_mergeability() -> None:
pr = {
"number": 11,
"title": "Unknown PR",
"draft": False,
"html_url": "https://github.com/o/r/pull/11",
"user": {"login": "bob"},
"head": {"sha": "def", "ref": "bugfix"},
"mergeable": None,
"mergeable_state": "unknown",
"updated_at": "2026-06-28T11:00:00Z",
}
def fake_request(self: GitHubRestClient, method: str, path: str, **_kwargs: Any) -> Any:
if path == "/repos/o/r/pulls/11":
return pr
if path == "/repos/o/r/commits/def/check-runs":
return {"check_runs": []}
raise AssertionError((method, path))
with (
patch.object(GitHubRestClient, "paginate", return_value=[pr]),
patch.object(GitHubRestClient, "request", fake_request),
):
result = summarize_github_pr_status(owner="o", repo="r", github_token="tok")
assert result["counts"]["unknown"] == 1
assert result["pull_requests"][0]["status"] == "unknown"
assert "mergeability unknown" in result["pull_requests"][0]["blocking_reasons"]
def test_generate_work_status_report_surfaces_fetch_errors() -> None:
with (
patch(
"tools.work_status_report_tool.list_github_work_items",
return_value={"available": False, "error": "boom", "items": []},
),
patch(
"tools.work_status_report_tool.summarize_github_pr_status",
return_value={"available": True, "pull_requests": []},
),
):
result = generate_work_status_report(owner="o", repo="r", github_token="tok")
assert result["available"] is False
assert result["incomplete"] is True
assert result["errors"] == ["work_items: boom"]
def test_build_work_status_report_does_not_hide_read_errors() -> None:
report = build_work_status_report(
work_items=[],
pull_requests=[],
context="today",
errors=["pull_requests: failed"],
)
assert report.available is False
assert report.incomplete is True
assert "Incomplete report" in report.slack_markdown
def test_summarize_community_followups_uses_repository_comments_endpoint() -> None:
with patch.object(
GitHubRestClient,
"paginate",
return_value=[
{
"issue_url": "https://api.github.com/repos/o/r/issues/7",
"body": "When is the meeting?",
"user": {"login": "contributor"},
"created_at": "2026-06-28T10:00:00Z",
"html_url": "u",
}
],
) as paginate:
result = __import__(
"tools.community_followup_tool", fromlist=["summarize_community_followups"]
).summarize_community_followups(owner="o", repo="r", github_token="tok")
paginate.assert_called_once()
assert paginate.call_args.args[0] == "/repos/o/r/issues/comments"
assert result["counts"]["unanswered_questions"] == 1
def test_proposal_id_is_stable_and_payload_has_idempotency_marker() -> None:
first = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="create",
slack_text="add this to the hackathon list",
slack_url="https://slack.example/archives/C/p1",
labels=["hackathon"],
)
second = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="create",
slack_text="add this to the hackathon list",
slack_url="https://slack.example/archives/C/p1",
labels=["hackathon"],
)
assert first["proposal"]["proposal_id"] == second["proposal"]["proposal_id"]
assert first["proposal"]["operation"] == "create"
assert "opensre-slack-proposal:" in first["proposal"]["payload"]["body"]
assert first["side_effects"] == []
def test_execute_tool_schema_has_no_confirm_parameter() -> None:
tool = _registered_tool(execute_github_issue_mutation)
assert "confirm" not in tool.input_schema["properties"]
assert tool.requires_approval is False
def test_execute_create_searches_idempotency_marker_before_create() -> None:
proposal = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="create",
slack_text="ship this",
slack_url="https://slack.example/archives/C/p1",
)["proposal"]
calls: list[tuple[str, str, dict[str, Any]]] = []
def fake_request(self: GitHubRestClient, method: str, path: str, **kwargs: Any) -> Any:
calls.append((method, path, kwargs))
if path == "/search/issues":
return {"total_count": 0, "items": []}
if path == "/repos/o/r/issues":
return {"number": 99, "html_url": "https://github.com/o/r/issues/99"}
raise AssertionError((method, path))
with patch.object(GitHubRestClient, "request", fake_request):
result = execute_github_issue_mutation(
owner="o", repo="r", proposal=proposal, github_token="tok"
)
assert result["executed"] is True
assert result["side_effect"] == "created_github_issue"
assert [call[1] for call in calls] == ["/search/issues", "/repos/o/r/issues"]
assert "in:body" in calls[0][2]["params"]["q"]
assert proposal["idempotency_marker"] in calls[0][2]["params"]["q"]
def test_execute_create_returns_existing_issue_for_idempotency_marker() -> None:
proposal = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="create",
slack_text="ship this",
slack_url="https://slack.example/archives/C/p1",
)["proposal"]
def fake_request(self: GitHubRestClient, method: str, path: str, **_kwargs: Any) -> Any:
if path == "/search/issues":
return {"total_count": 1, "items": [{"number": 12, "html_url": "existing"}]}
raise AssertionError((method, path))
with patch.object(GitHubRestClient, "request", fake_request):
result = execute_github_issue_mutation(
owner="o", repo="r", proposal=proposal, github_token="tok"
)
assert result["executed"] is False
assert result["side_effect"] == "existing_github_issue"
def test_execute_update_adds_comment_and_preserves_body() -> None:
proposal = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="update",
issue_number=51,
slack_text="PR shipped",
slack_url="https://slack.example/archives/C/p2",
labels=["done"],
)["proposal"]
calls: list[tuple[str, str, dict[str, Any]]] = []
def fake_request(self: GitHubRestClient, method: str, path: str, **kwargs: Any) -> Any:
calls.append((method, path, kwargs))
if path == "/repos/o/r/issues/51" and method == "GET":
return {"number": 51, "body": "original"}
if path == "/search/issues":
assert "in:comments" in kwargs["params"]["q"]
return {"total_count": 0, "items": []}
if path == "/repos/o/r/issues/51/comments":
assert "PR shipped" in kwargs["body"]["body"]
return {"id": 1}
if path == "/repos/o/r/issues/51" and method == "PATCH":
assert "body" not in kwargs["body"]
assert kwargs["body"] == {"labels": ["done"]}
return {"number": 51}
raise AssertionError((method, path, kwargs))
with patch.object(GitHubRestClient, "request", fake_request):
result = execute_github_issue_mutation(
owner="o", repo="r", proposal=proposal, github_token="tok"
)
assert result["executed"] is True
assert [call[1] for call in calls] == [
"/repos/o/r/issues/51",
"/search/issues",
"/repos/o/r/issues/51/comments",
"/repos/o/r/issues/51",
]
def test_execute_close_comments_before_closing_and_preserves_body() -> None:
proposal = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="close",
issue_number=51,
slack_text="done",
slack_url="https://slack.example/archives/C/p3",
)["proposal"]
calls: list[tuple[str, str, dict[str, Any]]] = []
def fake_request(self: GitHubRestClient, method: str, path: str, **kwargs: Any) -> Any:
calls.append((method, path, kwargs))
if path == "/repos/o/r/issues/51" and method == "GET":
return {"number": 51, "body": "original"}
if path == "/search/issues":
assert "in:comments" in kwargs["params"]["q"]
return {"total_count": 0, "items": []}
if path == "/repos/o/r/issues/51/comments":
return {"id": 1}
if path == "/repos/o/r/issues/51" and method == "PATCH":
assert kwargs["body"] == {"state": "closed", "state_reason": "completed"}
return {"number": 51, "state": "closed"}
raise AssertionError((method, path, kwargs))
with patch.object(GitHubRestClient, "request", fake_request):
result = execute_github_issue_mutation(
owner="o", repo="r", proposal=proposal, github_token="tok"
)
assert result["executed"] is True
assert [call[1] for call in calls] == [
"/repos/o/r/issues/51",
"/search/issues",
"/repos/o/r/issues/51/comments",
"/repos/o/r/issues/51",
]
def test_execute_update_skips_duplicate_comment_for_seen_marker() -> None:
proposal = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="update",
issue_number=51,
slack_text="PR shipped",
slack_url="https://slack.example/archives/C/p2",
labels=["done"],
)["proposal"]
calls: list[tuple[str, str, dict[str, Any]]] = []
def fake_request(self: GitHubRestClient, method: str, path: str, **kwargs: Any) -> Any:
calls.append((method, path, kwargs))
if path == "/repos/o/r/issues/51" and method == "GET":
return {"number": 51, "body": "original"}
if path == "/search/issues":
return {"total_count": 1, "items": [{"number": 51}]}
if path == "/repos/o/r/issues/51/comments":
raise AssertionError("duplicate comment should not be posted")
if path == "/repos/o/r/issues/51" and method == "PATCH":
return {"number": 51}
raise AssertionError((method, path, kwargs))
with patch.object(GitHubRestClient, "request", fake_request):
result = execute_github_issue_mutation(
owner="o", repo="r", proposal=proposal, github_token="tok"
)
assert result["executed"] is True
assert result["comment_already_recorded"] is True
assert [call[1] for call in calls] == [
"/repos/o/r/issues/51",
"/search/issues",
"/repos/o/r/issues/51",
]
def test_execute_mutation_rejects_malformed_proposal_without_api_call() -> None:
with patch.object(GitHubRestClient, "request") as request:
result = execute_github_issue_mutation(
owner="o", repo="r", proposal={"operation": "create"}, github_token="tok"
)
request.assert_not_called()
assert result["executed"] is False
assert result["side_effect"] == "github_issue_mutation_rejected"
assert "missing required field" in result["error"]
def test_execute_mutation_rejects_payload_without_marker() -> None:
proposal = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="create",
slack_text="ship this",
slack_url="https://slack.example/archives/C/p1",
)["proposal"]
proposal = {**proposal, "payload": {**proposal["payload"], "body": "missing marker"}}
with patch.object(GitHubRestClient, "request") as request:
result = execute_github_issue_mutation(
owner="o", repo="r", proposal=proposal, github_token="tok"
)
request.assert_not_called()
assert result["executed"] is False
assert result["side_effect"] == "github_issue_mutation_rejected"
assert "idempotency marker" in result["error"]
def test_execute_mutation_returns_api_errors() -> None:
proposal = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="close",
issue_number=51,
slack_text="done",
)["proposal"]
with patch.object(
GitHubRestClient, "request", side_effect=GitHubApiError("nope", status_code=403)
):
result = execute_github_issue_mutation(
owner="o", repo="r", proposal=proposal, github_token="tok"
)
assert result["available"] is False
assert result["executed"] is False
assert "nope" in result["error"]
def test_requires_approval_runs_without_hook() -> None:
tool: RegisteredTool = _registered_tool(execute_github_issue_mutation)
proposal = propose_github_issue_mutation_from_slack(
owner="o",
repo="r",
operation="close",
issue_number=51,
slack_text="done",
)["proposal"]
with patch.object(GitHubRestClient, "request", return_value={"number": 51}):
result = execute_tool_calls(
[
ToolCall(
id="c1",
name="execute_github_issue_mutation",
input={"owner": "o", "repo": "r", "proposal": proposal},
)
],
[tool],
{},
)[0]
assert result.is_error is False
def test_requires_approval_allows_runtime_approval_hook() -> None:
from core.tool_framework.registered_tool import RegisteredTool
def run() -> dict[str, str]:
return {"ok": "true"}
tool = RegisteredTool(
name="approval_gated_fixture",
description="fixture",
input_schema={"type": "object", "properties": {}},
source="github",
run=run,
requires_approval=True,
)
def approve(request: ToolExecutionRequest) -> BeforeToolCallResult:
assert request.tool.requires_approval is True
return BeforeToolCallResult(approved=True)
result = execute_tool_calls(
[ToolCall(id="c1", name="approval_gated_fixture", input={})],
[tool],
{},
hooks=ToolExecutionHooks(before_tool_call=approve),
)[0]
assert result.is_error is False
assert result.details == {"ok": "true"}