"""ORCH-016 / TC-01..TC-10, TC-12, TC-23: unified status comment format. Unit tests for src.usage.build_status_comment(...) — the single hot path for every agent's "I just finished a stage" comment in Plane (ADR-001). Covers: * Header per agent (icon + role + description from AC-1..AC-5). * Verdict / Status line read from frontmatter (reviewer / tester / deployer). * Длительность line shown when duration_s is supplied; suppressed otherwise. * link items per agent. * URL base picks gitea_public_url, falls back to gitea_url. * Graceful behaviour when files are missing / no frontmatter (AC-8). No DB / no network — only the worktree filesystem (via tmp_path). """ import os os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") import pytest # noqa: E402 from src import usage as U # noqa: E402 WID = "ET-016" REPO = "enduro-trails" BRANCH = "feature/ET-016-status-comments" @pytest.fixture(autouse=True) def _set_urls(monkeypatch): """gitea_public_url is the canonical clickable base (AC-9).""" monkeypatch.setattr(U, "logger", U.logger) from src.config import settings monkeypatch.setattr(settings, "gitea_url", "http://localhost:3000", raising=False) monkeypatch.setattr( settings, "gitea_public_url", "https://git.mva154.duckdns.org", raising=False ) monkeypatch.setattr(settings, "gitea_owner", "admin", raising=False) yield def _wt_with_files(tmp_path, files: dict) -> str: """Create a worktree skeleton with given files. `files` maps rel-path -> body.""" base = tmp_path / "wt" docs = base / "docs" / "work-items" / WID docs.mkdir(parents=True) for rel, body in files.items(): p = docs / rel if not rel.startswith("/") else base / rel.lstrip("/") p.parent.mkdir(parents=True, exist_ok=True) p.write_text(body) return str(base) # --------------------------------------------------------------------------- # TC-01: architect comment # --------------------------------------------------------------------------- def test_tc01_architect_comment(tmp_path): wt = _wt_with_files(tmp_path, {"06-adr/ADR-001-x.md": "x"}) html = U.build_status_comment( "architect", repo=REPO, branch=BRANCH, work_item_id=WID, duration_s=312, worktree_root=wt, ) # Header assert "\U0001f4d0 Architect — " in html, html assert "архитектурную" in html assert "См. ADR ниже" in html # Duration: 312s -> 5m 12s assert "Длительность: 5m 12s" in html # ADR link via gitea_public_url assert ("https://git.mva154.duckdns.org/admin/enduro-trails/src/branch/" f"{BRANCH}/docs/work-items/{WID}/06-adr") in html # No Verdict for architect assert "Verdict" not in html assert "Status:" not in html # --------------------------------------------------------------------------- # TC-02: developer comment with PR + branch # --------------------------------------------------------------------------- def test_tc02_developer_comment_links_branch_and_pr(): html = U.build_status_comment( "developer", repo=REPO, branch=BRANCH, work_item_id=WID, pr_number=42, duration_s=600, ) assert "\U0001f4bb Developer — " in html assert "разработку" in html # Both branch and PR links assert f"https://git.mva154.duckdns.org/admin/{REPO}/src/branch/{BRANCH}" in html assert f"https://git.mva154.duckdns.org/admin/{REPO}/pulls/42" in html assert f"PR #42" in html assert "Длительность: 10m 00s" in html assert "Verdict" not in html # --------------------------------------------------------------------------- # TC-03 / TC-04: reviewer verdict via frontmatter # --------------------------------------------------------------------------- def test_tc03_reviewer_verdict_approve(tmp_path): wt = _wt_with_files(tmp_path, { "12-review.md": "---\nverdict: APPROVE\n---\nbody...", }) html = U.build_status_comment( "reviewer", repo=REPO, branch=BRANCH, work_item_id=WID, duration_s=120, worktree_root=wt, ) assert "\U0001f50e Reviewer — " in html assert "Verdict: APPROVE" in html assert "Длительность: 2m 00s" in html assert "12-review.md" in html def test_tc04_reviewer_verdict_request_changes(tmp_path): wt = _wt_with_files(tmp_path, { "12-review.md": "---\nverdict: REQUEST_CHANGES\n---\nblockers...", }) html = U.build_status_comment( "reviewer", repo=REPO, branch=BRANCH, work_item_id=WID, duration_s=45, worktree_root=wt, ) assert "Verdict: REQUEST_CHANGES" in html assert "Длительность: 45s" in html # --------------------------------------------------------------------------- # TC-05: reviewer with NO 12-review.md -> graceful (no Verdict, no Review link) # but Длительность and header still present. # --------------------------------------------------------------------------- def test_tc05_reviewer_missing_artifact_graceful(tmp_path): wt = _wt_with_files(tmp_path, {}) # empty docs dir html = U.build_status_comment( "reviewer", repo=REPO, branch=BRANCH, work_item_id=WID, duration_s=30, worktree_root=wt, ) assert "\U0001f50e Reviewer — " in html assert "Verdict" not in html # Link to 12-review.md is dropped (AC-8 graceful). assert "12-review.md" not in html # Duration still printed when known. assert "Длительность: 30s" in html # --------------------------------------------------------------------------- # TC-06 / TC-07: tester verdict via frontmatter (verdict OR status) # --------------------------------------------------------------------------- def test_tc06_tester_pass(tmp_path): wt = _wt_with_files(tmp_path, { "13-test-report.md": "---\nverdict: PASS\n---\n", }) html = U.build_status_comment( "tester", repo=REPO, branch=BRANCH, work_item_id=WID, duration_s=240, worktree_root=wt, ) assert "\U0001f9ea Tester — " in html assert "Verdict: PASS" in html assert "Длительность: 4m 00s" in html assert "13-test-report.md" in html def test_tc07_tester_fail(tmp_path): wt = _wt_with_files(tmp_path, { "13-test-report.md": "---\nverdict: FAIL\n---\n", }) html = U.build_status_comment( "tester", repo=REPO, branch=BRANCH, work_item_id=WID, duration_s=240, worktree_root=wt, ) assert "Verdict: FAIL" in html assert "Длительность: 4m 00s" in html def test_tc07b_tester_falls_back_to_status_key(tmp_path): # Some testers used `status:` instead of `verdict:` (ET-006 / ET-008 pattern). wt = _wt_with_files(tmp_path, { "13-test-report.md": "---\nstatus: PASSED\n---\n", }) html = U.build_status_comment( "tester", repo=REPO, branch=BRANCH, work_item_id=WID, duration_s=10, worktree_root=wt, ) assert "Verdict: PASSED" in html # --------------------------------------------------------------------------- # TC-08 / TC-09: deployer status via frontmatter # --------------------------------------------------------------------------- def test_tc08_deployer_deploy_status_success(tmp_path): wt = _wt_with_files(tmp_path, { "14-deploy-log.md": "---\ndeploy_status: SUCCESS\n---\n", }) html = U.build_status_comment( "deployer", repo=REPO, branch=BRANCH, work_item_id=WID, stage="deploy", duration_s=120, worktree_root=wt, ) assert "\U0001f680 Deployer — " in html assert "Status: SUCCESS" in html assert "Длительность: 2m 00s" in html assert "14-deploy-log.md" in html def test_tc09_deployer_staging_status_success(tmp_path): wt = _wt_with_files(tmp_path, { "15-staging-log.md": "---\nstaging_status: SUCCESS\n---\n", }) html = U.build_status_comment( "deployer", repo=REPO, branch=BRANCH, work_item_id=WID, stage="deploy-staging", duration_s=60, worktree_root=wt, ) assert "Status: SUCCESS" in html assert "Длительность: 1m 00s" in html # The staging-stage helper links 15-staging-log.md, not 14-deploy-log.md. assert "15-staging-log.md" in html assert "14-deploy-log.md" not in html def test_deployer_status_failed_drives_status_line(tmp_path): wt = _wt_with_files(tmp_path, { "14-deploy-log.md": "---\ndeploy_status: FAILED\n---\n", }) html = U.build_status_comment( "deployer", repo=REPO, branch=BRANCH, work_item_id=WID, stage="deploy", duration_s=5, worktree_root=wt, ) assert "Status: FAILED" in html # --------------------------------------------------------------------------- # TC-10: gitea_public_url is preferred; falls back to gitea_url when empty. # --------------------------------------------------------------------------- def test_tc10_url_fallback_to_gitea_url(monkeypatch): from src.config import settings monkeypatch.setattr(settings, "gitea_public_url", "", raising=False) monkeypatch.setattr(settings, "gitea_url", "http://localhost:3000", raising=False) html = U.build_status_comment( "developer", repo=REPO, branch=BRANCH, work_item_id=WID, pr_number=7, duration_s=15, ) assert "http://localhost:3000/admin/enduro-trails/pulls/7" in html # And the public URL is not there because it was empty. assert "git.mva154.duckdns.org" not in html # --------------------------------------------------------------------------- # TC-12: frontmatter parser is graceful — missing file / empty / bad YAML -> None # (the comment still publishes the header + duration, just no Verdict / Status). # --------------------------------------------------------------------------- def test_tc12_frontmatter_missing_file_no_crash(tmp_path): from src.frontmatter import read_frontmatter_value assert read_frontmatter_value(str(tmp_path / "nope.md"), "verdict") is None def test_tc12_frontmatter_empty_no_crash(tmp_path): p = tmp_path / "empty.md" p.write_text("") from src.frontmatter import read_frontmatter_value assert read_frontmatter_value(str(p), "verdict") is None def test_tc12_frontmatter_bad_yaml_no_crash(tmp_path): p = tmp_path / "bad.md" p.write_text("---\nverdict: [unterminated\n---\nbody") from src.frontmatter import read_frontmatter_value assert read_frontmatter_value(str(p), "verdict") is None def test_tc12_frontmatter_missing_key_returns_none(tmp_path): p = tmp_path / "ok.md" p.write_text("---\nother: value\n---\nbody") from src.frontmatter import read_frontmatter_value assert read_frontmatter_value(str(p), "verdict") is None # --------------------------------------------------------------------------- # TC-23: duration_s=None and no task_id -> the Длительность line is OMITTED. # Header / description / artifact links remain. # --------------------------------------------------------------------------- def test_tc23_no_duration_no_line(tmp_path): wt = _wt_with_files(tmp_path, {"06-adr/ADR-001-x.md": "x"}) html_none = U.build_status_comment( "architect", repo=REPO, branch=BRANCH, work_item_id=WID, duration_s=None, worktree_root=wt, ) html_default = U.build_status_comment( "architect", repo=REPO, branch=BRANCH, work_item_id=WID, worktree_root=wt, ) for html in (html_none, html_default): assert "Длительность" not in html # But the header, description and ADR link are still there. assert "\U0001f4d0 Architect — " in html assert "архитектурную" in html assert "06-adr" in html # --------------------------------------------------------------------------- # Extra: usage tail is rendered as when non-zero, suppressed otherwise. # (Backs up ADR-001 §3 and keeps the old usage_comment test contract.) # --------------------------------------------------------------------------- def test_usage_tail_rendered_when_non_zero(): html = U.build_status_comment( "developer", repo=REPO, branch=BRANCH, work_item_id=WID, usage={"input_tokens": 45231, "output_tokens": 12100, "cost_usd": 0.21}, ) assert "" in html and "" in html assert "45.2k in" in html assert "12.1k out" in html assert "$0.21" in html def test_usage_tail_suppressed_when_all_zero(): html = U.build_status_comment("developer", repo=REPO, branch=BRANCH) assert "" not in html # --------------------------------------------------------------------------- # AC-1 / AC-5 literal strings — fixed wording per role. # --------------------------------------------------------------------------- def test_ac1_architect_header_literal(): html = U.build_status_comment("architect", repo=REPO, branch=BRANCH, work_item_id=WID, duration_s=10) assert "\U0001f4d0 Architect — " in html def test_ac5_deployer_deploy_description(): html = U.build_status_comment( "deployer", repo=REPO, branch=BRANCH, work_item_id=WID, stage="deploy", ) assert "прод-деплой" in html def test_ac5_deployer_staging_description(): html = U.build_status_comment( "deployer", repo=REPO, branch=BRANCH, work_item_id=WID, stage="deploy-staging", ) assert "staging-деплой" in html