Skip to content
Open
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
5 changes: 3 additions & 2 deletions crates/nat-aggregate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
28 changes: 28 additions & 0 deletions crates/nat-candle/src/train_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 21 additions & 0 deletions crates/nat-federated/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down
25 changes: 25 additions & 0 deletions crates/nat-sidecar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions crates/nat-weightspace/src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading