"""Tests for navi.skills.runs — the protocol run log against a fake pool."""
from datetime import datetime, timezone
import pytest
from navi.skills.runs import ProtocolRunStore
from tests.conftest_factory import FakeConnection, FakePool, FakeRecord
def _store(conn: FakeConnection | None = None) -> tuple[ProtocolRunStore, FakeConnection]:
conn = conn or FakeConnection()
store = ProtocolRunStore(FakePool(conn))
# Skip the DDL round trip: it would eat a queued result.
store._ready = True
return store, conn
def _row(**overrides) -> FakeRecord:
base = {
"id": "run-1",
"protocol_id": "proto-1",
"protocol_name": "Выключение",
"protocol_version": 2,
"session_id": "s1",
"user_id": "u1",
"status": "running",
"steps": ["Раз", "Два"],
"post_checks": ["Готово"],
"report": "",
"reason": "",
"started_at": datetime(2026, 11, 7, 10, 0, tzinfo=timezone.utc),
"finished_at": None,
}
base.update(overrides)
return FakeRecord(**base)
async def test_start_writes_the_snapshot_and_reads_it_back():
store, conn = _store()
conn.enqueue(None) # INSERT
conn.enqueue(_row()) # the SELECT that follows
run = await store.start(
protocol_id="proto-1",
protocol_name="Выключение",
protocol_version=2,
session_id="s1",
user_id="u1",
steps=["Раз", "Два"],
post_checks=["Готово"],
)
insert = conn.calls[0]
assert insert[0] == "execute"
assert "INSERT INTO protocol_runs" in insert[1]
assert "'running'" in insert[1]
assert '["Раз", "Два"]' in insert[2]
assert insert[2][1] == "proto-1"
assert insert[2][3] == 2
assert run.id == "run-1"
assert run.steps == ["Раз", "Два"]
assert run.status == "running"
async def test_jsonb_columns_come_back_as_text():
store, conn = _store()
conn.enqueue(_row(steps='["Раз","Два"]', post_checks='["Готово"]'))
run = await store.get("run-1")
assert run.steps == ["Раз", "Два"]
assert run.post_checks == ["Готово"]
async def test_broken_json_is_an_empty_list():
store, conn = _store()
conn.enqueue(_row(steps="{not json"))
run = await store.get("run-1")
assert run.steps == []
async def test_active_returns_the_running_row():
store, conn = _store()
conn.enqueue(_row())
run = await store.active("s1")
assert run is not None
assert run.session_id == "s1"
assert "status = 'running'" in conn.calls[0][1]
async def test_active_without_a_run_is_none():
store, conn = _store()
conn.enqueue(None)
assert await store.active("s1") is None
async def test_close_only_touches_a_running_row():
store, conn = _store()
conn.enqueue("UPDATE 1") # the UPDATE
conn.enqueue(_row(status="done", report="готово")) # read back
run = await store.close("run-1", status="done", report="готово")
update = conn.calls[0]
assert "UPDATE protocol_runs" in update[1]
assert "status = 'running'" in update[1]
assert update[2] == ("run-1", "done", "готово", "")
assert run.status == "done"
assert run.report == "готово"
async def test_close_refuses_an_unknown_status():
store, _ = _store()
with pytest.raises(ValueError):
await store.close("run-1", status="pending")
async def test_list_maps_rows_newest_first():
store, conn = _store()
conn.enqueue([_row(id="a"), _row(id="b", status="cancelled")])
runs = await store.list("s1")
assert [r.id for r in runs] == ["a", "b"]
assert runs[1].status == "cancelled"
assert "ORDER BY started_at DESC" in conn.calls[0][1]
async def test_to_dict_is_json_ready():
store, conn = _store()
conn.enqueue(_row())
data = (await store.get("run-1")).to_dict()
assert data["steps"] == ["Раз", "Два"]
assert data["started_at"].startswith("2026-11-07T10:00")
assert data["finished_at"] is None