From e15d339b14973a04e8ca130ca7d94ae1949c7624 Mon Sep 17 00:00:00 2001 From: Dev Agent Date: Thu, 4 Jun 2026 01:22:43 +0300 Subject: [PATCH] fix(qg): use check_ci_green instead of local tests on development stage --- src/qg/checks.py | 3 +++ src/stages.py | 2 +- src/webhooks/gitea.py | 10 ++++------ tests/test_qg.py | 17 ++++++++++++++++ tests/test_stage_engine.py | 7 +++++-- tests/test_webhooks.py | 40 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/qg/checks.py b/src/qg/checks.py index 769c94d..089e1d1 100644 --- a/src/qg/checks.py +++ b/src/qg/checks.py @@ -249,6 +249,9 @@ def check_reviewer_verdict(repo: str, work_item_id: str, branch: str | None = No def check_tests_local(repo: str, branch: str) -> tuple[bool, str]: """ + DEPRECATED: replaced by check_ci_green on the development stage (CI is now + configured). Kept for backward-compat; not wired to any stage. + S-1 fix: run the project test suite locally and judge by exit code, instead of depending on Gitea CI (which is not configured -> always false). diff --git a/src/stages.py b/src/stages.py index 5759313..f796979 100644 --- a/src/stages.py +++ b/src/stages.py @@ -13,7 +13,7 @@ STAGE_TRANSITIONS = { "created": {"next": "analysis", "agent": "analyst", "qg": None}, "analysis": {"next": "architecture", "agent": "architect", "qg": "check_analysis_approved"}, "architecture": {"next": "development", "agent": "developer", "qg": "check_architecture_done"}, - "development": {"next": "review", "agent": "reviewer", "qg": "check_tests_local"}, + "development": {"next": "review", "agent": "reviewer", "qg": "check_ci_green"}, "review": {"next": "testing", "agent": "tester", "qg": "check_reviewer_verdict"}, "testing": {"next": "deploy", "agent": "deployer", "qg": "check_tests_passed"}, "deploy": {"next": "done", "agent": None, "qg": None}, diff --git a/src/webhooks/gitea.py b/src/webhooks/gitea.py index 0957294..023acf1 100644 --- a/src/webhooks/gitea.py +++ b/src/webhooks/gitea.py @@ -216,12 +216,10 @@ async def handle_ci_status(payload: dict): else: notify_qg_failure(task_id, current_stage, "check_ci_green", reason) - elif state == "failure": - # S-1: Gitea CI is NOT the authoritative gate anymore (the orchestrator runs - # tests locally via check_tests_local). Gitea CI is often unconfigured, so a - # "failure"/empty status here is not actionable. Log only, do not alert. - logger.debug(f"Task {task_id}: Gitea CI state='failure' on branch '{branch}' " - f"(non-authoritative, suppressed — local tests are the gate)") + elif state == "failure" and current_stage == "development": + # CI is now the authoritative gate for development -> review. + # A failing CI means the QG did not pass; notify (do not silently advance). + notify_qg_failure(task_id, current_stage, "check_ci_green", f"Gitea CI failed on branch '{branch}'") async def handle_pr(payload: dict): diff --git a/tests/test_qg.py b/tests/test_qg.py index ed4adaf..e211f00 100644 --- a/tests/test_qg.py +++ b/tests/test_qg.py @@ -19,6 +19,7 @@ from src.qg.checks import ( check_tests_passed, check_tests_local, ) +from src.stages import get_qg_for_stage @pytest.fixture(autouse=True) @@ -189,6 +190,22 @@ class TestCheckTestsPassed: assert "not found" in reason.lower() +class TestDevelopmentStageQG: + """BUG 6: development stage QG is now check_ci_green (CI is the authoritative + gate), not the deprecated check_tests_local.""" + + def test_development_qg_is_check_ci_green(self): + assert get_qg_for_stage("development") == "check_ci_green" + + def test_check_tests_local_is_deprecated_and_unwired(self): + # Kept in the registry for backward-compat, but not wired to any stage. + from src.qg.checks import QG_CHECKS + from src.stages import STAGE_TRANSITIONS + assert "check_tests_local" in QG_CHECKS + wired = {t.get("qg") for t in STAGE_TRANSITIONS.values()} + assert "check_tests_local" not in wired + + class TestCheckTestsLocal: """BUG 5: check_tests_local must run pytest directly (not make, which is not installed in the orchestrator container).""" diff --git a/tests/test_stage_engine.py b/tests/test_stage_engine.py index cb8eb0f..b74ca8f 100644 --- a/tests/test_stage_engine.py +++ b/tests/test_stage_engine.py @@ -203,10 +203,13 @@ class TestQgFailureDoesNotAdvance: assert _jobs() == [] def test_webhook_path_emits_qg_failure_notification(self, monkeypatch): - """finished_agent=None -> generic QG-failure notification fires (plane parity).""" + """finished_agent=None -> generic QG-failure notification fires (plane parity). + + development stage QG is now check_ci_green (was check_tests_local). + """ monkeypatch.setattr( stage_engine, "QG_CHECKS", - {**stage_engine.QG_CHECKS, "check_tests_local": _fail("ci red")}, + {**stage_engine.QG_CHECKS, "check_ci_green": _fail("ci red")}, ) task_id = _make_task("development") advance_stage(task_id, "development", "enduro-trails", "ET-001", diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index 4b24617..1ee25ab 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -272,6 +272,46 @@ def test_gitea_ci_success_advances_to_review(mock_launcher, mock_ci): assert task["stage"] == "review" +@patch("src.webhooks.gitea.notify_qg_failure") +@patch("src.webhooks.gitea.launcher") +def test_gitea_ci_failure_on_development_notifies_qg_failure(mock_launcher, mock_notify): + """BUG 6: CI failure at development is now the authoritative QG gate failing. + + It must notify QG failure (not silently suppress) and must NOT advance the stage. + """ + conn = get_db() + conn.execute( + "INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) VALUES (?, ?, ?, ?, ?)", + ("ci-fail-001", "ET-011", "enduro-trails", "feature/ET-011-test", "development"), + ) + conn.commit() + conn.close() + + resp = client.post( + "/webhook/gitea", + json={ + "state": "failure", + "branches": [{"name": "feature/ET-011-test"}], + "repository": {"name": "enduro-trails"}, + }, + headers={"X-Gitea-Event": "status"}, + ) + assert resp.status_code == 200 + + # QG failure was reported for the development stage with check_ci_green. + assert mock_notify.called + args, kwargs = mock_notify.call_args + call = list(args) + list(kwargs.values()) + assert "development" in call + assert "check_ci_green" in call + + # Stage did NOT advance. + conn = get_db() + task = conn.execute("SELECT * FROM tasks WHERE plane_id = 'ci-fail-001'").fetchone() + conn.close() + assert task["stage"] == "development" + + def test_gitea_webhook_pr(): """PR event is accepted.""" resp = client.post( -- 2.49.1