Files
orchestrator/tests/test_status_comment_duration_db_fallback.py

146 lines
5.3 KiB
Python

"""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