frame-conv 0.2.0

Conversation patterns — request-response, subscription, pub/sub, and workflow over liminal
Documentation
//! Live-server support for the F-3a acceptance battery: the F-0c §R2
//! minimal-boot recipe as executable code (seeded from the spike harness at
//! `examples/spike-liminal-conversation/tests/support/mod.rs` — obtained,
//! not re-derived), scratch-dir management without extra dependencies, and
//! a file-backed resume store.

// Shared support compiled independently into every test binary; each binary
// uses a different subset, so per-binary dead-code is structural here.
#![allow(clippy::expect_used, clippy::unwrap_used, dead_code)]

pub mod embedded;
pub mod raw;

use std::error::Error;
use std::fs;
use std::io::Write as _;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;

use frame_conv::{BusAttachment, ResumeStore, StoreError};
use liminal_server::config::types::ParticipantConfig;
use liminal_server::config::{ChannelDef, LimitsConfig, ServerConfig, ServicesConfig};
use liminal_server::server::connection::ConnectionSupervisor;
use liminal_server::server::listener::ServerListener;

/// The substrate's hardcoded receive IO quantum at published liminal 0.3.0
/// (upstream ASK-2 — F-0c-FINDINGS assertion 8). Tests budget silence
/// windows in units of this quantum.
pub const QUANTUM: Duration = Duration::from_secs(5);

/// Values copied verbatim from liminal-server's own
/// `test_participant_config()` (published 0.3.0,
/// `src/server/participant/production/tests.rs:32-59`) — the
/// no-assumed-defaults rule for a test fixture, exactly as the F-0c spike
/// records.
#[must_use]
pub const fn participant_config() -> ParticipantConfig {
    ParticipantConfig {
        wire_frame_limit: 65_536,
        attach_receipt_ttl_ms: 60_000,
        receipt_provenance_ttl_ms: 600_000,
        max_live_attach_receipts_server: 1_024,
        max_live_attach_receipts_per_participant: 8,
        max_receipt_provenance_server: 4_096,
        max_receipt_provenance_per_conversation: 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,
        closure_episode_churn_limit: 1_024,
    }
}

fn reserve_loopback_port() -> Result<SocketAddr, Box<dyn Error>> {
    let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
    let address = listener.local_addr()?;
    drop(listener);
    Ok(address)
}

/// A per-test scratch directory under the OS temp root; removed on
/// [`RunningServer::shutdown`].
fn scratch_dir(label: &str) -> Result<PathBuf, Box<dyn Error>> {
    let dir = std::env::temp_dir().join(format!(
        "frame-conv-{label}-{}",
        uuid::Uuid::new_v4().simple()
    ));
    fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// One live in-process liminal-server booted from the published artifact
/// pinned in Cargo.toml — 0.3.3, the teardown-delivery-loss fix release
/// on the beamr-0.16.0 line (the F-0c §R2 recipe, characterized at 0.3.0:
/// config-section activation, real persistence path, supervisor +
/// listener — two calls).
pub struct RunningServer {
    listener: Option<ServerListener>,
    address: SocketAddr,
    scratch: PathBuf,
}

impl RunningServer {
    /// Boots the server on an ephemeral loopback port with durable
    /// participant persistence in a per-test scratch directory.
    pub fn start() -> Result<Self, Box<dyn Error>> {
        let scratch = scratch_dir("server")?;
        // liminal-server 0.3.2 durability hardening: the server creates only
        // the store's leaf directory and refuses a persistence path whose
        // parent does not already exist (0.3.1 created the full chain). The
        // fixture pre-creates the configured path — the "existing long-lived
        // location" the refusal message asks for, scratch-scoped per test.
        std::fs::create_dir_all(scratch.join("durability"))?;
        let config = ServerConfig {
            listen_address: "127.0.0.1:0".parse()?,
            health_listen_address: reserve_loopback_port()?,
            channels: vec![ChannelDef {
                name: "events".to_owned(),
                schema_ref: None,
                durable: false,
                loaded_schema: None,
            }],
            routing_rules: Vec::new(),
            persistence_path: Some(scratch.join("durability")),
            cluster: None,
            auth: None,
            drain_timeout_ms: 30_000,
            services: ServicesConfig::default(),
            limits: LimitsConfig::default(),
            participant: Some(participant_config()),
            websocket: None,
        };
        let supervisor = ConnectionSupervisor::from_config(&config)?;
        let listener = ServerListener::bind(&config, supervisor)?;
        let address = listener.local_addr();
        Ok(Self {
            listener: Some(listener),
            address,
            scratch,
        })
    }

    /// The server's live listen address, as an opaque bus endpoint string.
    #[must_use]
    pub fn endpoint(&self) -> String {
        self.address.to_string()
    }

    /// Orderly shutdown plus scratch removal; every test path calls this
    /// explicitly and reads the result.
    pub fn shutdown(mut self) -> Result<(), Box<dyn Error>> {
        if let Some(listener) = self.listener.take() {
            listener.shutdown()?;
        }
        fs::remove_dir_all(&self.scratch)?;
        Ok(())
    }
}

/// Declared bus attachment for the battery. Pool values (1, 10, 16) are
/// copied from the F-0c recipe (spike `remote_config`), the answer window
/// is this suite's declared bound on correlated-answer waits.
#[must_use]
pub fn attachment(endpoint: String) -> BusAttachment {
    BusAttachment {
        endpoint,
        session_capacity: 1,
        operation_window_millis: 10,
        inbound_buffer: 16,
        answer_window: Duration::from_secs(30),
    }
}

/// File-backed resume store: real bytes on a real disk so resume evidence
/// is observed, not mocked (the caller owns persistence — F-0c §R4).
pub struct FileStore {
    path: PathBuf,
}

impl FileStore {
    #[must_use]
    pub const fn new(path: PathBuf) -> Self {
        Self { path }
    }
}

impl ResumeStore for FileStore {
    fn persist(&mut self, canonical_state: &[u8]) -> Result<(), StoreError> {
        let mut file = fs::File::create(&self.path)
            .map_err(|error| StoreError::new(format!("create {}: {error}", self.path.display())))?;
        file.write_all(canonical_state)
            .and_then(|()| file.sync_all())
            .map_err(|error| StoreError::new(format!("write {}: {error}", self.path.display())))
    }
}

/// A scratch directory for participant resume stores, removed by the caller.
pub fn store_dir(label: &str) -> Result<PathBuf, Box<dyn Error>> {
    scratch_dir(label)
}