Files
server-26/drb-c2-core/tests/test_dedup.py
Logan Cusano 97013e1505
Build & Deploy / Build & push images (push) Successful in 4m0s
Build & Deploy / Deploy to VM (push) Successful in 2m29s
Stop Whisper hallucinations and dedupe recordings across nodes
Two independent sources of garbage in the AI pipeline, both visible in the
2026-08-16 correlation dump.

1. Hallucinated transcripts. The Whisper prompt opened with an enumerated run
   of ten-codes: 10-4, 10-23, 10-20, 10-97 and so on. Whisper treats prompt
   text as preceding transcript, so on noisy or silent audio it continued the
   series, emitting transcripts that count upward from 10-4 to 10-99. The
   existing no_speech_prob filter could not catch these: the model is highly
   confident in text it invented by continuing a pattern.

   The prompt no longer contains a series to extend, and _is_degenerate()
   rejects the three shapes this failure takes: ascending ten-code runs, one
   phrase looping, and near-identical segments across a whole recording.
   Verified against 13 transcripts from production: all four known
   hallucinations rejected, all nine real ones kept, including terse traffic
   containing legitimate codes.

2. Duplicate recordings. node-002 and node-PI-2 both cover TG 9048 and both
   uploaded the same transmissions, ~1.1s apart. Nine pairs appeared in one
   dump. Each was transcribed, billed and correlated twice, and the resulting
   incident listed two units where there was one.

   Canonical selection is by earliest started_at, tie-broken on call_id, NOT
   by upload order: upload order varies with encode time and network latency,
   so it would make the authoritative recording non-deterministic. Call
   documents are created from MQTT call_start before uploads arrive, so both
   nodes independently reach the same verdict. The loser keeps its audio (it
   may be the cleaner capture) but is excluded from STT, correlation, the
   re-correlation sweep and the orphan debug view.

Also fixes _sync_transcribe returning a bare None when OPENAI_API_KEY is
missing, where the caller unpacks two values. A missing key surfaced as a
misleading "Transcription failed" instead of the real warning.

Adds tests/test_dedup.py (15 cases). dedup.py reaches Firestore through an
injected callable so it stays importable without firebase-admin present.
2026-08-16 17:28:27 -04:00

159 lines
5.1 KiB
Python

"""
Unit tests for cross-node duplicate detection.
Fixture timings come from real production data: node-002 and node-PI-2 both
recorded TG 9048 on 2026-08-16, starting ~1.1s apart.
"""
import pytest
from datetime import datetime, timezone, timedelta
from app.internal.dedup import _parse_dt, _is_canonical, find_duplicate_of
BASE = datetime(2026, 8, 16, 19, 31, 46, tzinfo=timezone.utc)
def _query_returning(*calls):
"""Stand-in for fstore.collection_where."""
async def _q(_collection, _conditions):
return list(calls)
return _q
def _query_raising(exc):
async def _q(_collection, _conditions):
raise exc
return _q
def _call(call_id, node_id, offset_seconds=0.0, talkgroup_id=9048, **extra):
return {
"call_id": call_id,
"node_id": node_id,
"system_id": "sys-1",
"talkgroup_id": talkgroup_id,
"started_at": BASE + timedelta(seconds=offset_seconds),
**extra,
}
# ---------------------------------------------------------------------------
# Timestamp parsing — Firestore returns three different shapes
# ---------------------------------------------------------------------------
def test_parse_dt_accepts_aware_datetime():
assert _parse_dt(BASE) == BASE
def test_parse_dt_assumes_utc_for_naive_datetime():
naive = datetime(2026, 8, 16, 19, 31, 46)
assert _parse_dt(naive) == BASE
def test_parse_dt_accepts_iso_string_with_z():
assert _parse_dt("2026-08-16T19:31:46Z") == BASE
def test_parse_dt_returns_none_for_junk():
assert _parse_dt("not a date") is None
assert _parse_dt(None) is None
# ---------------------------------------------------------------------------
# Canonical selection
# ---------------------------------------------------------------------------
def test_earlier_start_wins():
early = _call("a", "node-002", 0.0)
late = _call("b", "node-PI-2", 1.1)
assert _is_canonical(early, [late]) is True
assert _is_canonical(late, [early]) is False
def test_identical_starts_break_tie_on_call_id():
first = _call("aaa", "node-002", 0.0)
second = _call("bbb", "node-PI-2", 0.0)
assert _is_canonical(first, [second]) is True
assert _is_canonical(second, [first]) is False
def test_both_nodes_reach_the_same_verdict():
"""The whole point: the decision must not depend on upload order."""
a = _call("a", "node-002", 0.0)
b = _call("b", "node-PI-2", 1.1)
verdicts = [_is_canonical(a, [b]), _is_canonical(b, [a])]
assert verdicts.count(True) == 1, "exactly one recording must be canonical"
# ---------------------------------------------------------------------------
# find_duplicate_of
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_returns_canonical_id_for_later_recording():
canonical = _call("canon", "node-002", 0.0)
later = _call("later", "node-PI-2", 1.1)
q = _query_returning(canonical, later)
assert await find_duplicate_of(later, query=q) == "canon"
@pytest.mark.asyncio
async def test_returns_none_for_the_canonical_recording():
canonical = _call("canon", "node-002", 0.0)
later = _call("later", "node-PI-2", 1.1)
q = _query_returning(canonical, later)
assert await find_duplicate_of(canonical, query=q) is None
@pytest.mark.asyncio
async def test_same_node_is_never_a_duplicate():
"""Back-to-back transmissions from one node are real, separate calls."""
first = _call("a", "node-002", 0.0)
second = _call("b", "node-002", 2.0)
q = _query_returning(first, second)
assert await find_duplicate_of(second, query=q) is None
@pytest.mark.asyncio
async def test_different_talkgroup_is_not_a_duplicate():
other_tg = _call("a", "node-002", 0.0, talkgroup_id=9600)
mine = _call("b", "node-PI-2", 1.0, talkgroup_id=9048)
q = _query_returning(other_tg, mine)
assert await find_duplicate_of(mine, query=q) is None
@pytest.mark.asyncio
async def test_never_chains_onto_another_duplicate():
"""A third node must point at the original, not at a duplicate of it."""
canonical = _call("canon", "node-002", 0.0)
already_dupe = _call("dupe", "node-PI-2", 0.5, duplicate_of="canon")
third = _call("third", "node-003", 1.0)
q = _query_returning(canonical, already_dupe, third)
assert await find_duplicate_of(third, query=q) == "canon"
@pytest.mark.asyncio
async def test_no_match_returns_none():
lonely = _call("only", "node-002", 0.0)
q = _query_returning(lonely)
assert await find_duplicate_of(lonely, query=q) is None
@pytest.mark.asyncio
async def test_missing_identifiers_skip_the_check():
called = False
async def _q(_collection, _conditions):
nonlocal called
called = True
return []
incomplete = {"call_id": "x", "node_id": "node-002", "started_at": BASE}
assert await find_duplicate_of(incomplete, query=_q) is None
assert called is False, "must bail out before querying"
@pytest.mark.asyncio
async def test_query_failure_never_blocks_the_upload():
call = _call("a", "node-002", 0.0)
q = _query_raising(RuntimeError("firestore down"))
assert await find_duplicate_of(call, query=q) is None