Add Repository unit tests: hydration fidelity, ordering, limits, edge cases

19 tests covering the persistence layer: field round-trip for successful
and failed traces (hops, RTT, tx_power, timestamp), recent_traces
ordering/limit/target filtering, all_traces cross-target span,
traced_targets count-ordering, latest_trace success_only variants,
and full run lifecycle (start → finish, error without summary, limit,
ordering).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 4.6 committed 2026-06-21 19:09:28 -04:00
1 parent 8d2320ba03
commit fa585b333d
1 file changed
+260
+260
View File
@@ -0,0 +1,260 @@
"""Tests for the Repository persistence layer."""
from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
import pytest
from meshtools.core.models import Hop, TraceResult
from meshtools.persistence.repository import Repository
@pytest.fixture()
def repo(tmp_path: Path) -> Repository:
r = Repository(tmp_path / "test.db")
yield r
r.close()
def _make_trace(
target: str,
*hops: tuple[str | None, float],
success: bool = True,
rtt: float | None = 42.0,
tx: int | None = 11,
ts: datetime | None = None,
) -> TraceResult:
"""Build a trace with full control over fields that matter for persistence."""
return TraceResult(
target=target,
success=success,
hops=[Hop(i, node, snr) for i, (node, snr) in enumerate(hops)],
round_trip_ms=rtt,
tx_power=tx,
timestamp=ts or datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc),
)
# -- record + hydrate round-trip -------------------------------------------
def test_record_and_hydrate_preserves_all_fields(repo: Repository) -> None:
"""Every TraceResult field survives a write→read cycle."""
ts = datetime(2026, 6, 1, 8, 30, 0, tzinfo=timezone.utc)
trace = _make_trace("Alice", ("relay", 3.5), ("Alice", -2.0), (None, 1.0),
rtt=88.5, tx=7, ts=ts)
run = repo.start_run("trace", {})
repo.record_trace(run, trace)
got = repo.recent_traces("Alice")[0]
assert got.target == "Alice"
assert got.success is True
assert got.round_trip_ms == 88.5
assert got.tx_power == 7
assert got.timestamp == ts
assert [(h.index, h.node, h.snr) for h in got.hops] == [
(0, "relay", 3.5),
(1, "Alice", -2.0),
(2, None, 1.0),
]
def test_failed_trace_round_trips(repo: Repository) -> None:
"""A failed trace (no hops, no RTT) persists and reads back correctly."""
trace = _make_trace("Bob", success=False, rtt=None, tx=None)
run = repo.start_run("trace", {})
repo.record_trace(run, trace)
got = repo.recent_traces("Bob")[0]
assert got.success is False
assert got.hops == []
assert got.round_trip_ms is None
assert got.tx_power is None
# -- recent_traces ---------------------------------------------------------
def test_recent_traces_returns_newest_first(repo: Repository) -> None:
"""Traces come back in reverse insertion order."""
run = repo.start_run("trace", {})
for i in range(5):
repo.record_trace(run, _make_trace("X", ("hop", float(i))))
traces = repo.recent_traces("X")
snrs = [t.hops[0].snr for t in traces]
assert snrs == [4.0, 3.0, 2.0, 1.0, 0.0]
def test_recent_traces_respects_limit(repo: Repository) -> None:
"""The limit parameter caps the number of returned traces."""
run = repo.start_run("trace", {})
for _ in range(10):
repo.record_trace(run, _make_trace("X", ("h", 1.0)))
assert len(repo.recent_traces("X", limit=3)) == 3
def test_recent_traces_filters_by_target(repo: Repository) -> None:
"""Only traces to the requested target are returned."""
run = repo.start_run("trace", {})
repo.record_trace(run, _make_trace("Alice", ("r", 1.0)))
repo.record_trace(run, _make_trace("Bob", ("r", 2.0)))
repo.record_trace(run, _make_trace("Alice", ("r", 3.0)))
got = repo.recent_traces("Alice")
assert len(got) == 2
assert all(t.target == "Alice" for t in got)
def test_recent_traces_includes_failures(repo: Repository) -> None:
"""Both successful and failed traces are returned (failures are informative)."""
run = repo.start_run("trace", {})
repo.record_trace(run, _make_trace("X", ("h", 1.0), success=True))
repo.record_trace(run, _make_trace("X", success=False))
got = repo.recent_traces("X")
assert len(got) == 2
statuses = {t.success for t in got}
assert statuses == {True, False}
def test_recent_traces_empty_target(repo: Repository) -> None:
"""Querying a target with no stored traces returns an empty list."""
assert repo.recent_traces("NoSuchNode") == []
# -- all_traces ------------------------------------------------------------
def test_all_traces_spans_targets(repo: Repository) -> None:
"""all_traces returns traces across every target."""
run = repo.start_run("trace", {})
repo.record_trace(run, _make_trace("Alice", ("r", 1.0)))
repo.record_trace(run, _make_trace("Bob", ("r", 2.0)))
repo.record_trace(run, _make_trace("Alice", ("r", 3.0)))
got = repo.all_traces()
assert len(got) == 3
targets = {t.target for t in got}
assert targets == {"Alice", "Bob"}
def test_all_traces_newest_first(repo: Repository) -> None:
"""all_traces returns traces in reverse insertion order."""
run = repo.start_run("trace", {})
repo.record_trace(run, _make_trace("A", ("h", 1.0)))
repo.record_trace(run, _make_trace("B", ("h", 2.0)))
repo.record_trace(run, _make_trace("A", ("h", 3.0)))
targets = [t.target for t in repo.all_traces()]
assert targets == ["A", "B", "A"]
def test_all_traces_respects_limit(repo: Repository) -> None:
"""The limit parameter caps the cross-target result."""
run = repo.start_run("trace", {})
for _ in range(10):
repo.record_trace(run, _make_trace("X", ("h", 1.0)))
assert len(repo.all_traces(limit=4)) == 4
def test_all_traces_empty_db(repo: Repository) -> None:
"""An empty database returns an empty list, not an error."""
assert repo.all_traces() == []
# -- traced_targets --------------------------------------------------------
def test_traced_targets_ordered_by_count(repo: Repository) -> None:
"""Targets are returned most-traced first."""
run = repo.start_run("trace", {})
for _ in range(5):
repo.record_trace(run, _make_trace("Alice", ("r", 1.0)))
for _ in range(2):
repo.record_trace(run, _make_trace("Bob", ("r", 1.0)))
for _ in range(8):
repo.record_trace(run, _make_trace("Carol", ("r", 1.0)))
assert repo.traced_targets() == ["Carol", "Alice", "Bob"]
def test_traced_targets_empty_db(repo: Repository) -> None:
"""No traces → empty list."""
assert repo.traced_targets() == []
# -- latest_trace edge cases -----------------------------------------------
def test_latest_trace_success_only_skips_failures(repo: Repository) -> None:
"""success_only=True skips failed traces, even if they're newer."""
run = repo.start_run("trace", {})
repo.record_trace(run, _make_trace("X", ("h", 1.0), success=True))
repo.record_trace(run, _make_trace("X", success=False))
got = repo.latest_trace("X", success_only=True)
assert got is not None
assert got.success is True
def test_latest_trace_success_only_false_includes_failures(repo: Repository) -> None:
"""success_only=False returns the newest trace regardless of outcome."""
run = repo.start_run("trace", {})
repo.record_trace(run, _make_trace("X", ("h", 1.0), success=True))
repo.record_trace(run, _make_trace("X", success=False))
got = repo.latest_trace("X", success_only=False)
assert got is not None
assert got.success is False
# -- runs ------------------------------------------------------------------
def test_run_lifecycle(repo: Repository) -> None:
"""A run transitions from running → ok with a summary."""
run_id = repo.start_run("route-map", {"target": "Alice"}, profile="field")
runs = repo.list_runs()
assert runs[0].status == "running"
assert runs[0].tool == "route-map"
assert runs[0].profile == "field"
assert runs[0].finished_at is None
repo.finish_run(run_id, "ok", {"stability": 0.85})
runs = repo.list_runs()
assert runs[0].status == "ok"
assert runs[0].finished_at is not None
assert runs[0].summary == {"stability": 0.85}
def test_finish_run_error_without_summary(repo: Repository) -> None:
"""An errored run stores status without a summary."""
run_id = repo.start_run("trace", {})
repo.finish_run(run_id, "error")
runs = repo.list_runs()
assert runs[0].status == "error"
assert runs[0].summary is None
def test_list_runs_respects_limit(repo: Repository) -> None:
"""list_runs caps results at the requested limit."""
for i in range(10):
repo.start_run("trace", {"i": i})
assert len(repo.list_runs(limit=3)) == 3
def test_list_runs_newest_first(repo: Repository) -> None:
"""Runs come back in reverse insertion order."""
repo.start_run("first", {})
repo.start_run("second", {})
repo.start_run("third", {})
tools = [r.tool for r in repo.list_runs()]
assert tools == ["third", "second", "first"]