diff --git a/src/agent_gitea/db.py b/src/agent_gitea/db.py index 765c565..16875b5 100644 --- a/src/agent_gitea/db.py +++ b/src/agent_gitea/db.py @@ -292,12 +292,23 @@ class Database: SELECT t.* FROM tasks t JOIN issues i ON i.repo_id = t.repo_id AND i.issue_number = t.issue_number - WHERE t.state = ? + WHERE t.state != ? AND t.pr_number IS NOT NULL AND i.state = 'open' + AND ( + t.state IN (?, ?) + OR t.lease_owner IS NULL + OR t.lease_expires_at IS NULL + OR t.lease_expires_at < ? + ) ORDER BY t.id """, - (TaskState.HUMAN_REVIEW_READY.value,), + ( + TaskState.CANCELLED.value, + TaskState.HUMAN_REVIEW_READY.value, + TaskState.FAILED.value, + dt_to_db(utcnow()), + ), ).fetchall() return [self._task(row) for row in rows] diff --git a/src/agent_gitea/service.py b/src/agent_gitea/service.py index cb3edfa..182c330 100644 --- a/src/agent_gitea/service.py +++ b/src/agent_gitea/service.py @@ -10,7 +10,7 @@ from .agents import CommandRunner, read_report, render_command, write_prompt from .config import AppConfig from .db import Database, PullRequestFeedbackCursor from .gitea import GiteaClient, GiteaComment, GiteaPullReview -from .models import IssueRecord, RepositoryRecord, TaskRecord, TaskState +from .models import ACTIVE_STATES, IssueRecord, RepositoryRecord, TaskRecord, TaskState from .rendering import ( parse_review_report, render_human_review_summary, @@ -92,12 +92,12 @@ def close_issues_for_merged_pull_requests(db: Database, client: GiteaClient) -> ) client.close_issue(owner=repo.owner, name=repo.name, issue_number=issue.issue_number) db.update_issue_state(task.repo_id, task.issue_number, "closed") - db.add_event( - task.id, - task.state, - task.state, - f"closed issue #{issue.issue_number} after merged PR #{task.pr_number}", - ) + message = f"closed issue #{issue.issue_number} after merged PR #{task.pr_number}" + if task.state in ACTIVE_STATES: + db.clear_pr_feedback_pending(task.id) + db.transition(task.id, TaskState.CANCELLED, message=message, clear_lease=True) + else: + db.add_event(task.id, task.state, task.state, message) closed += 1 return closed diff --git a/tests/test_gitea_service.py b/tests/test_gitea_service.py index 495f56e..3d6d497 100644 --- a/tests/test_gitea_service.py +++ b/tests/test_gitea_service.py @@ -656,6 +656,56 @@ def test_close_issues_for_merged_pull_requests_skips_unmerged_pr(db): assert db.get_issue(repo.id, 1).state == "open" # type: ignore[union-attr] +def test_close_issues_for_merged_pull_requests_handles_queued_feedback_task(db): + repo = db.upsert_repository( + owner="acme", + name="service", + clone_url="https://gitea.test/acme/service.git", + default_branch="main", + enabled=True, + ) + db.upsert_issue( + repo_id=repo.id, + issue_number=1, + title="Ready issue", + body="Body", + labels=["agent:ready"], + state="open", + html_url="https://gitea.test/acme/service/issues/1", + ) + task = db.create_task(repo.id, 1) + task = transition_to_human_review_ready(db, task.id, pr_number=5, branch_name="agent/issue-1-ready-issue") + db.mark_pr_feedback_pending(task.id) + db.transition( + task.id, + TaskState.DISCOVERED, + message="queued PR feedback from 1 human comment(s)", + clear_lease=True, + ) + requests: list[tuple[str, str, dict]] = [] + + def handler(request: httpx.Request) -> httpx.Response: + payload = json.loads(request.content.decode() or "{}") + requests.append((request.method, request.url.path, payload)) + if request.url.path == "/api/v1/repos/acme/service/pulls/5": + return httpx.Response(200, json={"number": 5, "state": "closed", "merged": True}) + if request.url.path == "/api/v1/repos/acme/service/issues/1/comments": + return httpx.Response(201, json={"id": 1}) + if request.url.path == "/api/v1/repos/acme/service/issues/1": + return httpx.Response(200, json={"number": 1, "state": "closed"}) + return httpx.Response(404) + + closed = close_issues_for_merged_pull_requests(db, make_client(handler)) + + updated_task = db.get_task(task.id) + assert closed == 1 + assert db.get_issue(repo.id, 1).state == "closed" # type: ignore[union-attr] + assert updated_task is not None + assert updated_task.state == TaskState.CANCELLED + assert not db.has_pending_pr_feedback(task.id) + assert ("PATCH", "/api/v1/repos/acme/service/issues/1", {"state": "closed"}) in requests + + def test_run_task_no_diff_becomes_blocked(db, tmp_path): config = make_config(tmp_path) seed_task(db)