Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/routes/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ async def association_status():
return {
"registered_nodes": len(_a.node_geometries),
"overlap_zones": len(_a.overlap_zones),
# Node pairs that got no grid because the two nodes are in different
# worlds (node_world above). Read next to overlap_zones: on a fleet
# of 50 synthetic nodes over the same city as 8 receivers it is the
# 400 sim/real pairs whose grids could only ever have paired a
# simulated echo with a real one. Counted per pair considered, so it
# keeps rising as nodes re-register — zero means the fleet is single-
# world (or untagged), not that the gate is off.
"assoc_world_skipped_pairs": getattr(_a, "assoc_world_skipped_pairs", 0),
# Confirmed single-node tracks each node last submitted; these are what
# pairings are drawn from.
"pending_tracks": {nid: len(tracks) for nid, tracks in list(_a._pending_tracks.items())},
Expand Down
29 changes: 29 additions & 0 deletions backend/tests/test_adsb_seed_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,3 +605,32 @@ def test_associator_gets_the_state_world_resolver(self):
must consult the same resolver claiming and the auto-tag filter use,
or one consumer accepts what another rejects."""
assert state.node_associator.node_world_provider is state.node_world

def test_a_sim_and_a_real_node_over_one_footprint_get_no_overlap_zone(self):
"""The same resolver, one level down: bottom-up pairing must not build
a grid across worlds either. Registering a synthetic node and a
hardware node on overlapping coverage used to leave a zone whose only
possible pairing was a simulated echo against a real one — which is how
real node ids reached the synthetic fleet's dark solves."""
_a = state.node_associator
try:
_a.register_node("synth-GVL-9001", dict(_NODE_CFG))
_a.register_node("hw-9001", dict(_NODE_CFG, rx_lat=34.86, rx_lon=-82.36))

assert _a.overlap_zones == {}
assert _a._neighbors.get("synth-GVL-9001", set()) == set()
assert _a.assoc_world_skipped_pairs == 1
finally:
state._reset_for_tests()

def test_two_synthetic_nodes_over_one_footprint_still_pair(self):
"""The gate is the world difference, not the registration."""
_a = state.node_associator
try:
_a.register_node("synth-GVL-9001", dict(_NODE_CFG))
_a.register_node("synth-GVL-9002", dict(_NODE_CFG, rx_lat=34.86, rx_lon=-82.36))

assert _a.overlap_zones
assert _a.assoc_world_skipped_pairs == 0
finally:
state._reset_for_tests()
12 changes: 12 additions & 0 deletions backend/tests/test_analytics_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ def test_status_returns_expected_fields(self, client):
assert "overlap_zones" in body
assert "overlaps" in body

def test_status_reports_world_skipped_pairs(self, client):
"""The world gate on overlap zones is otherwise invisible: a fleet
whose sim/real pairs are being refused looks exactly like a fleet whose
pairs never overlapped, and only this counter separates them."""
_a = state.node_associator
_a.assoc_world_skipped_pairs += 7
try:
body = client.get("/api/radar/association/status").json()
assert body["assoc_world_skipped_pairs"] == 7
finally:
state._reset_for_tests()

def test_status_includes_claiming_block(self, client):
"""Top-down claiming (ASSOC_CLAIM_MODE) since boot — off by default
in tests, so this pins the shape rather than any particular mode."""
Expand Down
Loading