liminal-server 0.25.0

Standalone server for the liminal messaging bus
Documentation
use super::*;

#[test]
fn default_limits_pass_validation_and_carry_signed_numbers()
-> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    // The signed §5 defaults resolve from an absent `[limits]` section.
    assert_eq!(config.limits.max_connections, 256);
    assert_eq!(config.limits.max_subscriptions_per_connection, 32);
    assert_eq!(config.limits.max_conversations_per_connection, 32);
    assert_eq!(config.limits.max_pending_pushes_per_connection, 32);
    assert_eq!(
        config
            .limits
            .max_pending_conversation_replies_per_connection,
        32
    );
    assert_eq!(config.limits.max_pending_replies_per_conversation, 8);
    assert_eq!(config.limits.max_connection_inbox_bytes, 4 * 1024 * 1024);
    // The outbound budget is the inbound one's twin and carries the same
    // number: a connection's two directions are bounded alike, and the
    // inbound field's own documentation has always said so. Asserted beside
    // it so moving one without the other fails here.
    assert_eq!(config.limits.max_connection_outbound_bytes, 4 * 1024 * 1024);
    assert_eq!(
        config.limits.max_connection_outbound_bytes, config.limits.max_connection_inbox_bytes,
        "the two per-connection byte budgets are deliberately equal"
    );
    // P0 #55 part 2: 4096, not the §5-era 256. The two caps that bound a
    // subscription inbox are asserted TOGETHER here because the ruling is
    // about their RATIO, not either number: the byte budget must be the one
    // that binds for realistic records, which requires
    // `max_connection_inbox_bytes / max_subscription_inbox_depth` to sit at or
    // below the record sizes real traffic carries. Changing either constant
    // without the other moves that crossover, so this pin fails on both.
    assert_eq!(config.limits.max_subscription_inbox_depth, 4096);
    assert_eq!(
        config.limits.max_connection_inbox_bytes / config.limits.max_subscription_inbox_depth,
        1024,
        "the byte-vs-count crossover must stay at 1 KiB per record"
    );
    assert_eq!(config.limits.delivery_slice_budget, 32);
    validate(&mut config, None)?;
    Ok(())
}

/// §5 cap-refusal (config half): every zero cap is a typed config validation
/// error — the unlimited-by-silence state §5 outlaws — reported by field name.
#[test]
fn zero_limits_are_typed_config_errors() -> Result<(), Box<dyn std::error::Error>> {
    type LimitMutator = (&'static str, fn(&mut ServerConfig));
    let mutators: [LimitMutator; 10] = [
        ("max_connections", |c| c.limits.max_connections = 0),
        ("max_subscriptions_per_connection", |c| {
            c.limits.max_subscriptions_per_connection = 0;
        }),
        ("max_conversations_per_connection", |c| {
            c.limits.max_conversations_per_connection = 0;
        }),
        ("max_pending_pushes_per_connection", |c| {
            c.limits.max_pending_pushes_per_connection = 0;
        }),
        ("max_pending_conversation_replies_per_connection", |c| {
            c.limits.max_pending_conversation_replies_per_connection = 0;
        }),
        ("max_pending_replies_per_conversation", |c| {
            c.limits.max_pending_replies_per_conversation = 0;
        }),
        ("max_connection_inbox_bytes", |c| {
            c.limits.max_connection_inbox_bytes = 0;
        }),
        ("max_connection_outbound_bytes", |c| {
            c.limits.max_connection_outbound_bytes = 0;
        }),
        ("max_subscription_inbox_depth", |c| {
            c.limits.max_subscription_inbox_depth = 0;
        }),
        ("delivery_slice_budget", |c| {
            c.limits.delivery_slice_budget = 0;
        }),
    ];
    for (field, mutate) in mutators {
        let mut config = sample_config()?;
        mutate(&mut config);
        let message = config_validation_message(validate(&mut config, None));
        assert!(
            message.contains(&format!("limits.{field}")),
            "zero {field} must report a typed limits.{field} error, got: {message}"
        );
        assert!(
            message.contains("greater than zero"),
            "the {field} refusal must say why: {message}"
        );
    }
    Ok(())
}

/// A complete participant section with deployment-plausible nonzero values.
const fn sample_participant() -> ParticipantConfig {
    ParticipantConfig {
        wire_frame_limit: 65_536,
        attach_receipt_ttl_ms: 60_000,
        receipt_provenance_ttl_ms: 600_000,
        live_receipt_server_report_threshold: 1_024,
        max_live_attach_receipts_per_participant: 8,
        receipt_provenance_server_report_threshold: 4_096,
        receipt_provenance_per_conversation_report_threshold: 256,
        max_receipt_provenance_per_participant: 64,
        max_retired_identity_slots_server: 1_024,
        identity_slots: 4,
        observer_recovery_max_entries: 64,
        max_semantic_conversations_per_connection: 32,
        max_ordinary_record_entries: 1,
        max_ordinary_record_bytes: 131_072,
        max_generated_marker_entries: 1,
        max_generated_marker_bytes: 4_096,
        mandatory_transaction_bound_entries: 4,
        mandatory_transaction_bound_bytes: 16_384,
        full_recovery_claim_entries: 4,
        full_recovery_claim_bytes: 16_384,
        retained_capacity_entries: 2_048,
        retained_capacity_bytes: 16_777_216,
        max_retained_record_rows: 1_024,
        pruning: None,
        closure_episode_churn_limit: 1_024,
    }
}

#[test]
fn valid_participant_section_passes_validation() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    config.participant = Some(sample_participant());
    let temp_dir = std::env::temp_dir().join(format!(
        "liminal-server-participant-validation-{}-{}",
        std::process::id(),
        NEXT_TEMP_DIR_ID.fetch_add(1, Ordering::Relaxed)
    ));
    fs::create_dir_all(&temp_dir)?;
    config.persistence_path = Some(temp_dir.clone());
    let result = validate(&mut config, None);
    fs::remove_dir_all(&temp_dir)?;
    assert!(result.is_ok(), "expected valid config, got {result:?}");
    Ok(())
}

