use kcode_k1_kmap_format::*;
use kcode_k1_transaction_id::TxId;
fn id(value: u8) -> NodeId {
NodeId([value; 12])
}
fn specs(count: usize, tier: ConnectionTier) -> Vec<ConnectionSpec> {
(0..count)
.map(|value| ConnectionSpec {
target: id(value as u8),
tier,
})
.collect()
}
#[test]
fn identity_weight_and_mass_decay_are_exact() {
let node = id(7);
let transaction: TxId = node.into();
assert_eq!(NodeId::from(transaction), node);
assert_eq!(Weight::initial(), Weight::new(1.0, 3.0).unwrap());
for (value, mass) in [(0.0, 3.0), (0.25, 0.2)] {
let aged = 3.0 * 2.0_f64.powf(-mass / 30.0);
let expected = Weight {
value: (aged + value * mass) / (aged + mass),
mass: aged + mass,
};
let actual = Weight::initial().update(value, mass).unwrap().unwrap();
assert!((actual.value - expected.value).abs() < 1e-12);
assert!((actual.mass - expected.mass).abs() < 1e-12);
}
assert_eq!(
Weight::new(0.0, 3.0).unwrap().update(0.0, 0.2).unwrap(),
None
);
}
#[test]
fn node_and_actions_roundtrip_with_callback_owned_create_identity() {
let node = Node::from_specs(
"title",
"hint",
"narrative",
vec![ConnectionSpec {
target: id(2),
tier: ConnectionTier::Navigation,
}],
)
.unwrap();
assert_eq!(Node::decode(&node.encode().unwrap()).unwrap(), node);
let actions = [
KmapAction::create_node("title", "hint", "narrative", Vec::new()).unwrap(),
KmapAction::update_node(id(1), Some("new".into()), None, None, Vec::new()).unwrap(),
KmapAction::apply_measurements(vec![
ConnectionMeasurement::new(id(1), id(2), 1.0, 3.0).unwrap(),
])
.unwrap(),
];
for action in actions {
assert_eq!(
KmapAction::decode(&action.encode().unwrap()).unwrap(),
action
);
}
assert!(matches!(
KmapAction::create_node("", "", "", Vec::new()).unwrap(),
KmapAction::CreateNode { .. }
));
}
#[test]
fn wire_rejects_empty_version_malformed_trailing_and_invalid_values() {
for bytes in [Vec::new(), vec![2], vec![FORMAT_VERSION, 255]] {
assert!(Node::decode(&bytes).is_err());
assert!(KmapAction::decode(&bytes).is_err());
}
let mut trailing = Node::new("", "", "", Vec::new()).unwrap().encode().unwrap();
trailing.push(0);
assert!(Node::decode(&trailing).is_err());
let invalid = Node {
title: String::new(),
navigation_hint: String::new(),
narrative: String::new(),
connections: vec![Connection {
target: id(1),
tier: ConnectionTier::Automated,
weight: Weight {
value: f64::NAN,
mass: 1.0,
},
}],
};
let mut bytes = vec![FORMAT_VERSION];
bytes.extend(postcard::to_stdvec(&invalid).unwrap());
assert!(Node::decode(&bytes).is_err());
}
#[test]
fn target_uniqueness_and_navigation_limit_are_enforced() {
let duplicate = vec![
ConnectionSpec {
target: id(1),
tier: ConnectionTier::Navigation,
},
ConnectionSpec {
target: id(1),
tier: ConnectionTier::Automated,
},
];
assert!(Node::from_specs("", "", "", duplicate.clone()).is_err());
assert!(KmapAction::create_node("", "", "", duplicate).is_err());
assert!(Node::from_specs("", "", "", specs(12, ConnectionTier::Navigation)).is_ok());
assert!(Node::from_specs("", "", "", specs(13, ConnectionTier::Navigation)).is_err());
assert!(Node::from_specs("", "", "", specs(100, ConnectionTier::Automated)).is_ok());
assert!(
KmapAction::update_node(
id(0),
None,
None,
None,
vec![
ConnectionChange::Remove(id(1)),
ConnectionChange::Set(ConnectionSpec {
target: id(1),
tier: ConnectionTier::Navigation,
}),
],
)
.is_err()
);
}
#[test]
fn changes_preserve_weight_and_measurements_are_atomic() {
let preserved = Weight::new(0.4, 7.0).unwrap();
let mut node = Node::new(
"",
"",
"",
vec![Connection {
target: id(2),
tier: ConnectionTier::Automated,
weight: preserved,
}],
)
.unwrap();
node.apply_connection_changes(&[ConnectionChange::Set(ConnectionSpec {
target: id(2),
tier: ConnectionTier::Navigation,
})])
.unwrap();
assert_eq!(node.connections[0].weight, preserved);
assert_eq!(node.connections[0].tier, ConnectionTier::Navigation);
assert_eq!(
node.apply_measurement(id(2), 0.5, 1.0).unwrap(),
MeasurementOutcome::Updated
);
assert_eq!(
node.apply_measurement(id(9), 0.5, 1.0).unwrap(),
MeasurementOutcome::Absent
);
let before = node.clone();
assert!(node.apply_measurement(id(2), f64::NAN, 1.0).is_err());
assert_eq!(node, before);
let mut weak = Node::new(
"",
"",
"",
vec![Connection {
target: id(3),
tier: ConnectionTier::Automated,
weight: Weight::new(0.0, 3.0).unwrap(),
}],
)
.unwrap();
assert_eq!(
weak.apply_measurement(id(3), 0.0, 0.2).unwrap(),
MeasurementOutcome::Pruned
);
}
#[test]
fn invalid_values_and_masses_are_rejected() {
for value in [f64::NAN, f64::INFINITY, -0.1, 1.1] {
assert!(Weight::new(value, 1.0).is_err());
assert!(ConnectionMeasurement::new(id(1), id(2), value, 1.0).is_err());
}
for mass in [f64::NAN, f64::INFINITY, 0.0, -0.1] {
assert!(Weight::new(0.5, mass).is_err());
assert!(ConnectionMeasurement::new(id(1), id(2), 0.5, mass).is_err());
}
}