From a07deb91803d7500600ff8c0726a7ec9429e95ee Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 23 May 2026 14:36:54 -0400 Subject: [PATCH] test+refactor(adr-110): NodeState::sync_snapshot + 3 helper tests, dedupe 4 call sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iter 30 — defends the iter 29 REST endpoints + iter 23 WebSocket broadcast with tests, AND deduplicates the four call sites that all built the same NodeSyncSnapshot inline. Refactor: Add `NodeState::sync_snapshot() -> Option` as the single source of truth. All four call sites simplified: 1. node_sync_endpoint (REST /api/v1/nodes/:id/sync) — 12 → 5 lines 2. mesh_endpoint (REST /api/v1/mesh) — 11 → 3 lines 3. WebSocket vitals-only NodeInfo (line 4284) — 9 → 1 line 4. WebSocket CSI-frame NodeInfo (line 4617) — 9 → 1 line Net: -35 lines, single point of contact for any future schema change. Tests (3 new, all green; brings binary suite to 95+): fresh_node_with_no_sync_returns_none Mirrors REST 404 "no_sync" + WebSocket sync omission paths. node_with_latest_sync_produces_correct_snapshot Mirrors REST 200 OK + WebSocket sync field paths. Asserts §A0.10's measured 1_163_565 µs offset survives the helper. snapshot_reflects_leader_state Leader-case shape: is_leader=true, offset≈0 (–7 µs call-stack). These tests cover BOTH REST routes and BOTH WebSocket NodeInfo sites through the shared helper — one test per behavioral path, no axum state plumbing required. cargo check -p ...sensing-server → green. Co-Authored-By: claude-flow --- .../wifi-densepose-sensing-server/src/main.rs | 132 ++++++++++++------ 1 file changed, 92 insertions(+), 40 deletions(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 6cc2a4fd..b4af1348 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -553,6 +553,25 @@ impl NodeState { /// `udp_receiver_task`. Uses `last_frame_time` as the previous-frame /// anchor; the first frame after init seeds the timer without producing /// a sample (no prior dt to measure). + /// ADR-110 iter 30 — pure snapshot of this node's mesh-sync state. + /// Returns `None` when no sync packet has been observed. Used by both + /// the WebSocket broadcaster (iter 23) and the REST handlers (iter 29); + /// extracted here so tests can build a `NodeState`, populate + /// `latest_sync`, and assert the snapshot shape without spinning up + /// the axum router. + pub(crate) fn sync_snapshot(&self) -> Option { + let sync = self.latest_sync.as_ref()?; + Some(NodeSyncSnapshot { + offset_us: sync.local_minus_epoch_us(), + is_leader: sync.flags.is_leader, + is_valid: sync.flags.is_valid, + smoothed: sync.flags.smoothed_used, + sequence: sync.sequence, + csi_fps_ema: self.csi_fps_ema, + csi_fps_samples: self.csi_fps_samples, + }) + } + pub(crate) fn observe_csi_frame_arrival(&mut self, now: std::time::Instant) { if let Some(prev) = self.last_frame_time { let dt = now.duration_since(prev).as_secs_f64(); @@ -4075,21 +4094,12 @@ async fn node_sync_endpoint( "error": "unknown_node", "node_id": id, }))) })?; - let sync = ns.latest_sync.as_ref().ok_or_else(|| { + ns.sync_snapshot().map(Json).ok_or_else(|| { (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "no_sync", "node_id": id, "hint": "node hasn't emitted a sync packet yet (no mesh peer or not v0.6.9+)", }))) - })?; - Ok(Json(NodeSyncSnapshot { - offset_us: sync.local_minus_epoch_us(), - is_leader: sync.flags.is_leader, - is_valid: sync.flags.is_valid, - smoothed: sync.flags.smoothed_used, - sequence: sync.sequence, - csi_fps_ema: ns.csi_fps_ema, - csi_fps_samples: ns.csi_fps_samples, - })) + }) } /// ADR-110 iter 29 — fleet-wide mesh state via HTTP. @@ -4102,16 +4112,7 @@ async fn mesh_endpoint(State(state): State) -> Json SyncPacket { + SyncPacket { + node_id, + proto_ver: 1, + flags: SyncPacketFlags { is_leader: false, is_valid: true, smoothed_used: true }, + local_us: 28_798_450, + epoch_us: 27_634_885, + sequence: 20, + } + } + + #[test] + fn fresh_node_with_no_sync_returns_none() { + // Mirrors the REST 404 "no_sync" branch. + let ns = NodeState::new(); + assert!(ns.sync_snapshot().is_none()); + } + + #[test] + fn node_with_latest_sync_produces_correct_snapshot() { + // Mirrors the REST 200 OK branch + the WebSocket sync field. + let mut ns = NodeState::new(); + ns.latest_sync = Some(populated_sync(9)); + ns.latest_sync_at = Some(std::time::Instant::now()); + // Pretend the fps EMA has settled (iter 18 5-sample warmup). + ns.csi_fps_ema = 10.5; + ns.csi_fps_samples = 42; + + let snap = ns.sync_snapshot().expect("populated state must produce a snapshot"); + assert_eq!(snap.offset_us, 1_163_565); // §A0.10 measured boot delta + assert!(!snap.is_leader); + assert!(snap.is_valid); + assert!(snap.smoothed); + assert_eq!(snap.sequence, 20); + assert!((snap.csi_fps_ema - 10.5).abs() < 1e-9); + assert_eq!(snap.csi_fps_samples, 42); + } + + #[test] + fn snapshot_reflects_leader_state() { + // Same data shape that /api/v1/mesh emits for a leader node. + let mut ns = NodeState::new(); + let mut s = populated_sync(12); + s.flags = SyncPacketFlags { is_leader: true, is_valid: true, smoothed_used: false }; + s.local_us = 28_864_932; + s.epoch_us = 28_864_939; // -7 µs delta on the leader + ns.latest_sync = Some(s); + ns.latest_sync_at = Some(std::time::Instant::now()); + + let snap = ns.sync_snapshot().unwrap(); + assert!(snap.is_leader); + assert_eq!(snap.offset_us, -7); // call-stack µs only + assert!(!snap.smoothed); + } +} + #[cfg(test)] mod novelty_tests { use super::*;