Stop a back-dated call from silently disabling every recency gate (server-26#74)
_call_fits_incident measured incident idle with the signed helper while every other recency gate in the file uses the unsigned one. On the re-correlation sweep, `now` is the call's own started_at, which can precede the incident's last activity, so the value went negative. Negative idle made `idle_min >= 15` false, which meant the content-divergence veto never ran and unit overlap was accepted unconditionally -- on a shared dispatch backbone that is the feedback loop that lets one incident absorb a whole talkgroup. It also made `idle_min < 20.0` true at any back-dating, so a tactical channel returned tactical_default for every swept orphan out to the 90-minute bound. One variable feeds all four gates in the function, so this is a one-line change at the source. The signed value is untouched where it belongs: callers still compute corr_incident_idle_min themselves, so debug output keeps its meaning. Direction is toward more splitting, on the sweep path only, which is the point -- the bug was suppressing an over-merge veto. Forward-dated calls and anything inside the thresholds behave exactly as before. Two tests added alongside the existing idle-gate cases; both fail on the old line and pass on the new one. 266 pass, 0 fail. Refs server-26#74, #5, #80. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
187b8c1500
commit
e30d594eea
@@ -1598,7 +1598,15 @@ def _call_fits_incident(
|
|||||||
Thin calls (no units/vehicles/coords) never reach this function —
|
Thin calls (no units/vehicles/coords) never reach this function —
|
||||||
they are intercepted before it in correlate_call.
|
they are intercepted before it in correlate_call.
|
||||||
"""
|
"""
|
||||||
idle_min = _incident_idle_minutes(inc, now) if now is not None else 9999.0
|
# Gate comparisons in this function must use the unsigned distance, not the
|
||||||
|
# signed value: the re-correlation sweep anchors `now` to the call's own
|
||||||
|
# started_at, which can be earlier than the incident's last activity and
|
||||||
|
# send the signed value negative — silently defeating every `idle_min`
|
||||||
|
# gate below (content-divergence veto and the tactical default alike).
|
||||||
|
# See `_idle_gate_minutes` docstring. The signed value is reported to
|
||||||
|
# callers separately as `corr_incident_idle_min` (they compute it via
|
||||||
|
# `_incident_idle_minutes` themselves) — nothing here needs it.
|
||||||
|
idle_min = _idle_gate_minutes(inc, now) if now is not None else 9999.0
|
||||||
inc_id = inc.get("incident_id", "?")
|
inc_id = inc.get("incident_id", "?")
|
||||||
|
|
||||||
# ── 1. Unit overlap ───────────────────────────────────────────────────────
|
# ── 1. Unit overlap ───────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ Three defects combined to produce it, and each has cases below:
|
|||||||
import pytest
|
import pytest
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
import app.internal.incident_correlator as correlator_mod
|
||||||
from app.internal.incident_correlator import (
|
from app.internal.incident_correlator import (
|
||||||
_run_decision, _is_thin_call, _idle_gate_minutes,
|
_run_decision, _is_thin_call, _idle_gate_minutes,
|
||||||
_incident_at_capacity, _incident_span_minutes,
|
_incident_at_capacity, _incident_span_minutes, _call_fits_incident,
|
||||||
)
|
)
|
||||||
|
|
||||||
NOW = datetime(2026, 8, 20, 7, 0, 0, tzinfo=timezone.utc)
|
NOW = datetime(2026, 8, 20, 7, 0, 0, tzinfo=timezone.utc)
|
||||||
@@ -221,6 +222,49 @@ def test_back_dated_thin_call_does_not_sail_through_the_recency_gate():
|
|||||||
assert _run_decision(_ctx(all_active=[future], recent=[future]))["action"] == "orphan"
|
assert _run_decision(_ctx(all_active=[future], recent=[future]))["action"] == "orphan"
|
||||||
|
|
||||||
|
|
||||||
|
def test_back_dated_call_does_not_bypass_the_content_divergence_veto(monkeypatch):
|
||||||
|
"""
|
||||||
|
Same `9d376ffe` failure mode, but exercised directly against
|
||||||
|
`_call_fits_incident` on a dispatch channel: unit overlap plus a
|
||||||
|
back-dated call (incident updated 45 minutes AFTER the call's own
|
||||||
|
`started_at`, which the sweep passes as `now`) used to make the signed
|
||||||
|
idle -45, so `idle_min >= 15` read False and the content-divergence
|
||||||
|
veto never ran — unit overlap alone forced the merge regardless of
|
||||||
|
what the call was actually about. With the gate fixed to compare
|
||||||
|
distance, idle_min is 45 (>= 15), the veto runs, and a divergent
|
||||||
|
embedding (patched below so the assertion doesn't depend on numpy
|
||||||
|
being installed in this environment) fails it.
|
||||||
|
"""
|
||||||
|
monkeypatch.setattr(correlator_mod, "_cosine_similarity", lambda a, b: 0.0)
|
||||||
|
inc = _incident(idle_minutes=-45, units=["6-Adam"])
|
||||||
|
inc["embedding"] = [1.0, 0.0]
|
||||||
|
fits, signal = _call_fits_incident(
|
||||||
|
inc, call_units=["6-Adam"], call_vehicles=[], call_coords=None,
|
||||||
|
proximity_km=settings.location_proximity_km, is_dispatch=True,
|
||||||
|
call_embedding=[0.0, 1.0], now=NOW,
|
||||||
|
)
|
||||||
|
assert (fits, signal) == (False, "content_divergence")
|
||||||
|
|
||||||
|
|
||||||
|
def test_back_dated_call_on_tactical_channel_does_not_get_the_default():
|
||||||
|
"""
|
||||||
|
Tactical-channel counterpart: no unit/vehicle/location signal, so the
|
||||||
|
function falls through to step 4's `idle_min < 20.0` default. A
|
||||||
|
back-dated call (incident updated 45 minutes after the call's own
|
||||||
|
started_at) used to read idle_min as -45, which is always < 20.0, so
|
||||||
|
`tactical_default` fired unconditionally no matter how stale the
|
||||||
|
incident actually was relative to this call. Fixed, idle_min is the
|
||||||
|
45-minute distance, which is not < 20.0.
|
||||||
|
"""
|
||||||
|
inc = _incident(idle_minutes=-45)
|
||||||
|
fits, signal = _call_fits_incident(
|
||||||
|
inc, call_units=[], call_vehicles=[], call_coords=None,
|
||||||
|
proximity_km=settings.location_proximity_km, is_dispatch=False,
|
||||||
|
call_embedding=None, now=NOW,
|
||||||
|
)
|
||||||
|
assert (fits, signal) == (False, "tactical_idle")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 4. Hard caps — path-independent, because pairwise fit tests can't see shape
|
# 4. Hard caps — path-independent, because pairwise fit tests can't see shape
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user