Files
orchestrator/tests/test_status_comment_authorship.py
claude-bot 0663da6e4c
All checks were successful
CI / test (push) Successful in 11s
CI / test (pull_request) Successful in 12s
feat(plane): unified status-comment format with duration line (ORCH-016)
Все агенты (analyst..deployer) теперь пишут финальный коммент через единый
хелпер usage.build_status_comment(...) — заголовок «{icon} {Role} — {описание}»,
опциональная строка Verdict/Status из YAML-frontmatter, строка
«Длительность: 4m 12s» (явный duration_s от launcher, fallback из agent_runs
для аналитика), HTML-блок Документы, тех-хвост <sub>tokens · cost</sub>.

- Новые публичные функции в src/usage.py: build_status_comment, fmt_duration,
  get_agent_duration. usage_comment(...) → тонкая deprecated-обёртка (legacy
  тесты в tests/test_usage.py продолжают работать). artifact_links(...)
  переписан на HTML <li><a>…</a></li> (breaking change для внутреннего API,
  но единственный внешний клиент — _post_usage_comments — мигрирован).
- Новый модуль src/frontmatter.py: defensive YAML reader, никогда не raise.
- stage_engine._build_analyst_ready_comment(...) теперь тонкая обёртка над
  build_status_comment(agent="analyst", ...); task_id пробрасывается из
  _handle_analysis_approved_flow для DB-фоллбэка длительности (AC-14).
- launcher._post_usage_comments(...) принимает duration_s, резолвит stage из
  tasks для deployer и worktree_root для AC-8 graceful skipping.

Тесты (16 файлов, 56 новых тестовых функций, покрывают TC-01..TC-25):
fmt_duration table, build_status_comment по всем агентам, DB-фоллбэк,
authorship под per-agent ботами, дедуп-инвариант, regression на
status-only verdict аналитика и финальный notify_done, snapshot
QG_CHECKS + STAGE_TRANSITIONS.

Документация: docs/architecture/README.md (раздел Plane Sync),
CHANGELOG.md (Unreleased Added/Changed).

Refs: ORCH-016

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 12:39:06 +00:00

123 lines
4.1 KiB
Python

"""ORCH-016 / TC-19 + AC-1..AC-5 authorship: status comments use per-agent bots.
When a status comment is posted by AgentLauncher._post_usage_comments, the
underlying plane_sync.add_comment must be invoked with ``author=<agent>`` so
plane_sync._headers_for(<agent>) picks the agent's bot token
(PLANE_BOT_TOKENS[role]) — falling back to PLANE_HEADERS when the bot token
is empty / role unknown. Comment FORMAT changes (ORCH-016) must not affect
that authorship contract.
"""
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_authorship.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
from src.agents.launcher import AgentLauncher # noqa: E402
REPO = "enduro-trails"
BRANCH = "feature/ET-016-x"
WID = "ET-016"
@pytest.fixture
def db(monkeypatch):
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, ?, ?, 'review', ?)",
(REPO, BRANCH, WID),
)
conn.commit()
conn.close()
yield
if os.path.exists(_test_db):
os.unlink(_test_db)
@pytest.fixture
def fake_wt(monkeypatch, tmp_path):
base = tmp_path / "wt"
(base / "docs" / "work-items" / WID).mkdir(parents=True)
monkeypatch.setattr("src.git_worktree.get_worktree_path", lambda r, b: str(base))
return base
@pytest.fixture
def capture(monkeypatch):
posts = []
def _spy(work_item_id, body, author=None, **kwargs):
posts.append({"wid": work_item_id, "body": body, "author": author})
monkeypatch.setattr("src.agents.launcher.plane_add_comment", _spy)
return posts
@pytest.mark.parametrize("agent", ["architect", "developer", "reviewer", "tester"])
def test_tc19_status_comment_carries_agent_author(agent, db, fake_wt, capture):
"""Each agent's status comment must be POST-ed under that agent's bot."""
AgentLauncher()._post_usage_comments(
run_id=1, agent=agent, repo=REPO, branch=BRANCH,
usage=None, duration_s=10,
)
assert len(capture) >= 1
assert capture[0]["author"] == agent, (
f"Expected author={agent!r}, got {capture[0]['author']!r}"
)
def test_tc19_deployer_status_and_summary_both_authored_by_deployer(db, fake_wt, capture):
"""Deployer posts TWO comments (status + per-task summary) — both ``author='deployer'``."""
conn = get_db()
conn.execute("UPDATE tasks SET stage='deploy' WHERE id=1")
conn.commit()
conn.close()
AgentLauncher()._post_usage_comments(
run_id=2, agent="deployer", repo=REPO, branch=BRANCH,
usage=None, duration_s=10,
)
assert len(capture) == 2
assert {c["author"] for c in capture} == {"deployer"}
def test_tc19_headers_for_unknown_role_falls_back(monkeypatch):
"""Ensure plane_sync._headers_for handles unknown agents (fallback contract)."""
from src import plane_sync
h = plane_sync._headers_for("unknown_role_xyz")
# PLANE_HEADERS fallback uses settings.plane_api_token (set to 'test-token').
assert isinstance(h, dict) and "X-API-Key" in h
def test_tc19_status_comment_format_preserves_author_contract(db, fake_wt, capture):
"""The ORCH-016 format change must not strip the author= kw from the call site."""
(fake_wt / "docs" / "work-items" / WID / "12-review.md").write_text(
"---\nverdict: APPROVE\n---\n",
)
AgentLauncher()._post_usage_comments(
run_id=3, agent="reviewer", repo=REPO, branch=BRANCH,
usage={"input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0},
duration_s=180,
)
assert len(capture) == 1
post = capture[0]
assert post["author"] == "reviewer"
# And the new format is present in the body (sanity).
assert "\U0001f50e Reviewer" in post["body"]
assert "Verdict: APPROVE" in post["body"]
assert "Длительность: 3m 00s" in post["body"]