ontoref-code/crates/ontoref-commit/tests/basic.rs
2026-07-10 01:44:59 +01:00

270 lines
7.6 KiB
Rust

use ed25519_dalek::SigningKey;
use ontoref_types::{
operation::{OpBody, OpPayload, Operation},
AttrId, EntityId, Hlc, PublicKey, Value,
};
use ontoref_commit::{verify_witness, CommitLayer};
fn signing_key() -> SigningKey {
SigningKey::from_bytes(&[11u8; 32])
}
fn assert_op(entity: &str, attrs: Vec<(&str, Value)>, tx: Hlc) -> Operation {
let key = signing_key();
let body = OpBody {
actor: PublicKey::from_signing_key(&key),
parents: vec![],
payload: OpPayload::EntityAssert {
attrs: attrs
.into_iter()
.map(|(name, value)| (AttrId::new(name), value))
.collect(),
entity: EntityId::new(entity),
valid_from: None,
},
timestamp: tx,
};
Operation::sign(body, &key)
}
fn fixture_ops() -> Vec<Operation> {
vec![
assert_op(
"alice",
vec![
("name", Value::Str("Alice".into())),
("age", Value::Int(30)),
],
Hlc::new(1, 0),
),
assert_op(
"bob",
vec![
("name", Value::Str("Bob".into())),
("age", Value::Int(25)),
],
Hlc::new(2, 0),
),
assert_op(
"alice",
vec![("age", Value::Int(31))],
Hlc::new(3, 0),
),
]
}
#[test]
fn determinism_same_ops_same_root() {
let mut a = CommitLayer::new();
let mut b = CommitLayer::new();
for op in fixture_ops() {
a.apply(&op);
b.apply(&op);
}
assert_eq!(a.root(), b.root());
}
#[test]
fn determinism_empty_root_is_fixed() {
let layer = CommitLayer::new();
let expected = *blake3::hash(b"empty-tree-v1").as_bytes();
assert_eq!(layer.root(), expected);
}
#[test]
fn determinism_root_changes_with_value_overwrite() {
let mut layer = CommitLayer::new();
layer.apply(&assert_op(
"e",
vec![("k", Value::Str("v1".into()))],
Hlc::new(1, 0),
));
let r1 = layer.root();
layer.apply(&assert_op(
"e",
vec![("k", Value::Str("v2".into()))],
Hlc::new(2, 0),
));
let r2 = layer.root();
assert_ne!(r1, r2, "root must change when an existing cell's value changes");
}
#[test]
fn witness_verify_succeeds_for_present_cell() {
let mut layer = CommitLayer::new();
for op in fixture_ops() {
layer.apply(&op);
}
let root = layer.root();
let witness = layer
.witness(&EntityId::new("alice"), &AttrId::new("name"))
.unwrap();
assert!(verify_witness(&witness, &root));
}
#[test]
fn witness_verify_succeeds_after_overwrite() {
let mut layer = CommitLayer::new();
for op in fixture_ops() {
layer.apply(&op);
}
let root = layer.root();
// alice's age was overwritten from 30 to 31; the witness should reflect 31.
let witness = layer
.witness(&EntityId::new("alice"), &AttrId::new("age"))
.unwrap();
assert!(verify_witness(&witness, &root));
// The witnessed value should be the canonical encoding of Int(31).
let expected = ontoref_types::canonical::encode(&Value::Int(31)).unwrap();
assert_eq!(witness.value, expected);
}
#[test]
fn witness_verify_fails_for_tampered_value() {
let mut layer = CommitLayer::new();
for op in fixture_ops() {
layer.apply(&op);
}
let root = layer.root();
let mut witness = layer
.witness(&EntityId::new("alice"), &AttrId::new("name"))
.unwrap();
witness.value[0] ^= 0x01;
assert!(!verify_witness(&witness, &root));
}
#[test]
fn witness_verify_fails_for_tampered_path() {
let mut layer = CommitLayer::new();
for op in fixture_ops() {
layer.apply(&op);
}
let root = layer.root();
let mut witness = layer
.witness(&EntityId::new("alice"), &AttrId::new("name"))
.unwrap();
if !witness.path.is_empty() {
witness.path[0].sibling[0] ^= 0x01;
} else {
// For a 1-cell tree, witness.path is empty; force the tree larger so
// the test exercises tampering at a real path step.
panic!("fixture must produce a non-trivial Merkle path");
}
assert!(!verify_witness(&witness, &root));
}
#[test]
fn witness_verify_fails_against_wrong_root() {
let mut layer = CommitLayer::new();
for op in fixture_ops() {
layer.apply(&op);
}
let witness = layer
.witness(&EntityId::new("alice"), &AttrId::new("name"))
.unwrap();
let bogus_root = [0xFFu8; 32];
assert!(!verify_witness(&witness, &bogus_root));
}
#[test]
fn cross_instance_determinism_with_independent_apply_order_per_entity() {
// Two instances apply ops in the same logical order; their state roots
// must be byte-identical.
let ops = fixture_ops();
let mut left = CommitLayer::new();
for op in &ops {
left.apply(op);
}
let mut right = CommitLayer::new();
for op in &ops {
right.apply(op);
}
assert_eq!(left.root(), right.root());
assert_eq!(left.len(), right.len());
}
#[test]
fn cross_instance_witness_verifies_under_other_instance_root() {
// A witness built on instance A verifies against instance B's root,
// because both roots are identical.
let ops = fixture_ops();
let mut left = CommitLayer::new();
let mut right = CommitLayer::new();
for op in &ops {
left.apply(op);
right.apply(op);
}
let witness = left
.witness(&EntityId::new("bob"), &AttrId::new("name"))
.unwrap();
let right_root = right.root();
assert!(verify_witness(&witness, &right_root));
}
// ── ADR-041 / bl-029 29b: per-cell typed CRDT merge ──────────────────────────
#[test]
fn union_cell_accumulates_and_is_order_independent() {
use ontoref_commit::{CellMergeKind, MergePolicy};
// The "tags" attribute folds as a grow-only set (GSet).
let policy = MergePolicy::new().with("tags", CellMergeKind::Union);
// Two actors assert different tag elements on the same cell. Applied in
// either order, the cell must hold BOTH and hash to the same root.
let a = assert_op("doc", vec![("tags", Value::Str("red".into()))], Hlc::new(1, 0));
let b = assert_op("doc", vec![("tags", Value::Str("blue".into()))], Hlc::new(2, 0));
let mut forward = CommitLayer::new().with_policy(policy.clone());
forward.apply(&a);
forward.apply(&b);
let mut reverse = CommitLayer::new().with_policy(policy.clone());
reverse.apply(&b);
reverse.apply(&a);
assert_eq!(
forward.root(),
reverse.root(),
"union merge is order-independent — actors converge regardless of arrival order"
);
// The union root differs from either single-element assertion: both
// elements are retained, not last-writer-wins.
let mut only_a = CommitLayer::new().with_policy(policy.clone());
only_a.apply(&a);
assert_ne!(
forward.root(),
only_a.root(),
"the second actor's element is retained, not overwritten"
);
}
#[test]
fn lww_is_unchanged_without_a_policy() {
// Default (empty) policy reproduces last-writer-wins: the later (higher
// HLC, applied last) assertion wins the cell.
let a = assert_op("e", vec![("v", Value::Int(1))], Hlc::new(1, 0));
let b = assert_op("e", vec![("v", Value::Int(2))], Hlc::new(2, 0));
let mut layer = CommitLayer::new();
layer.apply(&a);
layer.apply(&b);
// Equals a layer that only ever saw the winning (b) assertion.
let mut only_b = CommitLayer::new();
only_b.apply(&b);
assert_eq!(layer.root(), only_b.root(), "default policy stays last-writer-wins");
}