"""ORCH-016 / TC-16 + AC-6: analyst status-only regression. Status-only verdict model (PR #12 / #13): - analyst finishes its run -> Plane state becomes In Review, - ONE status comment is posted asking the stakeholder to flip the status to Approved (or write a reason and switch to Rejected), - NO auto-advance happens — the next stage waits for human approval. The ORCH-016 PR refactors the comment text into the unified status-comment helper. This regression test guards against: (a) the analyst path silently auto-advancing, (b) the analyst comment losing the «Approved» / «Rejected» instruction text, (c) the comment switching authorship away from the analyst bot. We exercise `_handle_analysis_approved_flow` directly (the launcher path). """ import os import tempfile os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") _test_db = os.path.join(tempfile.gettempdir(), "test_orch016_analyst_so.db") os.environ["ORCH_DB_PATH"] = _test_db import pytest # noqa: E402 from src import db as db_module # noqa: E402 from src.db import init_db, get_db # noqa: E402 REPO = "enduro-trails" BRANCH = "feature/ET-016-x" WID = "ET-016" @pytest.fixture(autouse=True) def setup_db(monkeypatch, tmp_path): monkeypatch.setattr(db_module.settings, "db_path", _test_db, raising=False) if os.path.exists(_test_db): os.unlink(_test_db) init_db() conn = get_db() conn.execute( "INSERT INTO tasks (id, repo, branch, stage, work_item_id) " "VALUES (1, ?, ?, 'analysis', ?)", (REPO, BRANCH, WID), ) conn.commit() conn.close() yield if os.path.exists(_test_db): os.unlink(_test_db) @pytest.fixture def fake_worktree(monkeypatch, tmp_path): base = tmp_path / "wt" docs = base / "docs" / "work-items" / WID docs.mkdir(parents=True) # All analyst artifacts present -> "files_check" returns True. for f in ("01-brd.md", "02-trz.md", "03-acceptance-criteria.md", "04-test-plan.yaml"): (docs / f).write_text("x") monkeypatch.setattr("src.git_worktree.get_worktree_path", lambda r, b: str(base)) monkeypatch.setattr("src.stage_engine.get_worktree_path", lambda r, b: str(base)) monkeypatch.setattr("src.qg.checks.get_worktree_path", lambda r, b: str(base)) return base @pytest.fixture def collect_calls(monkeypatch): calls = {"in_review": 0, "advance": 0, "comments": [], "enqueued": []} monkeypatch.setattr( "src.stage_engine.set_issue_in_review", lambda wid: calls.__setitem__("in_review", calls["in_review"] + 1), ) monkeypatch.setattr( "src.stage_engine.notify_approve_requested", lambda tid: None ) def _add_comment(wid, body, author=None, **kw): calls["comments"].append({"wid": wid, "body": body, "author": author}) monkeypatch.setattr("src.stage_engine.plane_add_comment", _add_comment) # advance_stage isn't directly hit; if anything calls update_task_stage to # 'architecture', we'd see it here. def _update_task_stage(task_id, stage): calls["advance"] += 1 monkeypatch.setattr("src.stage_engine.update_task_stage", _update_task_stage) def _enqueue(*a, **k): calls["enqueued"].append((a, k)) return 1 monkeypatch.setattr("src.stage_engine.enqueue_job", _enqueue) return calls def test_tc16_analyst_goes_to_in_review_no_advance(fake_worktree, collect_calls): """When the analyst finishes with complete artifacts, the task goes to In Review and NO advance/enqueue happens — the human approves via Plane status. """ from src.stage_engine import _handle_analysis_approved_flow, AdvanceResult result = AdvanceResult(from_stage="analysis") _handle_analysis_approved_flow( task_id=1, current_stage="analysis", repo=REPO, work_item_id=WID, branch=BRANCH, agent="analyst", result=result, ) # In Review state requested in Plane. assert collect_calls["in_review"] == 1, collect_calls # NO stage-machine advance. assert collect_calls["advance"] == 0, collect_calls # NO new job enqueued by the analyst path. assert collect_calls["enqueued"] == [], collect_calls # Exactly one comment posted, authored by analyst, with required text bits. assert len(collect_calls["comments"]) == 1, collect_calls["comments"] c = collect_calls["comments"][0] assert c["wid"] == WID assert c["author"] == "analyst" body = c["body"] assert "Approved" in body assert "Rejected" in body assert ":approved:" not in body assert "In Progress" not in body # AC-6 +: the new unified format adds a Длительность line (DB fallback). # No agent_runs row exists in this test, so the line should be ABSENT. assert "Длительность" not in body