#[test]
fn zero_participant_values_are_typed_config_errors() -> Result<(), Box<dyn std::error::Error>> {
    type ParticipantMutator = (&'static str, fn(&mut ParticipantConfig));
    let mutators: [ParticipantMutator; 12] = [
        ("wire_frame_limit", |p| p.wire_frame_limit = 0),
        ("attach_receipt_ttl_ms", |p| p.attach_receipt_ttl_ms = 0),
        ("receipt_provenance_ttl_ms", |p| {
            p.receipt_provenance_ttl_ms = 0;
        }),
        ("live_receipt_server_report_threshold", |p| {
            p.live_receipt_server_report_threshold = 0;
        }),
        ("max_live_attach_receipts_per_participant", |p| {
            p.max_live_attach_receipts_per_participant = 0;
        }),
        ("receipt_provenance_server_report_threshold", |p| {
            p.receipt_provenance_server_report_threshold = 0;
        }),
        (
            "receipt_provenance_per_conversation_report_threshold",
            |p| {
                p.receipt_provenance_per_conversation_report_threshold = 0;
            },
        ),
        ("max_receipt_provenance_per_participant", |p| {
            p.max_receipt_provenance_per_participant = 0;
        }),
        ("max_retired_identity_slots_server", |p| {
            p.max_retired_identity_slots_server = 0;
        }),
        ("identity_slots", |p| p.identity_slots = 0),
        ("observer_recovery_max_entries", |p| {
            p.observer_recovery_max_entries = 0;
        }),
        ("max_semantic_conversations_per_connection", |p| {
            p.max_semantic_conversations_per_connection = 0;
        }),
    ];
    for (field, mutate) in mutators {
        let mut config = sample_config()?;
        let mut participant = sample_participant();
        mutate(&mut participant);
        config.participant = Some(participant);
        let message = config_validation_message(validate(&mut config, None));
        assert!(
            message.contains(&format!("participant.{field}")),
            "expected typed error for participant.{field}, got: {message}"
        );
    }
    Ok(())
}

#[test]
fn provenance_ttl_shorter_than_receipt_is_a_typed_config_error()
-> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    let mut participant = sample_participant();
    participant.receipt_provenance_ttl_ms = participant.attach_receipt_ttl_ms - 1;
    config.participant = Some(participant);
    let message = config_validation_message(validate(&mut config, None));
    assert!(
        message.contains("participant.receipt_provenance_ttl_ms"),
        "expected TTL-ordering error, got: {message}"
    );
    Ok(())
}

#[test]
fn undersized_wire_frame_limit_is_a_typed_config_error() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    let mut participant = sample_participant();
    participant.wire_frame_limit = 1;
    config.participant = Some(participant);
    let message = config_validation_message(validate(&mut config, None));
    assert!(
        message.contains("participant.wire_frame_limit"),
        "expected minimum-frame error, got: {message}"
    );
    Ok(())
}

#[test]
fn worker_front_door_rejects_participant_section() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = worker_front_door_config()?;
    config.participant = Some(sample_participant());
    let message = config_validation_message(validate(&mut config, None));
    assert!(
        message.contains("installs no participant service"),
        "expected worker-front-door participant rejection, got: {message}"
    );
    Ok(())
}