epiphany/crates/epiphany-testkit/src/roundtrip.rs

766 lines
34 KiB
Rust

//! The canonical round-trip harness (QUICKSTART, Agent F):
//!
//! > the canonical round-trip harness (serialize → bytes → deserialize → assert
//! > byte-identical re-serialization)
//!
//! This is v0 acceptance criterion 4 (canonical serialization stability), which
//! tests Appendix D's canonical-serialization layer. All tiers are real (A, B,
//! C, and D have shipped):
//!
//! 1. [`assert_roundtrip`] — the generic property over any
//! [`CanonicalEncode`] + [`CanonicalDecode`] value:
//! `decode(encode(x)) == x` and `encode(decode(encode(x))) == encode(x)`.
//! [`run_roundtrip_corpus`] sweeps it across **every** canonical-serialized
//! public type in A and B (all typed identifiers, both `RationalTime` arms,
//! every `TypedObjectId` discriminant, the time types).
//! 2. [`assert_manifest_roundtrip`] — the real bundle [`Manifest`], plus the
//! [`FixedHeader`] and [`Superblock`] slot encodings, round-tripped. The
//! manifest is exercised with [`crate::generators::rich_manifest`], so
//! snapshots, blobs, extensions, profiles, retention, and the optional
//! roots — not just `operation_roots` — are covered.
//! 3. [`assert_reduction_serialization_stable`] — a score's canonical state: an
//! [`epiphany_ops::OperationSet`] is reduced to its canonical
//! [`epiphany_ops::MaterializedState`]'s `canonical_bytes` (the canonical
//! serialized score state), which survives content-addressed storage in a real
//! bundle, decodes back into the same materialized state, and re-serializes
//! byte-identically. Musical sensitivity is
//! proven by [`assert_content_mutation_changes_serialization`] (same
//! identities, changed content → different bytes) and
//! [`assert_distinct_scores_serialize_differently`].
use std::fmt::Debug;
use epiphany_bundle::{
pack_operation_blocks, Bundle, ChunkKind, CommitContext, DocumentId, FileUuid, FixedHeader,
FrontierBytes, Manifest, MemStore, OperationBlockSummary, ProfileId, ReductionAlgorithmVersion,
SchemaVersion, SlotParse, SnapshotId, SnapshotRef, StagedChunk, Superblock,
};
use epiphany_core::Score;
use epiphany_determinism::{CanonicalDecode, CanonicalEncode};
use epiphany_ops::{MaterializedState, OperationEnvelope, OperationSet};
use crate::generators;
use crate::rng::Rng;
/// The generic round-trip property. Returns the canonical bytes so callers can
/// sanity-check widths. Panics on any violation.
pub fn assert_roundtrip<T>(value: &T) -> Vec<u8>
where
T: CanonicalEncode + CanonicalDecode + PartialEq + Debug,
{
let bytes = value.to_canonical_bytes();
let decoded =
T::decode_canonical(&bytes).unwrap_or_else(|e| panic!("decode of {value:?} failed: {e}"));
assert_eq!(&decoded, value, "round-trip changed the value: {value:?}");
let re_encoded = decoded.to_canonical_bytes();
assert_eq!(
re_encoded, bytes,
"re-encode not byte-identical for {value:?}"
);
bytes
}
/// Sweeps [`assert_roundtrip`] over every canonical-serialized public type in
/// Agents A and B, drawing `iters` random values from `seed`. This is the
/// type-level half of acceptance criterion 4.
pub fn run_roundtrip_corpus(iters: u64, seed: u64) {
let mut rng = Rng::new(seed);
for _ in 0..iters {
match rng.below(43) {
// --- Agent A: epiphany-determinism ---
0 => drop(assert_roundtrip(&generators::quantized_coord(&mut rng))),
1 => drop(assert_roundtrip(&generators::canonical_f64(&mut rng))),
2 => drop(assert_roundtrip(&generators::content_hash(&mut rng))),
3 => drop(assert_roundtrip(&generators::chunk_id_gen(&mut rng))),
4 => drop(assert_roundtrip(&generators::domain_tag(&mut rng))),
// --- Agent B: the full typed-identifier family ---
5 => drop(assert_roundtrip(&generators::event_id(&mut rng))),
6 => drop(assert_roundtrip(&generators::pitch_id(&mut rng))),
7 => drop(assert_roundtrip(&generators::voice_id(&mut rng))),
8 => drop(assert_roundtrip(&generators::staff_id(&mut rng))),
9 => drop(assert_roundtrip(&generators::staff_instance_id(&mut rng))),
10 => drop(assert_roundtrip(&generators::staff_group_id(&mut rng))),
11 => drop(assert_roundtrip(&generators::region_id(&mut rng))),
12 => drop(assert_roundtrip(&generators::instrument_id(&mut rng))),
13 => drop(assert_roundtrip(&generators::part_definition_id(&mut rng))),
14 => drop(assert_roundtrip(&generators::measure_id(&mut rng))),
15 => drop(assert_roundtrip(&generators::barline_alignment_group_id(
&mut rng,
))),
16 => drop(assert_roundtrip(&generators::tuplet_id(&mut rng))),
17 => drop(assert_roundtrip(&generators::slur_id(&mut rng))),
18 => drop(assert_roundtrip(&generators::tie_id(&mut rng))),
19 => drop(assert_roundtrip(&generators::beam_id(&mut rng))),
20 => drop(assert_roundtrip(&generators::spanner_id(&mut rng))),
21 => drop(assert_roundtrip(&generators::marker_id(&mut rng))),
22 => drop(assert_roundtrip(&generators::analytical_annotation_id(
&mut rng,
))),
23 => drop(assert_roundtrip(&generators::comment_id(&mut rng))),
24 => drop(assert_roundtrip(&generators::time_signature_id(&mut rng))),
25 => drop(assert_roundtrip(&generators::analysis_layer_id(&mut rng))),
26 => drop(assert_roundtrip(&generators::repeat_structure_id(&mut rng))),
27 => drop(assert_roundtrip(&generators::lyric_line_id(&mut rng))),
28 => drop(assert_roundtrip(&generators::chord_symbol_id(&mut rng))),
29 => drop(assert_roundtrip(&generators::operation_id(&mut rng))),
// The tagged union over the whole family (every discriminant + Registered).
30 => drop(assert_roundtrip(&generators::typed_object_id(&mut rng))),
31 => drop(assert_roundtrip(&generators::graphic_object_id(&mut rng))),
32 => drop(assert_roundtrip(&generators::graphic_gesture_id(&mut rng))),
33 => drop(assert_roundtrip(&generators::view_id(&mut rng))),
34 => drop(assert_roundtrip(&generators::object_kind_registry_id(
&mut rng,
))),
35 => drop(assert_roundtrip(&generators::replica_id(&mut rng))),
36 => drop(assert_roundtrip(&generators::transaction_id(&mut rng))),
37 => drop(assert_roundtrip(&generators::integrity_anomaly_id(
&mut rng,
))),
// --- Agent B: time (both RationalTime arms via the generator) ---
38 => drop(assert_roundtrip(&generators::rational_time(&mut rng))),
39 => drop(assert_roundtrip(&generators::musical_position(&mut rng))),
40 => drop(assert_roundtrip(&generators::musical_duration(&mut rng))),
41 => drop(assert_roundtrip(&generators::wallclock_time(&mut rng))),
_ => drop(assert_roundtrip(&generators::wallclock_duration(&mut rng))),
}
}
}
/// Runs Agent A's own 1,000,000-iteration determinism round-trip gate (the
/// QUICKSTART hand-off gate), re-exposed here so the whole conformance suite has
/// a single entry point.
pub fn run_determinism_roundtrip_gate(iters: u64, seed: u64) {
epiphany_determinism::fuzz::run_round_trip_fuzz(iters, seed);
}
/// The commit-context closure used to advance a bundle: append the commit's new
/// chunks to the previous manifest's `operation_roots`.
fn append_roots(ctx: &CommitContext) -> Manifest {
let mut m = ctx.previous_manifest.clone();
m.operation_roots.extend(ctx.new_chunks.iter().copied());
m
}
/// Asserts the real bundle manifest serialization round-trips byte-stably:
/// `encode → decode → encode` is byte-identical and `decode` is a fixpoint.
/// (The manifest's reference vectors are put into canonical order at encode time,
/// so this is the bundle layer's statement of criterion 4.)
pub fn assert_manifest_roundtrip(manifest: &Manifest) {
let bytes = manifest.encode();
let decoded = Manifest::decode(&bytes).expect("manifest must decode");
let re_encoded = decoded.encode();
assert_eq!(
bytes, re_encoded,
"manifest re-encode not byte-identical (criterion 4)"
);
let decoded2 = Manifest::decode(&re_encoded).expect("re-decode");
assert_eq!(decoded, decoded2, "manifest decode is not a fixpoint");
}
/// Asserts the [`FixedHeader`] round-trips: `decode(encode(h)) == h` and the
/// re-encode is byte-identical.
pub fn assert_header_roundtrip(header: &FixedHeader) {
let bytes = header.encode();
let decoded = FixedHeader::decode(&bytes).expect("header decodes");
assert_eq!(&decoded, header, "header round-trip changed the value");
assert_eq!(
decoded.encode(),
bytes,
"header re-encode not byte-identical"
);
}
/// Asserts a committed [`Superblock`] round-trips through its 256-byte slot
/// encoding via [`Superblock::parse_slot`].
pub fn assert_superblock_roundtrip(sb: &Superblock) {
let bytes = sb.encode();
match Superblock::parse_slot(&bytes) {
SlotParse::Valid(parsed) => {
assert_eq!(&parsed, sb, "superblock round-trip changed the value");
assert_eq!(
parsed.encode(),
bytes,
"superblock re-encode not byte-identical"
);
}
SlotParse::Rejected(reject) => {
panic!("a committed superblock must parse as Valid, got {reject:?}")
}
}
}
/// Builds a non-trivial manifest by driving a real bundle through several
/// commits, then returns it.
pub fn committed_manifest(seed: u64) -> Manifest {
let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16());
let mut bundle =
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
for _ in 0..3 {
let n = rng.range_usize(1, 3);
let payloads: Vec<Vec<u8>> = (0..n).map(|_| rng.byte_vec(1, 80)).collect();
let blocks: Vec<StagedChunk> = epiphany_bundle::pack_operation_blocks(&payloads)
.into_iter()
.map(StagedChunk::operation_block)
.collect();
bundle.commit(&blocks, append_roots).expect("commit");
}
bundle.manifest().clone()
}
/// Reduces `envelopes` to the canonical serialized score state (Chapter 6: the
/// materialized graph is a deterministic reduction of the operation set).
fn canonical_score_state(envelopes: &[OperationEnvelope]) -> MaterializedState {
let mut set = OperationSet::new();
set.accept_all(envelopes.iter().cloned());
set.reduce()
}
fn canonical_score_bytes(envelopes: &[OperationEnvelope]) -> Vec<u8> {
canonical_score_state(envelopes).canonical_bytes()
}
/// Reduction-serialization stability for a **score's canonical state**
/// (acceptance criterion 4): the operation
/// set reduces to canonical bytes; re-reducing the same set yields byte-identical
/// bytes; and those bytes survive content-addressed storage in a real bundle —
/// stored as a `Snapshot` chunk referenced by the manifest's `canonical_base`
/// (its correct semantic home), hash-verified on reopen and read back
/// byte-identically.
///
/// The snapshot's `covers_causal_frontier` is the frontier the snapshot actually
/// materializes ([`crate::generators::frontier_bytes`] over the reduced
/// envelopes), so it is semantically consistent — not a falsely-empty frontier
/// that would invite a replay layer to reapply already-materialized effects.
///
/// After reopen, the snapshot payload is decoded through
/// [`MaterializedState::decode_canonical`], compared structurally with the
/// pre-storage reduction, and re-serialized byte-identically. This is the real
/// serialize → load → deserialize → reserialize cycle required by criterion 4.
pub fn assert_reduction_serialization_stable(envelopes: &[OperationEnvelope], seed: u64) {
let state = canonical_score_state(envelopes);
let canonical = state.canonical_bytes();
// re-reduce the same operation set: byte-identical canonical state.
assert_eq!(
canonical,
canonical_score_bytes(envelopes),
"re-reduction changed the canonical score bytes"
);
// serialize: stage the canonical state as a real **Snapshot** chunk and
// reference it from the manifest's `canonical_base` — its correct semantic
// home (a materialized snapshot), with the right chunk kind.
let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16());
let mut bundle =
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
let snapshot = StagedChunk {
kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::V0,
payload: canonical.clone(),
};
bundle
.commit(&[snapshot], |ctx| {
let mut m = ctx.previous_manifest.clone();
let root = ctx.new_chunks[0];
let mut sid = [0u8; 16];
sid.copy_from_slice(&root.hash.as_bytes()[..16]);
m.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId(sid),
// The frontier the snapshot actually materializes (covering every
// reduced envelope), not a falsely-empty one.
covers_causal_frontier: FrontierBytes::from_bytes(generators::frontier_bytes(
envelopes,
)),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
m
})
.expect("commit snapshot");
let image = bundle.into_store().into_bytes();
// load: reopen from exactly those bytes; the snapshot chunk is hash-verified
// on open and read back byte-identically.
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle");
reopened
.verify_canonical_chunks()
.expect("canonical chunks intact");
let base = reopened
.manifest()
.canonical_base
.as_ref()
.expect("a canonical base");
let loaded = reopened
.read_chunk(&base.root)
.expect("read snapshot chunk back");
assert_eq!(
loaded, canonical,
"canonical state was not preserved through content-addressed storage"
);
let decoded = MaterializedState::decode_canonical(&loaded)
.expect("loaded materialized snapshot must decode");
assert_eq!(decoded, state, "decoded materialized state changed");
assert_eq!(
decoded.canonical_bytes(),
loaded,
"decoded snapshot did not reserialize byte-identically"
);
// The reopened bundle's manifest is itself a real decode→reencode fixpoint.
assert_manifest_roundtrip(reopened.manifest());
}
/// **Full-`Score` canonical serialization stability** (acceptance criterion 4,
/// the whole-graph tier — item 5's whole-score codec). The real
/// [`epiphany_core::Score`] encodes to canonical bytes, survives
/// content-addressed storage as a `Snapshot` chunk in a real bundle
/// (hash-verified on reopen), decodes back to an **equal** `Score`, and
/// re-encodes byte-identically. Unlike [`assert_reduction_serialization_stable`]
/// (which round-trips the Chapter 6 bookkeeping projection), this round-trips the
/// whole musical graph — the arena, voices, regions, cross-cutting, and
/// tombstones — through [`Score::canonical_bytes`] / [`Score::decode_canonical`].
///
/// `frontier` is the causal frontier the snapshot materializes (so the snapshot
/// reference is semantically consistent, not falsely empty).
pub fn assert_score_serialization_stable(score: &Score, frontier: &[u8], seed: u64) {
let canonical = score.canonical_bytes();
// Determinism: re-encoding the same score is byte-identical.
assert_eq!(
canonical,
score.canonical_bytes(),
"re-encoding the same score changed its bytes"
);
// serialize: stage the score as a properly-roled ACCELERATION snapshot
// (Binary Format §Schema Major 2): a `ChunkKind::Snapshot` stamped with
// the current schema major and referenced from the manifest's
// `acceleration_snapshots` — NOT the canonical base, which is the
// MaterializedState's role and stays major 0. (The `SnapshotId` here is a
// hash-truncation stand-in; its derivation is a companion open question.)
let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16());
let mut bundle =
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
let snapshot = StagedChunk {
kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::for_major(2),
payload: canonical.clone(),
};
let frontier = frontier.to_vec();
bundle
.commit(&[snapshot], |ctx| {
let mut m = ctx.previous_manifest.clone();
let root = ctx.new_chunks[0];
let mut sid = [0u8; 16];
sid.copy_from_slice(&root.hash.as_bytes()[..16]);
m.acceleration_snapshots.push(SnapshotRef {
snapshot_id: SnapshotId(sid),
covers_causal_frontier: FrontierBytes::from_bytes(frontier.clone()),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
m
})
.expect("commit snapshot");
let image = bundle.into_store().into_bytes();
// load: reopen (read-write — an acceleration snapshot at the current
// major is within the snapshot role's accept-set), hash-verify, read the
// referenced chunk back byte-identically.
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle");
assert!(
!reopened.is_read_only(),
"a current-major acceleration snapshot must not force read-only"
);
reopened
.verify_canonical_chunks()
.expect("canonical chunks intact");
let accel = reopened
.manifest()
.acceleration_snapshots
.first()
.expect("an acceleration snapshot");
assert_eq!(accel.root.schema_version, SchemaVersion::for_major(2));
let loaded = reopened
.read_chunk(&accel.root)
.expect("read snapshot chunk back");
assert_eq!(
loaded, canonical,
"score bytes were not preserved through content-addressed storage"
);
// deserialize through the SCHEMA-VERSION DISPATCH SEAM, keyed by the
// chunk's stamped major — the read path a real acceleration-snapshot
// consumer uses — then re-serialize byte-identically.
let decoded = Score::decode_canonical_versioned(&loaded, accel.root.schema_version.major)
.expect("loaded score must decode at its stamped major");
assert_eq!(&decoded, score, "decoded score changed");
assert_eq!(
decoded.canonical_bytes(),
loaded,
"decoded score did not reserialize byte-identically"
);
}
/// Confirms criterion 4 is *musically sensitive* in the strongest form: a score
/// whose operations keep **identical identities and ordering metadata** but whose
/// payload *content* changes must reduce to **different** canonical bytes. This
/// is the exact rebuttal to an id-only "serializer" that would collapse distinct
/// scores: the ids/stamps/causal contexts are byte-for-byte the same, so only the
/// content differs.
pub fn assert_content_mutation_changes_serialization() {
let (base, mutated) = generators::content_mutation_pair();
// The operation identities and ordering metadata are byte-for-byte identical;
// only one payload's *content* differs.
assert_eq!(base.len(), mutated.len());
for (b, m) in base.iter().zip(&mutated) {
assert_eq!(b.id, m.id, "operation identity changed");
assert_eq!(b.stamp, m.stamp, "operation stamp changed");
assert_eq!(
b.causal_context, m.causal_context,
"operation causal context changed"
);
}
let differing = base
.iter()
.zip(&mutated)
.filter(|(b, m)| b.payload != m.payload)
.count();
assert_eq!(differing, 1, "exactly one payload's content should differ");
assert_ne!(
canonical_score_bytes(&base),
canonical_score_bytes(&mutated),
"changing operation content (with identities held fixed) must change the canonical bytes"
);
}
/// Confirms two independently-generated operation sets reduce to different
/// canonical bytes (a coarse sensitivity check; the strong form is
/// [`assert_content_mutation_changes_serialization`]).
pub fn assert_distinct_scores_serialize_differently(
a: &[OperationEnvelope],
b: &[OperationEnvelope],
) {
assert_ne!(
canonical_score_bytes(a),
canonical_score_bytes(b),
"distinct operation sets must reduce to distinct canonical bytes"
);
}
/// Asserts the real `Manifest` **decoder** rejects corrupted bytes — exercising
/// the decode/canonicalization validation path, not just the happy round-trip.
pub fn assert_manifest_decode_rejects_corruption(manifest: &Manifest) {
let bytes = manifest.encode();
assert!(bytes.len() > 4);
// Flip a byte in the body: the stored manifest id will no longer match the
// id re-derived from the (corrupted) body, so decode must reject it.
let mut corrupt = bytes.clone();
let i = corrupt.len() / 2;
corrupt[i] ^= 0xFF;
assert!(
Manifest::decode(&corrupt).is_err(),
"a corrupted manifest must be rejected by the decoder"
);
}
/// Asserts the real `FixedHeader` decoder rejects a corrupted header (CRC).
pub fn assert_header_decode_rejects_corruption(header: &FixedHeader) {
let mut bytes = header.encode().to_vec();
bytes[8] ^= 0xFF; // a byte inside the CRC-covered region
assert!(
FixedHeader::decode(&bytes).is_err(),
"a corrupted header must be rejected by the decoder"
);
}
/// The ops-computed summary of an operation block (Chapter 8: an
/// `OperationEnvelopeBlock`'s `dvv_summary`/`min_stamp`/`max_stamp`). This is the
/// **C/D integration point**: the operation layer (Agent C) computes the
/// semantic summary by reading the envelopes — the causal frontier they cover
/// and the canonical bytes of the minimum and maximum operation stamps — and the
/// bundle (Agent D) carries it opaquely, keyed by the block's chunk id, so a
/// reader can select a block by frontier/stamp range without decoding it.
pub fn operation_block_summary(envelopes: &[OperationEnvelope]) -> OperationBlockSummary {
let stamp_bytes = |e: &OperationEnvelope| e.stamp.to_canonical_bytes();
OperationBlockSummary {
dvv_summary: FrontierBytes::from_bytes(generators::frontier_bytes(envelopes)),
min_stamp: envelopes
.iter()
.min_by_key(|e| e.stamp.reduction_tuple())
.map(stamp_bytes)
.unwrap_or_default(),
max_stamp: envelopes
.iter()
.max_by_key(|e| e.stamp.reduction_tuple())
.map(stamp_bytes)
.unwrap_or_default(),
}
}
/// Asserts an ops-computed [`operation_block_summary`] survives a real bundle
/// commit + reopen and is selectable by the block's chunk id without decoding the
/// block payload (Chapter 8 operation-block summary metadata, C/D integration).
pub fn assert_operation_block_summary_survives_storage(envelopes: &[OperationEnvelope], seed: u64) {
let summary = operation_block_summary(envelopes);
assert!(
!summary.dvv_summary.as_bytes().is_empty()
&& !summary.min_stamp.is_empty()
&& !summary.max_stamp.is_empty(),
"a non-empty envelope set must produce a non-vacuous summary"
);
let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16());
let mut bundle =
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
// A real operation block (opaque payload bytes) carrying the summary.
let blocks: Vec<StagedChunk> = pack_operation_blocks(&[rng.byte_vec(4, 64)])
.into_iter()
.map(StagedChunk::operation_block)
.collect();
bundle
.commit(&blocks, |ctx| {
let mut m = ctx.previous_manifest.clone();
let root = ctx.new_chunks[0];
m.operation_roots.push(root);
m.operation_block_summaries.insert(root.id, summary.clone());
m
})
.expect("commit operation block + summary");
// Reopen and select the summary by block id — no block payload is decoded.
let image = bundle.into_store().into_bytes();
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle");
let root_id = reopened.manifest().operation_roots[0].id;
assert_eq!(
reopened.manifest().operation_block_summary(root_id),
Some(&summary),
"the ops-computed block summary must survive storage and be selectable"
);
}
#[cfg(test)]
mod tests {
use super::*;
use epiphany_bundle::encode_block;
#[test]
fn operation_block_summaries_survive_storage_and_select() {
let mut rng = Rng::new(0x05_5077_5044_0B0B);
for seed in 0..16u64 {
let envelopes = generators::operation_envelopes(&mut rng, 24, 3, 8, 8);
assert_operation_block_summary_survives_storage(&envelopes, seed.wrapping_add(1));
}
}
/// A real `CreateRegion` operation envelope (schema major 1: its payload
/// carries `Region.permits_spanning_slurs`).
fn create_region_envelope() -> OperationEnvelope {
use epiphany_core::{OperationId, RegionId, ReplicaId, WallClockTime};
use epiphany_ops::{
AuthorId, CausalContext, CreateRegionOp, HybridLogicalClock, OperationKind,
OperationPayload, OperationStamp,
};
let rid = RegionId::new(ReplicaId(9), 3);
let mut region = epiphany_ops::valuegen::region(rid);
region.permits_spanning_slurs = true;
let id = OperationId::new(ReplicaId(9), 1);
OperationEnvelope {
id,
author: AuthorId(0),
stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(1), 0), id),
causal_context: CausalContext::new(),
transaction: None,
payload: OperationPayload::Primitive(OperationKind::CreateRegion(CreateRegionOp {
region,
})),
}
}
/// Commits a single already-staged operation block into a fresh bundle and
/// returns the reopened bundle.
fn reopen_with_op_block(seed: u64, block: StagedChunk) -> Bundle<MemStore> {
let mut rng = Rng::new(seed);
let mut bundle = Bundle::create(
MemStore::new(),
FileUuid(rng.array16()),
Manifest::empty(DocumentId(rng.array16())),
)
.expect("create bundle");
bundle
.commit(&[block], |ctx| {
let mut m = ctx.previous_manifest.clone();
m.operation_roots.push(ctx.new_chunks[0]);
m
})
.expect("commit op block");
let image = bundle.into_store().into_bytes();
Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle")
}
#[test]
fn create_region_op_block_is_stamped_major_1_and_reopens_read_write() {
let env = create_region_envelope();
assert_eq!(
env.schema_major(),
1,
"CreateRegion encodes at schema major 1"
);
// The WRITER *derives* the block major from its operations — the same
// `stage_operation_block` the real-envelope harness uses — so this proves
// derivation, not a hand-picked version. A v1 CreateRegion → major 1.
let block = crate::bundle_harness::stage_operation_block(std::slice::from_ref(&env));
let reopened = reopen_with_op_block(0xD2_0001, block);
// Major 1 is within the op-block accept-set [0,1], so the bundle opens
// read-write and the block reads back opaquely.
assert_eq!(
reopened.manifest().operation_roots[0].schema_version,
SchemaVersion::V1
);
assert!(!reopened.is_read_only());
let blocks = reopened
.read_operation_block(&reopened.manifest().operation_roots[0])
.expect("major-1 op block is admitted by the accept-set");
assert_eq!(blocks, vec![env.to_canonical_bytes()]);
}
#[test]
fn cross_cutting_op_block_is_stamped_major_2_and_reopens_read_write() {
// Schema major 2 (minimal stamping): a CreateCrossCutting payload's
// v2 fills are mandatory appended fields, so the kind is always
// major 2; the writer derives the block stamp from its operations.
use epiphany_core::{OperationId, ReplicaId, SlurId, WallClockTime};
use epiphany_ops::{
AuthorId, CausalContext, CreateCrossCuttingOp, CrossCuttingValue, HybridLogicalClock,
OperationKind, OperationPayload, OperationStamp,
};
let slur = epiphany_ops::valuegen::slur(
SlurId::new(ReplicaId(9), 5),
epiphany_core::EventId::new(ReplicaId(9), 100),
epiphany_core::EventId::new(ReplicaId(9), 101),
);
let id = OperationId::new(ReplicaId(9), 2);
let env = OperationEnvelope {
id,
author: AuthorId(0),
stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(2), 0), id),
causal_context: CausalContext::new(),
transaction: None,
payload: OperationPayload::Primitive(OperationKind::CreateCrossCutting(
CreateCrossCuttingOp {
structure: CrossCuttingValue::Slur(slur),
},
)),
};
assert_eq!(
env.schema_major(),
2,
"CreateCrossCutting encodes at schema major 2"
);
let block = crate::bundle_harness::stage_operation_block(std::slice::from_ref(&env));
let reopened = reopen_with_op_block(0xD2_0003, block);
// Major 2 is within the op-block accept-set [0,2]: read-write.
assert_eq!(
reopened.manifest().operation_roots[0].schema_version,
SchemaVersion::V2
);
assert!(!reopened.is_read_only());
let blocks = reopened
.read_operation_block(&reopened.manifest().operation_roots[0])
.expect("major-2 op block is admitted by the accept-set");
assert_eq!(blocks, vec![env.to_canonical_bytes()]);
}
#[test]
fn op_block_beyond_the_accept_set_opens_read_only() {
use epiphany_bundle::IntegrityAnomaly;
// A newer writer's op block, stamped schema major 3 — beyond the reader's
// op-block accept-set [0,2]. The bundle opens read-only preservation (the
// canonical base and manifest still read) rather than hard-rejecting.
let block = StagedChunk::operation_block_versioned(
encode_block(&[vec![1u8, 2, 3, 4]]),
SchemaVersion::new(3, 0),
);
let reopened = reopen_with_op_block(0xD2_0002, block);
assert!(
reopened.is_read_only(),
"a beyond-accept-set canonical root opens read-only"
);
assert!(reopened.anomalies().iter().any(|a| matches!(
a,
IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major: 3 }
)));
}
#[test]
fn corpus_round_trips() {
run_roundtrip_corpus(60_000, 0x00C0_FFEE_1234_5678);
}
#[test]
fn manifest_round_trips_and_rejects_corruption() {
for seed in 0..64u64 {
let m = committed_manifest(seed.wrapping_mul(0x9E37_79B9).wrapping_add(3));
assert_manifest_roundtrip(&m);
// The rich manifest: every optional field and reference vector.
let mut rng = Rng::new(seed.wrapping_mul(0x100_0193).wrapping_add(17));
let rich = generators::rich_manifest(&mut rng);
assert_manifest_roundtrip(&rich);
// The decoder rejects corruption (canonicalization/validation path).
assert_manifest_decode_rejects_corruption(&rich);
}
}
#[test]
fn header_and_superblock_round_trip() {
let mut rng = Rng::new(0x4845_4144); // "HEAD"
for _ in 0..64 {
let header = FixedHeader::new(FileUuid(rng.array16()));
assert_header_roundtrip(&header);
assert_header_decode_rejects_corruption(&header);
}
// A real committed superblock from a live bundle.
let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16());
let mut bundle =
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create");
bundle
.commit(
&[StagedChunk::operation_block(encode_block(&[vec![1u8; 8]]))],
append_roots,
)
.expect("commit");
assert_header_roundtrip(bundle.header());
assert_superblock_roundtrip(bundle.superblock());
// Generated committed superblocks round-trip through the slot encoding.
for _ in 0..32 {
assert_superblock_roundtrip(&generators::superblock(&mut rng));
}
}
#[test]
fn scores_serialize_stably_and_distinctly() {
for seed in 0..48u64 {
let mut rng = Rng::new(seed.wrapping_mul(0x9E37_79B9));
let score = generators::operation_envelopes(&mut rng, 40, 3, 6, 6);
assert_reduction_serialization_stable(&score, seed);
let other = generators::operation_envelopes(&mut rng, 41, 3, 6, 6);
assert_distinct_scores_serialize_differently(&score, &other);
}
// Strong sensitivity: same identities, changed content → different bytes.
assert_content_mutation_changes_serialization();
}
}