canwu-sim 0.10.0

Deterministic simulation runtime, scheduling, persistence, replay, and plugins for Canwu
Documentation
use super::*;

#[test]
fn knowledge_publication_is_phase_scoped_atomic_persisted_and_replayable() {
    let (scenario, _) = demo_scenario();
    let plugin = KnowledgePublicationPlugin;
    let mut simulation = Simulation::new(53, scenario.clone())
        .expect("knowledge publication fixture should initialize");
    simulation
        .register_plugin(&plugin)
        .expect("knowledge publication plugin should register");
    let receipt = simulation
        .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
        .expect("phase-scoped publications should settle");
    assert_eq!(receipt.knowledge_batch_count, 3);
    assert_eq!(receipt.knowledge_record_count, 3);
    assert_eq!(receipt.emitted_events.len(), 3);
    let holder = KnowledgeHolderRef::Person(PersonId::new(1));
    assert_eq!(
        simulation
            .knowledge()
            .for_holder(&holder)
            .map_or(0, BTreeMap::len),
        3
    );
    assert_eq!(simulation.boundaries()[0].knowledge_changes.len(), 3);

    let snapshot = simulation.snapshot();
    let restored = Simulation::from_snapshot_with_plugins(snapshot.clone(), &[&plugin])
        .expect("published knowledge should restore with exact schemas");
    assert_eq!(restored.snapshot(), snapshot);

    let replayed = Simulation::replay_from_journal_with_scenario(
        scenario,
        &[&plugin],
        &simulation.replay_journal(),
    )
    .expect("published knowledge should replay exactly");
    assert_eq!(replayed.snapshot(), snapshot);

    let mut missing_record = snapshot;
    missing_record
        .knowledge
        .records
        .get_mut(&holder)
        .expect("holder ledger exists")
        .remove(&KnowledgeRecordId::new(2));
    rehash_tampered_snapshot(&mut missing_record);
    let error = Simulation::from_snapshot_with_plugins(missing_record, &[&plugin])
        .err()
        .expect("boundary evidence must reconstruct the exact holder ledger");
    assert_eq!(error.code, ErrorCode::InvalidSnapshot);
}

#[test]
fn phase13_publication_resolves_exact_current_boundary_record_evidence() {
    let (scenario, _) = demo_scenario();
    let plugin = PendingEvidencePublicationPlugin;
    let mut simulation =
        Simulation::new(59, scenario).expect("pending evidence fixture should initialize");
    simulation
        .register_plugin(&plugin)
        .expect("pending evidence plugin should register");
    simulation
        .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
        .expect("phase 13 should resolve the exact phase-7 record version");

    let snapshot = simulation.snapshot();
    Simulation::from_snapshot_with_plugins(snapshot.clone(), &[&plugin])
        .expect("exact current-boundary evidence should restore");

    let mut wrong_index = snapshot;
    let holder = KnowledgeHolderRef::Person(PersonId::new(1));
    let record = wrong_index
        .knowledge
        .records
        .get_mut(&holder)
        .and_then(|records| records.values_mut().next())
        .expect("published record exists");
    let EvidenceRef::DomainRecordVersion(version) = &mut record.origin.evidence[0] else {
        panic!("fixture origin should contain a record version");
    };
    version.established_by = DomainRecordVersionSource::BoundaryChange {
        boundary: BoundaryId::new(1),
        change_index: 1,
    };
    wrong_index.boundaries[0].knowledge_changes[0].records[0] = record.clone();
    rehash_tampered_snapshot(&mut wrong_index);
    let error = Simulation::from_snapshot_with_plugins(wrong_index, &[&plugin])
        .err()
        .expect("wrong record-change evidence index must fail");
    assert_eq!(error.code, ErrorCode::InvalidSnapshot);
}

