feat(plane): unified status-comment format with duration line (ORCH-016) (#34)
This commit was merged in pull request #34.
This commit is contained in:
145
tests/test_status_comment_duration_db_fallback.py
Normal file
145
tests/test_status_comment_duration_db_fallback.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""ORCH-016 / TC-24 + TC-25 + AC-14: DB fallback for the duration line.
|
||||
|
||||
When build_status_comment is called WITHOUT an explicit duration_s but with a
|
||||
task_id, it must:
|
||||
- read the last finished agent_runs row for (task_id, agent),
|
||||
- compute (julianday(finished_at) - julianday(started_at)) * 86400 in seconds,
|
||||
- format it via fmt_duration and inject the «Длительность: …» line.
|
||||
|
||||
Failure modes (DB locked / row missing / NULL finished_at / negative diff) must
|
||||
NEVER raise; they simply suppress the duration line and let the rest of the
|
||||
comment publish.
|
||||
"""
|
||||
|
||||
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_duration_fallback.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 import usage as U # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
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()
|
||||
yield
|
||||
if os.path.exists(_test_db):
|
||||
os.unlink(_test_db)
|
||||
|
||||
|
||||
def _insert_run(task_id, agent, *, seconds_ago_start=None, finished=True):
|
||||
"""Insert an agent_runs row with controllable timestamps."""
|
||||
conn = get_db()
|
||||
if seconds_ago_start is None:
|
||||
conn.execute(
|
||||
"INSERT INTO agent_runs (task_id, agent) VALUES (?, ?)",
|
||||
(task_id, agent),
|
||||
)
|
||||
else:
|
||||
if finished:
|
||||
conn.execute(
|
||||
"INSERT INTO agent_runs (task_id, agent, started_at, finished_at) "
|
||||
"VALUES (?, ?, datetime('now', ?), datetime('now'))",
|
||||
(task_id, agent, f"-{seconds_ago_start} seconds"),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT INTO agent_runs (task_id, agent, started_at) "
|
||||
"VALUES (?, ?, datetime('now', ?))",
|
||||
(task_id, agent, f"-{seconds_ago_start} seconds"),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-24: explicit duration_s missing -> DB lookup populates the line.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc24_fallback_reads_agent_runs_for_last_finished():
|
||||
_insert_run(7, "reviewer", seconds_ago_start=240)
|
||||
secs = U.get_agent_duration(7, "reviewer")
|
||||
# SQLite's julianday math can be off by a second on either side.
|
||||
assert secs is not None and abs(secs - 240) <= 1, secs
|
||||
|
||||
html = U.build_status_comment("reviewer", task_id=7)
|
||||
assert any(
|
||||
s in html for s in (
|
||||
"Длительность: 4m 00s",
|
||||
"Длительность: 4m 01s",
|
||||
"Длительность: 3m 59s",
|
||||
)
|
||||
), html
|
||||
|
||||
|
||||
def test_tc24_fallback_picks_last_run_when_multiple():
|
||||
_insert_run(11, "developer", seconds_ago_start=120)
|
||||
_insert_run(11, "developer", seconds_ago_start=10)
|
||||
secs = U.get_agent_duration(11, "developer")
|
||||
assert secs is not None and abs(secs - 10) <= 1, secs
|
||||
|
||||
|
||||
def test_tc24_no_row_returns_none():
|
||||
assert U.get_agent_duration(999, "tester") is None
|
||||
|
||||
|
||||
def test_tc24_finished_at_null_returns_none():
|
||||
_insert_run(13, "tester", seconds_ago_start=100, finished=False)
|
||||
assert U.get_agent_duration(13, "tester") is None
|
||||
|
||||
|
||||
def test_tc24_missing_args_returns_none():
|
||||
assert U.get_agent_duration(None, "tester") is None
|
||||
assert U.get_agent_duration(7, "") is None
|
||||
assert U.get_agent_duration(0, "tester") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-25: read failure -> logged at debug, NO exception, comment still ships.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc25_db_read_failure_no_raise(monkeypatch, caplog):
|
||||
"""A locked / broken DB must not crash the status comment hot path."""
|
||||
import logging
|
||||
|
||||
def _boom():
|
||||
raise RuntimeError("simulated DB outage")
|
||||
|
||||
monkeypatch.setattr(U, "get_db", _boom)
|
||||
with caplog.at_level(logging.DEBUG, logger="orchestrator.usage"):
|
||||
assert U.get_agent_duration(1, "developer") is None
|
||||
# build_status_comment must still publish (no duration line, no crash).
|
||||
html = U.build_status_comment("developer", task_id=1, repo="r", branch="b")
|
||||
assert "Длительность" not in html
|
||||
assert "\U0001f4bb Developer" in html
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sanity: explicit duration_s wins over DB fallback (no SELECT at all).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_explicit_duration_wins_over_db_fallback(monkeypatch):
|
||||
called = {"n": 0}
|
||||
real = U.get_agent_duration
|
||||
|
||||
def _spy(task_id, agent):
|
||||
called["n"] += 1
|
||||
return real(task_id, agent)
|
||||
|
||||
monkeypatch.setattr(U, "get_agent_duration", _spy)
|
||||
_insert_run(5, "architect", seconds_ago_start=300)
|
||||
|
||||
html = U.build_status_comment(
|
||||
"architect", task_id=5, duration_s=12, repo="r", branch="b",
|
||||
)
|
||||
assert "Длительность: 12s" in html
|
||||
# Explicit value supplied -> DB fallback is short-circuited.
|
||||
assert called["n"] == 0
|
||||
Reference in New Issue
Block a user