telltale-machine 17.0.0

Protocol machine for choreographic session type protocols
Documentation
#![allow(missing_docs)]
//! Flow-policy serialization compatibility tests.

use cfg_if::cfg_if;
use std::collections::BTreeSet;

use telltale_machine::{FlowPolicy, FlowPredicate, ProtocolMachineConfig};

fn role_set(values: &[&str]) -> BTreeSet<String> {
    values.iter().map(|v| (*v).to_string()).collect()
}

#[test]
fn flow_policy_json_roundtrip_for_serializable_variants() {
    let cases = vec![
        FlowPolicy::AllowAll,
        FlowPolicy::DenyAll,
        FlowPolicy::AllowRoles(role_set(&["Observer", "Auditor"])),
        FlowPolicy::DenyRoles(role_set(&["Blocked"])),
        FlowPolicy::PredicateExpr(FlowPredicate::All(vec![
            FlowPredicate::TargetRolePrefix("Obs".to_string()),
            FlowPredicate::Any(vec![
                FlowPredicate::FactContains("secret".to_string()),
                FlowPredicate::EndpointRoleMatchesTarget,
            ]),
        ])),
    ];

    for policy in cases {
        let encoded = serde_json::to_string(&policy).expect("serialize flow policy to JSON");
        let decoded: FlowPolicy =
            serde_json::from_str(&encoded).expect("deserialize flow policy from JSON");
        assert_eq!(
            decoded, policy,
            "roundtrip mismatch for JSON payload: {encoded}"
        );
    }
}

#[test]
fn flow_policy_yaml_roundtrip_for_serializable_variants() {
    let cases = vec![
        FlowPolicy::AllowAll,
        FlowPolicy::DenyRoles(role_set(&["Observer"])),
        FlowPolicy::PredicateExpr(FlowPredicate::FactContains("classified".to_string())),
    ];

    for policy in cases {
        // serde_yaml cannot directly serialize nested externally-tagged enums.
        // Roundtrip through JSON Value preserves the canonical config shape.
        let json_value =
            serde_json::to_value(&policy).expect("serialize flow policy to JSON value");
        let encoded =
            serde_yaml::to_string(&json_value).expect("serialize flow policy value to YAML");
        let yaml_as_json: serde_json::Value =
            serde_yaml::from_str(&encoded).expect("deserialize YAML into JSON value");
        let decoded: FlowPolicy =
            serde_json::from_value(yaml_as_json).expect("deserialize flow policy from JSON value");
        assert_eq!(
            decoded, policy,
            "roundtrip mismatch for YAML payload: {encoded}"
        );
    }
}

#[test]
fn dynamic_flow_predicate_is_not_serializable() {
    let policy = FlowPolicy::predicate(|knowledge, target| {
        knowledge.fact.contains("secret") && target.starts_with("Obs")
    });

    let err = serde_json::to_string(&policy).expect_err("closure flow policy must not serialize");
    assert!(
        err.to_string()
            .contains("runtime closure predicate is not serializable"),
        "unexpected serde error: {err}"
    );
}

#[test]
fn protocol_machine_config_with_dynamic_flow_predicate_is_not_serializable() {
    let cfg = ProtocolMachineConfig {
        flow_policy: FlowPolicy::predicate(|knowledge, target| {
            knowledge.fact.contains("secret") && target.starts_with("Obs")
        }),
        ..ProtocolMachineConfig::default()
    };

    let err = serde_json::to_string(&cfg)
        .expect_err("config with closure flow policy must not serialize");
    assert!(
        err.to_string()
            .contains("runtime closure predicate is not serializable"),
        "unexpected serde error: {err}"
    );
}

#[test]
fn protocol_machine_config_schema_version_defaults_when_missing() {
    let mut encoded = serde_json::to_value(ProtocolMachineConfig::default())
        .expect("serialize default ProtocolMachine config");
    let obj = encoded
        .as_object_mut()
        .expect("ProtocolMachine config JSON value should be an object");
    obj.remove("config_schema_version");

    let decoded: ProtocolMachineConfig = serde_json::from_value(encoded)
        .expect("deserialize ProtocolMachine config without schema version");
    assert_eq!(decoded.config_schema_version, 1);
}

#[test]
fn protocol_machine_config_optional_hooks_have_deterministic_defaults() {
    let mut encoded = serde_json::to_value(ProtocolMachineConfig::default())
        .expect("serialize default ProtocolMachine config");
    let obj = encoded
        .as_object_mut()
        .expect("ProtocolMachine config JSON value should be an object");
    obj.remove("monitor_mode");
    obj.remove("flow_policy");
    obj.remove("instruction_cost");
    obj.remove("initial_cost_budget");
    obj.remove("payload_validation_mode");
    obj.remove("max_payload_bytes");
    obj.remove("config_schema_version");

    let decoded: ProtocolMachineConfig = serde_json::from_value(encoded)
        .expect("deserialize ProtocolMachine config without optional hooks");
    let defaults = ProtocolMachineConfig::default();
    assert_eq!(decoded.monitor_mode, defaults.monitor_mode);
    assert_eq!(decoded.flow_policy, defaults.flow_policy);
    assert_eq!(decoded.instruction_cost, defaults.instruction_cost);
    assert_eq!(decoded.initial_cost_budget, defaults.initial_cost_budget);
    assert_eq!(
        decoded.payload_validation_mode,
        defaults.payload_validation_mode
    );
    assert_eq!(decoded.max_payload_bytes, defaults.max_payload_bytes);
    assert_eq!(
        decoded.config_schema_version,
        defaults.config_schema_version
    );
}

#[test]
fn named_strict_profiles_encode_explicit_runtime_modes() {
    let minimal = ProtocolMachineConfig::strict_minimal();
    let observable = ProtocolMachineConfig::strict_observable();
    let verified = ProtocolMachineConfig::strict_verified();

    assert_eq!(
        minimal.determinism_mode,
        telltale_machine::DeterminismMode::Full
    );
    assert_eq!(
        minimal.threaded_round_semantics,
        telltale_machine::ThreadedRoundSemantics::CanonicalOneStep
    );
    assert_eq!(
        minimal.effect_trace_capture_mode,
        telltale_machine::EffectTraceCaptureMode::Disabled
    );
    assert_eq!(
        verified.communication_replay_mode,
        telltale_machine::CommunicationReplayMode::Nullifier
    );
    assert_eq!(
        verified.payload_validation_mode,
        telltale_machine::PayloadValidationMode::StrictSchema
    );
    assert_eq!(
        observable.effect_trace_capture_mode,
        telltale_machine::EffectTraceCaptureMode::Full
    );
}

#[test]
#[should_panic(expected = "max_sessions must be > 0")]
fn protocol_machine_new_rejects_invalid_config() {
    let cfg = ProtocolMachineConfig {
        max_sessions: 0,
        ..ProtocolMachineConfig::default()
    };
    drop(telltale_machine::ProtocolMachine::new(cfg));
}

cfg_if! {
    if #[cfg(feature = "multi-thread")] {
        #[test]
        #[should_panic(expected = "instruction_cost must be > 0")]
        fn threaded_vm_rejects_invalid_config() {
            let cfg = ProtocolMachineConfig {
                instruction_cost: 0,
                ..ProtocolMachineConfig::default()
            };
            drop(telltale_machine::ThreadedProtocolMachine::with_workers(cfg, 2));
        }
    }
}