Files
MeshTerm/tests/test_link_quality.py
T
Jean-Pierre MartineauandClaude Opus 4.8 6405e7ec7b Add link-health regression detection against a rolling baseline
Splits a target's persisted trace history into a recent window and an older
baseline and flags end-to-end (bottleneck SNR, success rate) and per-hop link
SNR metrics that dropped beyond a threshold, with warn/critical severity.

Adds Repository.recent_traces / traced_targets, a pure link_quality service,
and a `link-health` tool that can analyze stored history or take fresh traces
first. Also tidies the repository import ordering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:57:31 -04:00

94 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for link-quality regression detection against a rolling baseline."""
from __future__ import annotations
from datetime import timedelta
from meshtools.core.models import Hop, TraceResult, utcnow
from meshtools.services import link_quality
_NOW = utcnow()
def _trace(snr: float, age_s: int, *, success: bool = True, hop: str = "3d") -> TraceResult:
"""Build a two-hop trace; bottleneck SNR is ``snr`` and it is ``age_s`` seconds old."""
return TraceResult(
target="Alice",
success=success,
hops=[Hop(0, hop, snr), Hop(1, None, snr + 1)] if success else [],
timestamp=_NOW - timedelta(seconds=age_s),
)
def test_detects_snr_regression() -> None:
"""A recent SNR drop past the threshold is flagged, with critical severity."""
# Baseline (older) ~+8 dB, recent (newer) ~+2 dB: a 6 dB drop.
baseline = [_trace(8.0, age_s=100 + i) for i in range(5)]
recent = [_trace(2.0, age_s=i) for i in range(3)]
report = link_quality.analyze_target("Alice", baseline + recent, recent_count=3)
assert report.status == "critical" # 6 dB drop ≥ 2× the 3 dB threshold
snr_reg = next(c for c in report.regressions if c.metric == "min SNR (dB)")
assert snr_reg.baseline == 8.0
assert snr_reg.recent == 2.0
assert snr_reg.delta == -6.0
# The per-hop link also regressed.
assert any(c.kind == "link" and c.regressed for c in report.regressions)
def test_healthy_link_reports_ok() -> None:
"""A steady link shows no regressions and an ok status."""
traces = [_trace(7.5, age_s=i) for i in range(8)]
report = link_quality.analyze_target("Alice", traces, recent_count=3)
assert report.status == "ok"
assert report.regressions == []
assert report.changes # metrics were still evaluated
def test_success_rate_regression() -> None:
"""A jump in recent failures is flagged even when surviving traces look fine."""
baseline = [_trace(7.0, age_s=100 + i) for i in range(5)]
recent = [_trace(7.0, age_s=i, success=False) for i in range(3)]
report = link_quality.analyze_target("Alice", baseline + recent, recent_count=3)
success_reg = next(c for c in report.regressions if c.metric == "success rate")
assert success_reg.baseline == 1.0
assert success_reg.recent == 0.0
assert report.status == "critical"
def test_insufficient_history() -> None:
"""Too little baseline data yields an insufficient-data verdict, not a false alarm."""
traces = [_trace(8.0, age_s=i) for i in range(3)] # only 3 total, < min baseline + recent
report = link_quality.analyze_target("Alice", traces, recent_count=3, min_baseline=3)
assert report.status == "insufficient-data"
assert report.changes == []
def test_links_only_compared_when_present_in_both_windows() -> None:
"""A path that appears in only one window is not reported as a link regression."""
# Baseline goes via hop 'aa'; recent via a different hop 'bb' — no shared link.
baseline = [_trace(8.0, age_s=100 + i, hop="aa") for i in range(5)]
recent = [_trace(8.0, age_s=i, hop="bb") for i in range(3)]
report = link_quality.analyze_target("Alice", baseline + recent, recent_count=3)
link_changes = [c for c in report.changes if c.kind == "link"]
# Only the shared 'X → us' return links overlap; the 'us → aa'/'us → bb' legs don't.
assert all("aa" not in c.subject or "bb" not in c.subject for c in link_changes)
assert report.status == "ok"
def test_resolver_names_link_subjects() -> None:
"""A resolver renames hop hashes in link subjects for readable output."""
baseline = [_trace(8.0, age_s=100 + i) for i in range(5)]
recent = [_trace(2.0, age_s=i) for i in range(3)]
report = link_quality.analyze_target(
"Alice",
baseline + recent,
recent_count=3,
resolve=lambda h: "Repeater" if h == "3d" else h,
device_label="Me",
)
link_regs = [c for c in report.regressions if c.kind == "link"]
assert any("Repeater" in c.subject for c in link_regs)
assert any("Me" in c.subject for c in link_regs)