#[test]
fn knowledge_snapshot_tamper_matrix_covers_ledger_evidence_and_batch_metadata() {
    let (scenario, _) = demo_scenario();
    let plugin = KnowledgePublicationPlugin;
    let mut simulation =
        Simulation::new(61, scenario).expect("knowledge tamper fixture should initialize");
    simulation
        .register_plugin(&plugin)
        .expect("knowledge publication plugin should register");
    simulation
        .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
        .expect("knowledge tamper fixture should publish");
    let snapshot = simulation.snapshot();
    let holder = KnowledgeHolderRef::Person(PersonId::new(1));
    let reject = |label: &str, mut tampered: SimulationSnapshot| {
        if let Err(error) = try_rehash_tampered_snapshot(&mut tampered) {
            assert_eq!(error.code, ErrorCode::InvalidSnapshot, "{label}");
            return;
        }
        let error = Simulation::from_snapshot_with_plugins(tampered, &[&plugin])
            .err()
            .unwrap_or_else(|| panic!("{label} tamper unexpectedly loaded"));
        assert_eq!(error.code, ErrorCode::InvalidSnapshot, "{label}");
    };

    let mutate_first_record =
        |snapshot: &mut SimulationSnapshot, mutate: &dyn Fn(&mut KnowledgeRecord)| {
            let record = snapshot
                .knowledge
                .records
                .get_mut(&holder)
                .and_then(|records| records.values_mut().next())
                .expect("fixture holder record should exist");
            mutate(record);
        };

    let mut record_holder = snapshot.clone();
    mutate_first_record(&mut record_holder, &|record| {
        record.holder = KnowledgeHolderRef::Person(PersonId::new(2));
    });
    reject("record holder", record_holder);

    let mut schema_version = snapshot.clone();
    mutate_first_record(&mut schema_version, &|record| record.schema.version += 1);
    reject("schema version", schema_version);

    let mut schema_hash = snapshot.clone();
    schema_hash.plugin_descriptors[0].knowledge_schemas[0]
        .schema_hash
        .replace_range(..1, "f");
    reject("schema semantic hash", schema_hash);

    let mut payload = snapshot.clone();
    mutate_first_record(&mut payload, &|record| {
        record.payload = serde_json::json!({ "value": 999 });
    });
    reject("record payload", payload);

    let mut subject = snapshot.clone();
    mutate_first_record(&mut subject, &|record| {
        record.subjects.push(KnowledgeSubject {
            role: "unexpected".to_owned(),
            target: KnowledgeSubjectTarget::Event(EventId::new(1)),
        });
    });
    reject("record subject", subject);

    let mut confidence = snapshot.clone();
    mutate_first_record(&mut confidence, &|record| {
        record.confidence_per_mille = 1_001;
    });
    reject("record confidence", confidence);

    let mut learned_at = snapshot.clone();
    mutate_first_record(&mut learned_at, &|record| {
        record.learned_at = SimTime::from_minutes(1);
    });
    reject("record learned time", learned_at);

    let mut forward_relation = snapshot.clone();
    mutate_first_record(&mut forward_relation, &|record| {
        record.supersedes = vec![KnowledgeRecordId::new(3)];
    });
    reject("forward relation", forward_relation);

    let mut evidence = snapshot.clone();
    mutate_first_record(&mut evidence, &|record| {
        record.origin.evidence = vec![EvidenceRef::Event(EventId::new(999))];
    });
    reject("origin evidence", evidence);

    let mut producer = snapshot.clone();
    producer.boundaries[0].knowledge_changes[0].plugin = "foreign-plugin".to_owned();
    reject("batch producer", producer);

    let mut phase = snapshot.clone();
    phase.boundaries[0].knowledge_changes[0].phase = BoundaryPhase::DomainDeltaProposal;
    reject("batch phase", phase);

    let mut visibility = snapshot.clone();
    visibility.boundaries[0]
        .knowledge_changes
        .iter_mut()
        .find(|change| change.phase == BoundaryPhase::PerspectiveAndReportMaterialization)
        .expect("fixture phase-13 publication should exist")
        .visibility = StateVisibility::NextBoundary;
    reject("batch visibility", visibility);

    let mut batch_order = snapshot.clone();
    batch_order.boundaries[0].knowledge_changes.swap(0, 1);
    reject("batch order", batch_order);

    let mut first_id = snapshot.clone();
    first_id.boundaries[0].knowledge_changes[0].records[0].id = KnowledgeRecordId::new(99);
    reject("batch first record ID", first_id);

    let mut event_holder = snapshot.clone();
    let event = event_holder
        .events
        .iter_mut()
        .find(|event| event.kind.is_type("knowledge_published"))
        .expect("fixture publication event should exist");
    event
        .kind
        .set_field("holder", &KnowledgeHolderRef::Person(PersonId::new(2)))
        .expect("publication event should have a holder");
    reject("event holder", event_holder);

    let mut emission_index = snapshot.clone();
    let emission = emission_index.boundaries[0]
        .emissions
        .iter_mut()
        .find(|emission| matches!(emission.kind, BoundaryEmissionKind::KnowledgeChange { .. }))
        .expect("fixture knowledge emission should exist");
    emission.kind = BoundaryEmissionKind::KnowledgeChange { change_index: 99 };
    reject("knowledge emission index", emission_index);

    let mut counter_backward = snapshot.clone();
    counter_backward.next_knowledge_record_id = 3;
    reject("next record counter backward", counter_backward);

    let mut counter_forward = snapshot;
    counter_forward.next_knowledge_record_id = 99;
    reject("next record counter forward", counter_forward);
}

