fsqlite-core 0.3.0

Core engine: connection, prepare, schema, DDL/DML codegen
Documentation
//! frankensqlite#81 — strict multi-process refusal mode.
//!
//! These tests cover the infrastructure for the strict-multi-process
//! opt-in: the `ConnectionEnv` flag, the `Connection::open_strict_multi_process`
//! convenience constructor, and the `MultiProcessContractViolation` error
//! variant. Transaction lock admission is an enforced refusal site; open-time
//! WAL-checkpoint and freelist-integrity refusals remain separate work.

use fsqlite_core::connection::{Connection, ConnectionEnv};
use fsqlite_error::{ErrorCode, FrankenError};

#[test]
fn connection_env_strict_multi_process_defaults_off() {
    let env = ConnectionEnv::default();
    assert!(
        !env.strict_multi_process(),
        "default ConnectionEnv should leave strict_multi_process disabled to preserve existing best-effort behavior"
    );
}

#[test]
fn connection_env_strict_multi_process_round_trips() {
    let mut env = ConnectionEnv::default();
    env.set_strict_multi_process(true);
    assert!(
        env.strict_multi_process(),
        "after enable, flag should read true"
    );
    env.set_strict_multi_process(false);
    assert!(
        !env.strict_multi_process(),
        "after disable, flag should read false"
    );
}

#[test]
fn open_strict_multi_process_constructor_opens_a_usable_connection() {
    asupersync::test_utils::run_test(|| async {
        let conn = Connection::open_strict_multi_process(":memory:")
            .await
            .expect("strict multi-process constructor should open an in-memory database");
        conn.execute("CREATE TABLE strict_smoke (id INTEGER PRIMARY KEY)")
            .await
            .expect("strict multi-process connection should execute ordinary DDL");
    });
}

#[test]
fn multi_process_contract_violation_carries_detail() {
    let err = FrankenError::MultiProcessContractViolation {
        detail: "freelist trunk page 42 exceeds db_size 10".to_string(),
    };
    let msg = format!("{err}");
    assert!(
        msg.contains("multi-process contract violation"),
        "error display should mention the contract violation kind: {msg}"
    );
    assert!(
        msg.contains("freelist trunk page 42"),
        "error display should propagate the detail: {msg}"
    );
    assert_eq!(
        err.error_code(),
        ErrorCode::Busy,
        "strict multi-process refusal should preserve SQLite BUSY compatibility"
    );
}

#[test]
fn strict_multi_process_refuses_expired_transaction_lock_admission() {
    asupersync::test_utils::run_test(|| async {
        let dir = tempfile::tempdir().expect("create temp directory");
        let path = dir.path().join("strict-lock-admission.db");
        let path = path.to_string_lossy().into_owned();

        let holder = Connection::open(path.clone())
            .await
            .expect("open lock holder");
        holder
            .execute("CREATE TABLE guarded(value INTEGER NOT NULL);")
            .await
            .expect("initialize test database");
        let waiter = Connection::open_strict_multi_process(path)
            .await
            .expect("open strict waiter");

        holder
            .execute("BEGIN EXCLUSIVE;")
            .await
            .expect("holder acquires exclusive transaction lock");
        waiter
            .execute("PRAGMA busy_timeout = 0;")
            .await
            .expect("disable retries for deterministic refusal");
        let error = waiter
            .execute("BEGIN IMMEDIATE;")
            .await
            .expect_err("strict waiter must classify exhausted lock admission");

        assert!(
            matches!(error, FrankenError::MultiProcessContractViolation { .. }),
            "strict mode must surface a typed contract violation, got {error:?}"
        );
        holder
            .execute("ROLLBACK;")
            .await
            .expect("release holder lock");
    });
}