Newer
Older
navi-1 / tests / unit / skills / test_protocol_runs.py
"""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