bcode_workflow_store 0.0.1-alpha.0

Durable workflow persistence for bcode
//! Read-only maintenance backup verification behavior.
use super::{open_verified_backup, verify_reset_backup};
use switchy_database::Database;
use switchy_database::schema::{Column, DataType, create_table};
use switchy_database_connection::{SqliteAccess, SqliteOpenOptions, open_sqlite_rusqlite};

#[test]
fn missing_or_corrupt_backup_is_preserved_and_refused() {
    let directory = tempfile::tempdir().unwrap();
    let missing = directory.path().join("missing.db");
    assert!(verify_reset_backup(&missing).is_err());
    assert!(!missing.exists());

    let corrupt = directory.path().join("corrupt.db");
    let bytes = b"not a SQLite database";
    std::fs::write(&corrupt, bytes).unwrap();
    assert!(verify_reset_backup(&corrupt).is_err());
    assert_eq!(std::fs::read(&corrupt).unwrap(), bytes);
}

#[test]
fn valid_backup_is_verified_without_mutation_and_handle_is_read_only() {
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("backup.db");
    let options = SqliteOpenOptions {
        access: SqliteAccess::ReadWriteCreate,
        allow_uri: false,
        busy_timeout: std::time::Duration::from_secs(5),
        foreign_keys: false,
        connections: std::num::NonZeroUsize::MIN,
    };
    let writer = open_sqlite_rusqlite(&path, &options).unwrap();
    let transaction =
        super::database::begin(&writer, switchy_database::TransactionMode::Immediate).unwrap();
    futures::executor::block_on(
        create_table("backup_fixture")
            .column(Column {
                name: "identity".into(),
                data_type: DataType::Int,
                nullable: false,
                default: None,
                auto_increment: false,
            })
            .execute(&*transaction),
    )
    .unwrap();
    super::database::commit(transaction).unwrap();
    drop(writer);
    let before = std::fs::read(&path).unwrap();
    assert_eq!(
        verify_reset_backup(&path).unwrap(),
        super::sha256_hex(&before)
    );
    assert_eq!(std::fs::read(&path).unwrap(), before);

    let reader = open_verified_backup(&path).unwrap();
    assert!(
        futures::executor::block_on(
            reader
                .insert("backup_fixture")
                .value("identity", 1_i64)
                .execute(&reader)
        )
        .is_err()
    );
    let transaction =
        super::database::begin(&reader, switchy_database::TransactionMode::Deferred).unwrap();
    let rows = super::database::rows(
        &*transaction,
        &switchy_database::query::select("backup_fixture")
            .columns(&["identity"])
            .limit(1),
    )
    .unwrap();
    assert!(rows.is_empty());
    super::database::commit(transaction).unwrap();
    drop(reader);
    assert_eq!(std::fs::read(&path).unwrap(), before);
}

#[test]
fn structured_baseline_initialization_preserves_both_compatibility_contracts() {
    use switchy_database::query::FilterableQuery as _;
    for version in [42_u32, 43] {
        let directory = tempfile::tempdir().unwrap();
        let path = directory.path().join("workflow.db");
        let db = super::database::open(
            &path,
            SqliteAccess::ReadWriteCreate,
            std::time::Duration::ZERO,
            true,
        )
        .unwrap();
        super::schema::initialize(&db, version).unwrap();
        super::storage_compatibility::verify(&db).unwrap();
        let rows = super::database::rows(
            &db,
            &switchy_database::query::select("workflow_store_contract")
                .where_eq("contract_id", 1_i64)
                .limit(1),
        )
        .unwrap();
        assert_eq!(
            super::database::unsigned(&rows[0], "schema_version").unwrap(),
            u64::from(version)
        );
        let columns = futures::executor::block_on(db.get_table_columns("workflow_runs")).unwrap();
        assert_eq!(
            columns
                .iter()
                .find(|column| column.name == "concurrency_cap")
                .unwrap()
                .nullable,
            version == 43
        );
        let violations = futures::executor::block_on(db.sqlite_foreign_key_check()).unwrap();
        assert!(violations.is_empty());
    }
}

#[test]
fn typed_lock_errors_retain_retry_classification() {
    for error in [
        switchy_database::DatabaseError::Busy,
        switchy_database::DatabaseError::Locked,
    ] {
        assert!(
            super::WorkflowStoreError::StructuredDatabase(error)
                .is_transient_initialization_failure()
        );
    }
    assert!(
        !super::WorkflowStoreError::InvalidData("corrupt contract".into())
            .is_transient_initialization_failure()
    );
}