mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-18 21:00:22 -05:00
fix(engine): wire node positions into StreamingEngine's governed fuser (#1880)
StreamingEngine owns its own MultistaticFuser instance -- a different one from wifi-densepose-sensing-server's AppState, which was fixed for this same class of bug in #1860. Nothing ever called set_node_positions / set_node_positions_by_id on this instance, so every FusedSensingFrame produced by the governed path (fuse_scored_calibrated, which feeds provenance and the witness) carried every node's position as the origin regardless of deployment configuration. Adds StreamingEngine::set_node_positions(HashMap<u8, [f32;3]>), delegating to the fuser's existing set_node_positions_by_id. Also fixes a related hazard in set_multistatic_config: it rebuilt the fuser wholesale (MultistaticFuser::with_config(cfg)), which silently discarded any positions configured before it. It now preserves them across the rebuild, so callers don't need to remember a specific ordering between the two setters. Two regression tests per the issue's own acceptance criteria: one calling fuse_scored_calibrated directly and asserting each fused contribution carries its own node's configured position, and one asserting positions survive a later set_multistatic_config call. Fixes #1870.
This commit is contained in:
1 file changed
+82
-1
@@ -234,9 +234,30 @@ impl StreamingEngine {
|
||||
/// WiFi/ESP-NOW-synced multi-node deployments spuriously fail governed
|
||||
/// trust cycles even after widening the guard elsewhere.
|
||||
///
|
||||
/// Rebuilds the fuser, so call before any frames are processed.
|
||||
/// Rebuilds the fuser, so call before any frames are processed. Preserves
|
||||
/// any node positions configured via [`Self::set_node_positions`] across
|
||||
/// the rebuild (issue #1870) — a caller that configures positions once at
|
||||
/// startup and calls this later (e.g. after deriving a TDM schedule) must
|
||||
/// not have that configuration silently discarded.
|
||||
pub fn set_multistatic_config(&mut self, cfg: MultistaticConfig) {
|
||||
let positions = self.fuser.node_positions_by_id().clone();
|
||||
self.fuser = MultistaticFuser::with_config(cfg);
|
||||
self.fuser.set_node_positions_by_id(positions);
|
||||
}
|
||||
|
||||
/// Configure node positions for the governed multistatic fuser, keyed by
|
||||
/// node id (issue #1870). Without this, `StreamingEngine`'s fuser — the
|
||||
/// one behind `fuse_scored_calibrated`, which feeds provenance and the
|
||||
/// witness — always fuses with every node at the origin, regardless of
|
||||
/// deployment configuration, because nothing ever called this on it. This
|
||||
/// is a separate `MultistaticFuser` instance from the one
|
||||
/// `wifi-densepose-sensing-server`'s `AppState` owns (fixed for that
|
||||
/// instance in #1860); each fuser instance needs positions set on it
|
||||
/// independently. Safe to call before or after [`Self::set_multistatic_config`] —
|
||||
/// order does not matter, since that method now preserves whatever
|
||||
/// positions are already set across its rebuild.
|
||||
pub fn set_node_positions(&mut self, positions: std::collections::HashMap<u8, [f32; 3]>) {
|
||||
self.fuser.set_node_positions_by_id(positions);
|
||||
}
|
||||
|
||||
/// Activate a per-room calibration adapter (ADR-150 §3.4). From the next
|
||||
@@ -1359,4 +1380,64 @@ mod tests {
|
||||
"an extra evidence ref must not collide with a model_version that absorbs it"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Issue #1870: StreamingEngine's own fuser never received positions ----
|
||||
|
||||
/// The governed path (`fuse_scored_calibrated`, feeding provenance and the
|
||||
/// witness) must carry each node's configured position -- not the origin
|
||||
/// for every node, which is what happened before `set_node_positions`
|
||||
/// existed on `StreamingEngine` and nothing ever configured this fuser
|
||||
/// instance (distinct from the one `wifi-densepose-sensing-server`'s
|
||||
/// `AppState` owns, fixed separately in #1860).
|
||||
#[test]
|
||||
fn governed_fuse_carries_each_nodes_configured_position() {
|
||||
let (mut e, _room) = engine();
|
||||
let mut positions = std::collections::HashMap::new();
|
||||
positions.insert(0u8, [1.0, 2.0, 3.0]);
|
||||
positions.insert(1u8, [4.0, 5.0, 6.0]);
|
||||
e.set_node_positions(positions.clone());
|
||||
|
||||
let frames = [node_frame(0, 1000, 56), node_frame(1, 1001, 56)];
|
||||
let (fused, _score) = e
|
||||
.fuser
|
||||
.fuse_scored_calibrated(&frames, &[None, None], StreamingEngine::DEFAULT_COHERENCE_ACCEPT)
|
||||
.expect("cohort fuses");
|
||||
|
||||
assert_eq!(fused.node_positions.len(), frames.len());
|
||||
for (frame, position) in fused.node_frames.iter().zip(&fused.node_positions) {
|
||||
let expected = positions
|
||||
.get(&frame.node_id)
|
||||
.copied()
|
||||
.expect("every configured node has a position in this test");
|
||||
assert_eq!(
|
||||
*position, expected,
|
||||
"node {} received the wrong position on the governed path",
|
||||
frame.node_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `set_multistatic_config` used to rebuild the fuser from scratch,
|
||||
/// silently discarding any positions configured before it. A caller that
|
||||
/// sets positions once at startup and derives/applies a TDM-schedule
|
||||
/// config later (a real, documented ordering per #1049/#1057) must not
|
||||
/// lose them.
|
||||
#[test]
|
||||
fn positions_survive_a_later_set_multistatic_config() {
|
||||
let (mut e, _room) = engine();
|
||||
let mut positions = std::collections::HashMap::new();
|
||||
positions.insert(0u8, [7.0, 8.0, 9.0]);
|
||||
e.set_node_positions(positions.clone());
|
||||
|
||||
e.set_multistatic_config(MultistaticConfig {
|
||||
guard_interval_us: 30_000,
|
||||
..MultistaticConfig::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
e.fuser.node_position(0),
|
||||
Some([7.0, 8.0, 9.0]),
|
||||
"set_multistatic_config must not discard previously-configured positions"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user