test(adr-110): lock NodeSyncSnapshot JSON wire contract (iter 24)
Iter 24 — ultra-opt for public-API stability. Iter 23 added a new JSON
field that UI clients (viz.html, future Tauri desktop, automation) now
depend on; this iter locks its exact shape so any future rename /
removal fails a named test instead of silently breaking consumers.
New module `node_sync_snapshot_serialization_tests` (3 tests, all green):
* sync_present_serializes_all_seven_fields
Builds NodeInfo with Some(sample_sync), serializes to serde_json::Value,
asserts all 7 documented field names exist (offset_us, is_leader,
is_valid, smoothed, sequence, csi_fps_ema, csi_fps_samples) and
spot-checks numeric values.
* sync_absent_omits_the_key_entirely
Builds NodeInfo with sync = None, asserts the `sync` JSON key is
DROPPED entirely (not emitted as `"sync": null`). This is the
backwards-compat contract that lets pre-iter-23 UI clients ignore
mesh-aware nodes silently.
* sync_round_trips_through_serde
to_string / from_str round-trip on a populated NodeInfo recovers
every field of the sync sub-object byte-for-byte (modulo float tol).
Test infrastructure: pure pure serde_json — no network, no fixtures,
no I/O. Adds 92 lines, 0 runtime allocs in the steady path.
Branch-coord clean (no Cargo.toml or cli.rs touched).
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
41f28ae85e
commit
e764504dc5
|
|
@ -5703,6 +5703,85 @@ async fn main() {
|
|||
info!("Server shut down cleanly");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod node_sync_snapshot_serialization_tests {
|
||||
//! ADR-110 iter 24 — JSON public-API contract for the iter 23
|
||||
//! NodeSyncSnapshot field. Any future rename / removal here must be
|
||||
//! intentional and update both Rust + UI/automation consumers.
|
||||
|
||||
use super::*;
|
||||
|
||||
fn sample_sync() -> NodeSyncSnapshot {
|
||||
NodeSyncSnapshot {
|
||||
offset_us: 1_163_565,
|
||||
is_leader: false,
|
||||
is_valid: true,
|
||||
smoothed: true,
|
||||
sequence: 20,
|
||||
csi_fps_ema: 10.0,
|
||||
csi_fps_samples: 47,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_node(sync: Option<NodeSyncSnapshot>) -> NodeInfo {
|
||||
NodeInfo {
|
||||
node_id: 9,
|
||||
rssi_dbm: -38.0,
|
||||
position: [2.0, 0.0, 1.5],
|
||||
amplitude: vec![],
|
||||
subcarrier_count: 0,
|
||||
sync,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_present_serializes_all_seven_fields() {
|
||||
let v = serde_json::to_value(sample_node(Some(sample_sync()))).unwrap();
|
||||
let s = v.get("sync").expect("sync key must be present");
|
||||
// All seven contract fields named exactly as iter 23 documented.
|
||||
for key in ["offset_us", "is_leader", "is_valid", "smoothed",
|
||||
"sequence", "csi_fps_ema", "csi_fps_samples"] {
|
||||
assert!(s.get(key).is_some(),
|
||||
"sync object missing field `{}` — UI contract broken", key);
|
||||
}
|
||||
// Spot-check values round-trip.
|
||||
assert_eq!(s["offset_us"], 1_163_565);
|
||||
assert_eq!(s["is_leader"], false);
|
||||
assert_eq!(s["sequence"], 20);
|
||||
assert_eq!(s["csi_fps_samples"], 47);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_absent_omits_the_key_entirely() {
|
||||
// skip_serializing_if = "Option::is_none" must drop the key, not
|
||||
// emit `"sync": null`. The non-mesh paths rely on this for
|
||||
// backwards compatibility with pre-iter-23 UI clients.
|
||||
let v = serde_json::to_value(sample_node(None)).unwrap();
|
||||
assert!(v.get("sync").is_none(),
|
||||
"expected `sync` key omitted when None, got {:?}", v.get("sync"));
|
||||
// The base NodeInfo fields are still there.
|
||||
assert_eq!(v["node_id"], 9);
|
||||
assert_eq!(v["rssi_dbm"], -38.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_round_trips_through_serde() {
|
||||
let original = sample_node(Some(sample_sync()));
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let parsed: NodeInfo = serde_json::from_str(&json).unwrap();
|
||||
// Field-level equality on the sync sub-object.
|
||||
let s_orig = original.sync.unwrap();
|
||||
let s_parsed = parsed.sync.expect("sync should survive round-trip");
|
||||
assert_eq!(s_parsed.offset_us, s_orig.offset_us);
|
||||
assert_eq!(s_parsed.is_leader, s_orig.is_leader);
|
||||
assert_eq!(s_parsed.is_valid, s_orig.is_valid);
|
||||
assert_eq!(s_parsed.smoothed, s_orig.smoothed);
|
||||
assert_eq!(s_parsed.sequence, s_orig.sequence);
|
||||
assert!((s_parsed.csi_fps_ema - s_orig.csi_fps_ema).abs() < 1e-9);
|
||||
assert_eq!(s_parsed.csi_fps_samples, s_orig.csi_fps_samples);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod novelty_tests {
|
||||
use super::*;
|
||||
|
|
|
|||
Loading…
Reference in New Issue