Files
orchestrator/tests/test_post_usage_comments_integration.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

200 lines
6.8 KiB
Python

"""ORCH-016 / TC-13..TC-15: _post_usage_comments integration tests.
End-to-end (DB + filesystem worktree, no network) verification that
AgentLauncher._post_usage_comments:
- resolves the task by (repo, branch),
- threads the explicit duration_s into build_status_comment,
- posts exactly ONE status comment authored by the finishing agent,
- for deployer: ALSO posts the per-task usage summary (deployer authorship).
The actual Plane HTTP call (plane_sync.add_comment) is patched out; we only
check the (work_item_id, body, author) tuples the launcher passes to it.
"""
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_post_usage.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 setup_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_worktree(monkeypatch, tmp_path):
"""Stub get_worktree_path inside the launcher module to a tmp_path location."""
wt = tmp_path / "wt"
(wt / "docs" / "work-items" / WID).mkdir(parents=True)
def _get_wt(repo, branch):
return str(wt)
# The launcher imports get_worktree_path lazily inside the function body
# (`from ..git_worktree import get_worktree_path`); patch the source module.
monkeypatch.setattr("src.git_worktree.get_worktree_path", _get_wt)
monkeypatch.setattr("src.usage._input_total", lambda u: 0) # quiet <sub> tail
return wt
@pytest.fixture
def capture_comments(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.fixture
def public_url(monkeypatch):
from src.config import settings
monkeypatch.setattr(
settings, "gitea_public_url", "https://git.mva154.duckdns.org", raising=False
)
monkeypatch.setattr(settings, "gitea_url", "http://localhost:3000", raising=False)
monkeypatch.setattr(settings, "gitea_owner", "admin", raising=False)
# ---------------------------------------------------------------------------
# TC-13: reviewer comment.
# ---------------------------------------------------------------------------
def test_tc13_reviewer_posts_one_status_comment(
setup_db, fake_worktree, capture_comments, public_url
):
(fake_worktree / "docs" / "work-items" / WID / "12-review.md").write_text(
"---\nverdict: APPROVE\n---\nReviewed.",
)
AgentLauncher()._post_usage_comments(
run_id=99, agent="reviewer", repo=REPO, branch=BRANCH,
usage={"input_tokens": 1, "output_tokens": 1, "cost_usd": 0.01},
duration_s=180,
)
assert len(capture_comments) == 1
post = capture_comments[0]
assert post["wid"] == WID
assert post["author"] == "reviewer"
body = post["body"]
assert "\U0001f50e Reviewer" in body
assert "Verdict: APPROVE" in body
assert "Длительность: 3m 00s" in body
assert "12-review.md" in body
# ---------------------------------------------------------------------------
# TC-14: tester comment.
# ---------------------------------------------------------------------------
def test_tc14_tester_posts_one_status_comment(
setup_db, fake_worktree, capture_comments, public_url
):
(fake_worktree / "docs" / "work-items" / WID / "13-test-report.md").write_text(
"---\nverdict: PASS\n---\n",
)
AgentLauncher()._post_usage_comments(
run_id=100, agent="tester", repo=REPO, branch=BRANCH,
usage=None, duration_s=42,
)
assert len(capture_comments) == 1
post = capture_comments[0]
assert post["author"] == "tester"
body = post["body"]
assert "\U0001f9ea Tester" in body
assert "Verdict: PASS" in body
assert "Длительность: 42s" in body
# ---------------------------------------------------------------------------
# TC-15: deployer comment + per-task summary (two comments, both from deployer).
# ---------------------------------------------------------------------------
def test_tc15_deployer_posts_status_then_summary(
setup_db, fake_worktree, capture_comments, public_url
):
# Task stage = 'deploy' so build_status_comment uses 14-deploy-log.md.
conn = get_db()
conn.execute("UPDATE tasks SET stage='deploy' WHERE id=1")
conn.commit()
conn.close()
(fake_worktree / "docs" / "work-items" / WID / "14-deploy-log.md").write_text(
"---\ndeploy_status: SUCCESS\n---\nDeployed.",
)
AgentLauncher()._post_usage_comments(
run_id=101, agent="deployer", repo=REPO, branch=BRANCH,
usage={"input_tokens": 1, "output_tokens": 1, "cost_usd": 0.01},
duration_s=300,
)
# 2 comments: status + per-task summary.
assert len(capture_comments) == 2
status, summary = capture_comments
assert status["author"] == "deployer"
assert "Status: SUCCESS" in status["body"]
assert "Длительность: 5m 00s" in status["body"]
assert "14-deploy-log.md" in status["body"]
assert summary["author"] == "deployer"
# task_summary_comment header (Russian "Итого по задаче").
assert "\U0001f4ca" in summary["body"]
assert "Итого" in summary["body"]
def test_deployer_staging_picks_15_log(
setup_db, fake_worktree, capture_comments, public_url
):
conn = get_db()
conn.execute("UPDATE tasks SET stage='deploy-staging' WHERE id=1")
conn.commit()
conn.close()
(fake_worktree / "docs" / "work-items" / WID / "15-staging-log.md").write_text(
"---\nstaging_status: SUCCESS\n---\n",
)
AgentLauncher()._post_usage_comments(
run_id=102, agent="deployer", repo=REPO, branch=BRANCH,
usage=None, duration_s=10,
)
# deployer always also posts the summary; check the FIRST comment is status.
assert len(capture_comments) == 2
status = capture_comments[0]
assert "Status: SUCCESS" in status["body"]
assert "15-staging-log.md" in status["body"]
assert "14-deploy-log.md" not in status["body"]
assert "staging-деплой" in status["body"]