#[test]
fn operation_keyed_randomness_is_order_independent_idempotent_and_replayable() {
    let (baseline, baseline_sequential, baseline_run, baseline_scenario) =
        run_keyed_fixture(&["alpha", "beta"]);
    let (reordered, reordered_sequential, reordered_run, reordered_scenario) =
        run_keyed_fixture(&["noise", "beta", "alpha"]);
    assert_eq!(baseline["alpha"], reordered["alpha"]);
    assert_eq!(baseline["beta"], reordered["beta"]);
    assert_eq!(baseline_sequential, reordered_sequential);
    assert_eq!(baseline_run.random_draws().len(), 3);
    assert_eq!(reordered_run.random_draws().len(), 4);
    assert_eq!(
        baseline_run
            .state
            .current
            .random_streams
            .get(&keyed_fixture_stream())
            .expect("keyed stream exists")
            .position,
        1
    );

    Simulation::from_snapshot_with_plugins(baseline_run.snapshot(), &[&KeyedRandomPlugin])
        .expect("operation-keyed draws should restore");
    let replayed = Simulation::replay_from_journal_with_scenario(
        baseline_scenario,
        &[&KeyedRandomPlugin],
        &baseline_run.replay_journal(),
    )
    .expect("operation-keyed draws should replay exactly");
    assert_eq!(replayed.snapshot(), baseline_run.snapshot());
    let replayed_reordered = Simulation::replay_from_journal_with_scenario(
        reordered_scenario,
        &[&KeyedRandomPlugin],
        &reordered_run.replay_journal(),
    )
    .expect("reordered operation-keyed draws should replay exactly");
    assert_eq!(replayed_reordered.snapshot(), reordered_run.snapshot());
}

#[test]
fn later_boundary_failure_rolls_back_staged_keyed_draws_and_indexes() {
    let (scenario, _) = demo_scenario();
    let plugin = KeyedRollbackPlugin;
    let mut simulation =
        Simulation::new(89, scenario).expect("keyed rollback fixture should initialize");
    simulation
        .register_plugin(&plugin)
        .expect("keyed rollback plugin should register");
    simulation
        .enqueue_plugin_ingress(PluginIngressRequest::new(
            plugin.name(),
            "operation",
            SimTime::EPOCH,
            serde_json::json!({ "operation": "rollback-operation" }),
        ))
        .expect("rollback probe should enqueue");
    let before = simulation.snapshot();
    let error = simulation
        .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
        .expect_err("the undeclared later publication must reject the boundary");
    assert_eq!(error.code, ErrorCode::UndeclaredKnowledgeWrite);
    assert_eq!(simulation.snapshot(), before);
}

