From d898e60353f212362b50fb275afb9c4c99ab6eeb Mon Sep 17 00:00:00 2001 From: Larry Klosowski Date: Thu, 22 Oct 2026 15:35:03 -0700 Subject: [PATCH] test(nat): pin commit, gather, sidecar-parse and step-digest guards at their entry points Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KGMeatV8UjtCB4CTbWuAv4 --- crates/nat-aggregate/src/lib.rs | 5 +++-- crates/nat-candle/src/train_loop.rs | 28 ++++++++++++++++++++++++++++ crates/nat-federated/src/lib.rs | 21 +++++++++++++++++++++ crates/nat-sidecar/src/lib.rs | 25 +++++++++++++++++++++++++ crates/nat-weightspace/src/commit.rs | 28 ++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 2 deletions(-) diff --git a/crates/nat-aggregate/src/lib.rs b/crates/nat-aggregate/src/lib.rs index 7ff2a45..aa7fb81 100644 --- a/crates/nat-aggregate/src/lib.rs +++ b/crates/nat-aggregate/src/lib.rs @@ -47,8 +47,9 @@ mod tests { pg("b", &[1.5, 2.5]), pg("c", &[1.25, 2.25]), ]; - let r1 = one_per_bucket(&g, 0); - let r2 = one_per_bucket(&g, 0); + // Trim 1 of 3: the shared kernel refuses a zero trim budget. + let r1 = one_per_bucket(&g, 1); + let r2 = one_per_bucket(&g, 1); assert_eq!(r1, r2); assert_eq!(r1.digest.len(), 64); } diff --git a/crates/nat-candle/src/train_loop.rs b/crates/nat-candle/src/train_loop.rs index 671332b..0625c85 100644 --- a/crates/nat-candle/src/train_loop.rs +++ b/crates/nat-candle/src/train_loop.rs @@ -468,6 +468,34 @@ mod tests { assert!(base.bytes().all(|b| b.is_ascii_hexdigit())); } + /// The contribution a training step emits carries a provenance hash built + /// from that step's realized loss and the resolved backend, not just from the + /// public `(seed, step, tokens)` triple. Checked at the emitting call, with a + /// freshly built (untrained) model; no training runs. + #[test] + fn step_contribution_binds_the_realized_loss_and_backend() { + let cfg = NatTrainConfig::small_3zone(); + let model = NatTrainModel::new(&cfg).unwrap(); + let a = model.step_contribution(3, 1024, 2.5); + let b = model.step_contribution(3, 1024, 2.4); + assert_ne!( + a.provenance_hash, b.provenance_hash, + "same (seed, step, tokens) with a different loss must not share a hash" + ); + assert_eq!( + a.provenance_hash, + step_provenance_hash( + cfg.seed, + 3, + 1024, + Q16::from_f32(2.5).raw(), + crate::device::backend_label(), + ) + ); + assert_eq!(a.tokens, 1024); + assert_eq!(a, model.step_contribution(3, 1024, 2.5), "deterministic"); + } + #[test] fn held_out_loss_drops_end_to_end() { // The whole pass (embedding → router → zones → merge → readout) trains: diff --git a/crates/nat-federated/src/lib.rs b/crates/nat-federated/src/lib.rs index 259a798..9b4d4dd 100644 --- a/crates/nat-federated/src/lib.rs +++ b/crates/nat-federated/src/lib.rs @@ -633,6 +633,27 @@ mod tests { assert_eq!(r2.accepted.len(), 1); } + /// The gather accepts only full-width digests: a lowercase-hex trace hash that + /// is one character short or long, or upper-case, is refused before it can + /// enter the merged hash. + #[test] + fn gather_refuses_a_trace_hash_that_is_not_64_lowercase_hex() { + let v = roster(&["a"]); + let full = th("ta"); + let short = full[..63].to_string(); + let long = format!("{full}0"); + let upper = full.to_uppercase(); + for bad in [short, long, upper, String::new()] { + let c = SignedContribution::create(&signer("a"), contrib(4.0, 0.5, "pa"), "ma", &bad) + .expect("local toy signer cannot fail"); + let r = gather_and_aggregate(std::slice::from_ref(&c), &v); + assert!(r.accepted.is_empty(), "{bad:?} must not be accepted"); + assert_eq!(r.rejected.len(), 1); + assert_eq!(r.rejected[0].reason, RejectReason::MalformedTraceHash); + assert_eq!(r.total_reward_weight, Q16::ZERO); + } + } + #[test] fn merged_hash_is_order_independent() { let v = roster(&["a", "b"]); diff --git a/crates/nat-sidecar/src/lib.rs b/crates/nat-sidecar/src/lib.rs index 0b4f90b..f99b639 100644 --- a/crates/nat-sidecar/src/lib.rs +++ b/crates/nat-sidecar/src/lib.rs @@ -249,6 +249,31 @@ mod tests { ); } + /// The parse entry point applies the slice checks: a `.nat.json` whose zones + /// alias one slice, or declare a zero-width slice, does not load. + #[test] + fn from_json_refuses_aliased_and_zero_width_slices() { + let mut aliased = Sidecar::default_l0(); + for z in &mut aliased.zones { + z.slice_offset = 0; + z.slice_width = 16; + } + assert!(matches!( + Sidecar::from_json(&aliased.to_json()), + Err(SidecarError::OverlappingSlices { .. }) + )); + + let mut zero = Sidecar::default_l0(); + zero.zones[2].slice_width = 0; + assert!(matches!( + Sidecar::from_json(&zero.to_json()), + Err(SidecarError::ZeroWidthSlice { .. }) + )); + + let honest = Sidecar::default_l0(); + assert_eq!(Sidecar::from_json(&honest.to_json()).unwrap(), honest); + } + /// NAT2-B-005 tripwire. A sidecar is the untrusted half of a downloaded model. /// Zone-slice isolation must be *enforced*, not conventional: a sidecar that /// aliases two zones onto overlapping hidden channels used to validate cleanly, diff --git a/crates/nat-weightspace/src/commit.rs b/crates/nat-weightspace/src/commit.rs index 6e6686f..4ce27e6 100644 --- a/crates/nat-weightspace/src/commit.rs +++ b/crates/nat-weightspace/src/commit.rs @@ -368,6 +368,34 @@ mod tests { )); } + /// The checked commitment validates node features as well as edge weights: + /// a non-finite or out-of-grid feature is refused, never quantized onto the + /// grid point a legal feature already occupies. + #[test] + fn commit_checked_rejects_a_bad_node_feature() { + let g = lower_nat(&nat_checkpoint(11)); + assert!(!g.nodes.is_empty() && !g.nodes[0].feats.is_empty()); + + let mut nan_feat = g.clone(); + nan_feat.nodes[0].feats[0] = f32::NAN; + assert_eq!(commit_checked(&nan_feat), Err(CommitError::NonFiniteWeight)); + + let mut inf_feat = g.clone(); + let last = inf_feat.nodes.len() - 1; + inf_feat.nodes[last].feats[0] = f32::NEG_INFINITY; + assert_eq!(commit_checked(&inf_feat), Err(CommitError::NonFiniteWeight)); + + let mut big_feat = g.clone(); + big_feat.nodes[0].feats[0] = -1e20; + assert!(matches!( + commit_checked(&big_feat), + Err(CommitError::OutOfGridWeight(_)) + )); + + // The untouched graph still commits to the unchecked digest. + assert_eq!(commit_checked(&g).unwrap(), canonical_digest(&g)); + } + #[test] fn adding_an_edge_changes_the_digest() { let g = lower_transformer(&transformer_checkpoint(11, 2, 4));