correlator: mirror the dispatch/tactical idle split in the escape hatch, record the gate-veto reason (#115)
Review of #126 found: (1) the escape hatch applied tg_dispatch_thin_idle_minutes (5 min) unconditionally, but incident_correlator's own fast/thin path only uses that on dispatch channels and 15 min on tactical ones via _is_dispatch_channel -- mirrored the same selection here, plus a config.py note flagging the second consumer. (2) the docstring claimed a 'confirmed explanation' for 2 window-3 gate misses that was actually wrong (self-contradictory in its own text); replaced the guess with corr_gate_veto, written into corr_debug on every llm=orphan/rules=new disagreement that escalates, so window #4 can see *why* each one escaped instead of reconstructing it from the raw dump. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tbknwttzou4s46PAykmtix
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
400b74b519
commit
598054746a
@@ -156,6 +156,7 @@ async def test_recent_incident_on_same_talkgroup_is_not_gated():
|
||||
ctx = {
|
||||
"system_id": "sys-1",
|
||||
"talkgroup_id": 9048,
|
||||
"talkgroup_name": "Dispatch",
|
||||
"now": NOW,
|
||||
"recent": [{
|
||||
"incident_id": "inc-live",
|
||||
@@ -175,14 +176,17 @@ async def test_recent_incident_on_same_talkgroup_is_not_gated():
|
||||
# as "recent", which on a busy dispatch channel (3-13 incidents/2h) was
|
||||
# satisfied almost unconditionally — the gate fired 0/24 times against its own
|
||||
# target shape. It now only counts an incident as recent within
|
||||
# settings.tg_dispatch_thin_idle_minutes (5 min) — the same bound the
|
||||
# fast/thin path uses for the "dispatch, thin ack" case this escape hatch is
|
||||
# actually for.
|
||||
# settings.tg_dispatch_thin_idle_minutes (5 min) on a dispatch channel, or
|
||||
# tg_thin_idle_minutes (15 min) on a tactical channel — the same split
|
||||
# incident_correlator's own fast/thin path uses, selected by the same
|
||||
# _is_dispatch_channel test, so a retune of one for fast/thin reasons doesn't
|
||||
# silently move this escape hatch on channels never re-measured for it.
|
||||
|
||||
async def test_recent_same_tg_incident_inside_new_short_window_still_escapes_gate():
|
||||
ctx = {
|
||||
"system_id": "sys-1",
|
||||
"talkgroup_id": 9048,
|
||||
"talkgroup_name": "Dispatch",
|
||||
"now": NOW,
|
||||
"recent": [{
|
||||
"incident_id": "inc-live",
|
||||
@@ -199,19 +203,23 @@ async def test_recent_same_tg_incident_inside_new_short_window_still_escapes_gat
|
||||
|
||||
|
||||
async def test_recent_same_tg_incident_older_than_short_window_now_gates():
|
||||
# Regression test for the fix: this incident is well outside the new
|
||||
# 5-minute window but still inside the OLD 2-hour correlation_window_hours
|
||||
# lookback — before the fix this escaped the gate (tiebreak called);
|
||||
# after the fix it no longer counts as "recent", so the gate fires.
|
||||
# Regression test for the fix: 8 minutes is past the 5-minute DISPATCH
|
||||
# bound but still inside the 15-minute TACTICAL bound and the OLD 2-hour
|
||||
# correlation_window_hours lookback — this specifically proves the
|
||||
# dispatch-channel number is being used here, not just "some window
|
||||
# shorter than 2h". Before the fix this escaped the gate on any channel;
|
||||
# after the fix a dispatch channel gates at this age (a tactical channel
|
||||
# would not — see test_tactical_channel_uses_the_longer_window below).
|
||||
ctx = {
|
||||
"system_id": "sys-1",
|
||||
"talkgroup_id": 9048,
|
||||
"talkgroup_name": "Dispatch",
|
||||
"now": NOW,
|
||||
"recent": [{
|
||||
"incident_id": "inc-stale",
|
||||
"system_ids": ["sys-1"],
|
||||
"talkgroup_ids": ["9048"],
|
||||
"updated_at": (NOW - timedelta(minutes=45)).isoformat(),
|
||||
"updated_at": (NOW - timedelta(minutes=8)).isoformat(),
|
||||
}],
|
||||
}
|
||||
m_apply, m_tiebreak = await _run_consensus(
|
||||
@@ -221,6 +229,55 @@ async def test_recent_same_tg_incident_older_than_short_window_now_gates():
|
||||
assert m_apply.call_args[0][0]["decision"]["action"] == "orphan"
|
||||
|
||||
|
||||
async def test_tactical_channel_uses_the_longer_window():
|
||||
# Same 8-minute age as the dispatch test above, but on a channel name that
|
||||
# does not match _DISPATCH_TG_RE — this must fall back to the 15-minute
|
||||
# tg_thin_idle_minutes bound, same as incident_correlator's own fast/thin
|
||||
# selection, and 8 min is still "recent" under that bound.
|
||||
ctx = {
|
||||
"system_id": "sys-1",
|
||||
"talkgroup_id": 383,
|
||||
"talkgroup_name": "Tac 3",
|
||||
"now": NOW,
|
||||
"recent": [{
|
||||
"incident_id": "inc-tac",
|
||||
"system_ids": ["sys-1"],
|
||||
"talkgroup_ids": ["383"],
|
||||
"updated_at": (NOW - timedelta(minutes=8)).isoformat(),
|
||||
}],
|
||||
}
|
||||
m_apply, m_tiebreak = await _run_consensus(
|
||||
_preview("new", {}, ctx=ctx), _llm("orphan"),
|
||||
)
|
||||
m_tiebreak.assert_called_once()
|
||||
|
||||
|
||||
async def test_gate_veto_reason_is_recorded_on_the_escalation_path():
|
||||
# server-26#115: a live measurement window must be able to see *why* an
|
||||
# llm=orphan/rules=new call escaped the gate without guessing from the raw
|
||||
# dump (which produced a wrong "confirmed explanation" for 2 window-#3
|
||||
# misses the first time). corr_gate_veto names the surviving condition.
|
||||
ctx = {"call_severity": "major"}
|
||||
m_apply, m_tiebreak = await _run_consensus(
|
||||
_preview("new", {}, ctx=ctx), _llm("orphan"),
|
||||
)
|
||||
m_tiebreak.assert_called_once()
|
||||
final = m_apply.call_args[0][0]["decision"]
|
||||
assert final["corr_debug"]["corr_gate_veto"] == "severity"
|
||||
|
||||
|
||||
async def test_gate_veto_reason_is_absent_when_the_disagreement_is_not_orphan_vs_new():
|
||||
# corr_gate_veto is only meaningful for the llm=orphan/rules=new shape the
|
||||
# gate targets — it must not appear (or be misleadingly None-vs-absent) on
|
||||
# an unrelated disagreement shape.
|
||||
m_apply, m_tiebreak = await _run_consensus(
|
||||
_preview("link", {}), _llm("orphan"),
|
||||
)
|
||||
m_tiebreak.assert_called_once()
|
||||
final = m_apply.call_args[0][0]["decision"]
|
||||
assert "corr_gate_veto" not in final["corr_debug"]
|
||||
|
||||
|
||||
async def test_recent_incident_on_a_different_talkgroup_still_gates():
|
||||
ctx = {
|
||||
"system_id": "sys-1",
|
||||
|
||||
Reference in New Issue
Block a user