#[test]
fn operation_keyed_snapshot_tamper_matrix_is_recomputed_fail_closed() {
    let (_, _, simulation, _) = run_keyed_fixture(&["alpha", "beta"]);
    let snapshot = simulation.snapshot();
    let plugin = KeyedRandomPlugin;

    let reject = |label: &str, mut tampered: SimulationSnapshot| {
        rehash_tampered_snapshot(&mut tampered);
        let error = Simulation::from_snapshot_with_plugins(tampered, &[&plugin])
            .err()
            .unwrap_or_else(|| panic!("{label} tamper unexpectedly loaded"));
        assert_eq!(error.code, ErrorCode::InvalidSnapshot, "{label}");
    };
    let keyed_index = snapshot
        .random_draws
        .iter()
        .position(|draw| matches!(&draw.address, RandomDrawAddress::OperationV1(_)))
        .expect("fixture should contain a keyed draw");

    let mut address = snapshot.clone();
    let RandomDrawAddress::OperationV1(operation) = &mut address.random_draws[keyed_index].address
    else {
        unreachable!("selected draw is operation-keyed")
    };
    operation.application_operation_id.push_str("-tampered");
    reject("operation address", address);

    let mut evidence = snapshot.clone();
    evidence.random_draws[keyed_index].operation_evidence =
        Some(EvidenceRef::Ingress(IngressId::new(2)));
    reject("operation evidence", evidence);

    let mut bound = snapshot.clone();
    bound.random_draws[keyed_index].upper_exclusive += 1;
    reject("operation bound", bound);

    let mut purpose = snapshot.clone();
    purpose.random_draws[keyed_index]
        .purpose
        .push_str(" tampered");
    reject("operation purpose", purpose);

    let mut value = snapshot;
    value.random_draws[keyed_index].value = (value.random_draws[keyed_index].value + 1)
        % value.random_draws[keyed_index].upper_exclusive;
    reject("operation value", value);
}

#[test]
fn read_only_runs_reject_live_plugin_ingress_without_mutation() {
    let (scenario, _) = demo_scenario();
    let configuration = RunConfiguration::read_only_observer();
    let manifest = manifest_for_configuration(&scenario, &configuration);
    let plugin = CanonicalIngressPlugin;
    let mut simulation =
        Simulation::new_with_run_configuration(47, scenario, manifest, configuration)
            .expect("read-only ingress fixture should load");
    simulation
        .register_plugin(&plugin)
        .expect("read-only ingress plugin should register");
    let before = simulation.snapshot();
    let error = simulation
        .enqueue_plugin_ingress(PluginIngressRequest::new(
            "canonical-ingress",
            "report",
            SimTime::EPOCH,
            serde_json::json!({ "label": "unauthorized live report" }),
        ))
        .expect_err("read-only runs cannot accept newly authored plugin ingress");
    assert_eq!(error.code, ErrorCode::InteractionReadOnly);
    assert_eq!(simulation.snapshot(), before);

    simulation
        .append_ingress(
            SimTime::EPOCH,
            IngressClass::Information,
            0,
            IngressPayload::Plugin {
                plugin: "canonical-ingress".to_owned(),
                packet_type: "report".to_owned(),
                payload: serde_json::json!({ "label": "forged live report" }),
                affected_entities: Vec::new(),
                archive_retention: Vec::new(),
            },
            None,
            false,
        )
        .expect("the fixture should construct coherent but unauthorized evidence");
    let error = Simulation::from_snapshot_with_plugins(simulation.snapshot(), &[&plugin])
        .err()
        .expect("snapshot validation must reject impossible read-only live ingress");
    assert_eq!(error.code, ErrorCode::InvalidSnapshot);
}