215 lines
6.4 KiB
Python
215 lines
6.4 KiB
Python
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
|