correlator: use srcaddr for thin-call disambiguation instead of pure recency (#158)
The fast/thin path (short acks like "10-4", enabled to attempt linking at
all by 1ffff25) had no identity signal available -- transcript_too_short
skips GPT extraction entirely, so call_units is always empty for this
population. It fell back to "most recently updated incident on this
talkgroup", which silently misattaches a short ack to the wrong incident
whenever two are live on the same busy dispatch channel at once.
metadata_watcher.py already captures the P25 source radio ID (srcaddr) on
every call independent of transcript content, and it already reaches the
call doc (models.py, mqtt_handler.py:245) -- it was just never read by the
correlator. Thread it through _build_context, check it against the
srcaddrs already seen on each TG-matched incident before falling back to
recency, and accumulate it on the incident (_update_incident/_create_incident)
so later calls from the same radio can match.
Verified: 422 pass, 0 fail (4 new tests).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2e67d1bad6
commit
6c095083fc
@@ -180,6 +180,55 @@ def test_tactical_thin_call_is_ambiguous_with_two_candidates():
|
||||
assert decision["action"] == "orphan"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# server-26#158: srcaddr identity beats recency guesswork for thin calls
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_thin_call_srcaddr_match_resolves_tier2_ambiguity():
|
||||
"""
|
||||
Same fixture as test_tactical_thin_call_is_ambiguous_with_two_candidates —
|
||||
two candidates, tier-2 window, no unit ID parsed (transcript_too_short
|
||||
skipped GPT). Without srcaddr this orphans. With it, the radio that sent
|
||||
the call already touched inc-b, so that's the thread — not a guess.
|
||||
"""
|
||||
a = _incident(idle_minutes=3.0, incident_id="inc-a", srcaddrs=["9001"])
|
||||
b = _incident(idle_minutes=4.0, incident_id="inc-b", srcaddrs=["9002"])
|
||||
decision = _run_decision(_ctx(
|
||||
all_active=[a, b], recent=[a, b], talkgroup_name=TACTICAL_TG,
|
||||
call_srcaddr="9002",
|
||||
))
|
||||
assert decision["action"] == "link"
|
||||
assert decision["matched_incident"]["incident_id"] == "inc-b"
|
||||
assert decision["corr_debug"]["corr_fit_signal"] == "thin_srcaddr_match"
|
||||
|
||||
|
||||
def test_thin_call_srcaddr_match_overrides_recency_in_tier1():
|
||||
"""
|
||||
Both candidates are inside the 30s conversational window, where recency
|
||||
alone would pick inc-a (more recently updated) even though the radio that
|
||||
sent this call has only ever touched inc-b — the exact busy-channel,
|
||||
two-concurrent-incidents misattach server-26#158 was filed for.
|
||||
"""
|
||||
a = _incident(idle_minutes=0.1, incident_id="inc-a", srcaddrs=["9001"])
|
||||
b = _incident(idle_minutes=0.2, incident_id="inc-b", srcaddrs=["9002"])
|
||||
decision = _run_decision(_ctx(
|
||||
all_active=[a, b], recent=[a, b], call_srcaddr="9002",
|
||||
))
|
||||
assert decision["action"] == "link"
|
||||
assert decision["matched_incident"]["incident_id"] == "inc-b"
|
||||
|
||||
|
||||
def test_thin_call_with_no_srcaddr_match_falls_back_to_recency():
|
||||
"""A radio ID that matches nothing on this talkgroup behaves exactly as
|
||||
before — no regression for the ordinary case."""
|
||||
a = _incident(idle_minutes=0.1, incident_id="inc-a", srcaddrs=["9001"])
|
||||
decision = _run_decision(_ctx(
|
||||
all_active=[a], recent=[a], call_srcaddr="unrelated-radio",
|
||||
))
|
||||
assert decision["action"] == "link"
|
||||
assert decision["corr_debug"]["corr_fit_signal"] == "thin_recency"
|
||||
|
||||
|
||||
def test_call_with_unit_overlap_does_attach():
|
||||
"""
|
||||
Positive control: real evidence still links. Carrying units also means the
|
||||
|
||||
Reference in New Issue
Block a user