from typing import cast from agentci.engine.repository import Repository from agentci.gitea import CommentInfo, Gitea, IssueInfo, PullRequestInfo from agentci.workflows.context import build_issue_context, build_pull_request_context class FakeIssueGitea: async def issue(self, *_args): return IssueInfo(number=2, title="Broken widget", body="It fails.", state="open") async def issue_comments(self, *_args): return [ CommentInfo(1, "alice", "Details", "2026-01-01"), CommentInfo(2, "agentci", "Agent job queued", "2026-01-02"), ] class FakeRepository: async def operational_comment_ids(self, *_args): return {2} class FakePullRequestGitea: async def pull_request(self, *_args): return PullRequestInfo( number=3, title="Fix widget", body="Fixes the failure.", state="open", merged=False, base_branch="main", head_branch="fix-widget", head_sha="abcdef1234567890", head_owner="alice", head_repo="repo", ) async def issue_comments(self, *_args): return [CommentInfo(3, "bob", "Please add a test.", "2026-01-03")] async def pull_reviews(self, *_args): return [ { "id": 4, "user": {"login": "carol"}, "state": "REQUEST_CHANGES", "body": "One issue remains.", } ] async def review_comments(self, *_args): return [ { "path": "src/widget.py", "new_position": 12, "body": "Handle the empty value.", } ] async def pull_commits(self, *_args): return [{"sha": "abcdef1234567890", "commit": {"message": "Fix widget"}}] class EmptyIssueGitea: async def issue(self, *_args): return IssueInfo(number=5, title="Empty issue", body="", state="open") async def issue_comments(self, *_args): return [] class EmptyRepository: async def operational_comment_ids(self, *_args): return set() class EmptyPullRequestGitea: async def pull_request(self, *_args): return PullRequestInfo( number=6, title="Empty pull request", body="", state="open", merged=False, base_branch="main", head_branch="empty", head_sha="1234567890abcdef", head_owner="alice", head_repo="repo", ) async def issue_comments(self, *_args): return [] async def pull_reviews(self, *_args): return [] async def pull_commits(self, *_args): return [] async def review_comments(self, *_args): raise AssertionError("review comments should not be requested without reviews") class OrderedPullRequestGitea(FakePullRequestGitea): async def issue_comments(self, *_args): return [ CommentInfo(1, "alice", "First timeline comment", "2026-01-01"), CommentInfo(2, "bob", "Second timeline comment", "2026-01-02"), ] async def pull_reviews(self, *_args): return [ { "id": 10, "user": {"login": "carol"}, "state": "REQUEST_CHANGES", "body": "First formal review", }, { "id": 11, "user": {"login": "dave"}, "state": "APPROVED", "body": "Second formal review", }, ] async def review_comments(self, *_args): review_id = _args[-1] if review_id == 10: return [ { "path": "src/old.py", "new_position": None, "old_position": 21, "body": "Old-side position", } ] return [ { "path": "src/new.py", "new_position": 34, "body": "New-side position", } ] async def pull_commits(self, *_args): return [ {"sha": "111111111111aaaa", "commit": {"message": "First commit"}}, {"sha": "222222222222bbbb", "commit": {"message": "Second commit"}}, ] async def test_issue_context_excludes_operational_comments() -> None: context = await build_issue_context( cast(Gitea, FakeIssueGitea()), cast(Repository, FakeRepository()), "org", "repo", 2, ) assert "Broken widget" in context assert "Details" in context assert "Agent job queued" not in context async def test_pull_request_context_includes_feedback_and_commits() -> None: pull, context = await build_pull_request_context( cast(Gitea, FakePullRequestGitea()), "org", "repo", 3 ) assert pull.number == 3 assert "Please add a test." in context assert "One issue remains." in context assert "`src/widget.py:12`: Handle the empty value." in context assert "abcdef123456 Fix widget" in context async def test_issue_context_labels_empty_body_and_discussion() -> None: context = await build_issue_context( cast(Gitea, EmptyIssueGitea()), cast(Repository, EmptyRepository()), "org", "repo", 5, ) assert "## Issue body\n(empty)" in context assert "## Discussion\n(none)" in context async def test_pull_request_context_labels_empty_sections() -> None: _, context = await build_pull_request_context( cast(Gitea, EmptyPullRequestGitea()), "org", "repo", 6 ) assert "## Pull request body\n(empty)" in context assert "## Commits\n(none)" in context assert "## Timeline discussion\n(none)" in context assert "## Formal and inline reviews\n(none)" in context async def test_pull_request_context_preserves_source_order_and_positions() -> None: _, context = await build_pull_request_context( cast(Gitea, OrderedPullRequestGitea()), "org", "repo", 3 ) assert context.index("First timeline comment") < context.index("Second timeline comment") assert context.index("111111111111 First commit") < context.index("222222222222 Second commit") assert context.index("Review 10 by carol") < context.index("Review 11 by dave") assert "`src/old.py:21`: Old-side position" in context assert "`src/new.py:34`: New-side position" in context