slave-framework 0.13.1

Runtime for slave apps (a Laravel-inspired Rust framework for MCP-first apps): server lifecycle, config, logging, a SurrealDB database with SurrealQL migrations, and OpenID Connect sign-in
Documentation
//! The SurrealDB database and its migrations (see `slave_framework::database`).
//! Needs a server: `TEST_SURREAL_URL`, default `ws://127.0.0.1:8000`, with
//! root user `root`/`root` (`surreal start --user root --pass root`).

use std::path::PathBuf;

use slave_framework::database::{Db, is_already_exists, is_transaction_conflict, migrations};
use slave_framework::surrealdb::types::SurrealValue;
use slave_framework::testing::TestDatabase;

#[derive(Debug, SurrealValue)]
struct Note {
    id: i64,
    title: String,
}

/// A migrations folder of its own holding `files`.
fn migrations_dir(test: &str, files: &[(&str, &str)]) -> PathBuf {
    let dir = std::env::temp_dir().join(format!("slave-migrations-{}-{test}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    for (name, source) in files {
        std::fs::write(dir.join(name), source).unwrap();
    }
    dir
}

const NOTES: &str = "
DEFINE SEQUENCE note_id START 1;
DEFINE TABLE note SCHEMAFULL;
DEFINE FIELD title ON note TYPE string;
DEFINE INDEX note_title ON note FIELDS title UNIQUE;
";

async fn create_note(db: &Db, title: &str) -> Result<Note, slave_framework::surrealdb::Error> {
    let note: Option<Note> = db
        .query(
            "CREATE ONLY type::record('note', sequence::nextval('note_id')) SET title = $title
             RETURN *, record::id(id) AS id",
        )
        .bind(("title", title.to_owned()))
        .await?
        .check()?
        .take(0)?;
    Ok(note.expect("created note"))
}

#[tokio::test]
async fn migrations_apply_once_in_order() {
    let dir = migrations_dir(
        "order",
        &[
            ("20260101_000000_notes.surql", NOTES),
            (
                "20260102_000000_seed.surql",
                "CREATE note:100 SET title = 'seeded';",
            ),
            ("README.md", "not a migration"),
        ],
    );
    let test_db = TestDatabase::start();
    let db = test_db.connect().await;

    let applied = migrations::apply(&db, &dir).await.unwrap();
    assert_eq!(applied, ["20260101_000000_notes", "20260102_000000_seed"]);
    assert!(migrations::apply(&db, &dir).await.unwrap().is_empty());

    let status = migrations::status(&db, &dir).await.unwrap();
    assert!(status.iter().all(|(_, applied)| *applied));
}

#[tokio::test]
async fn a_failing_migration_changes_nothing() {
    let dir = migrations_dir(
        "failing",
        &[
            ("20260101_000000_notes.surql", NOTES),
            (
                "20260102_000000_broken.surql",
                "CREATE note:1 SET title = 'a'; CREATE note:2 SET title = 1;",
            ),
        ],
    );
    let test_db = TestDatabase::start();
    let db = test_db.connect().await;

    assert!(migrations::apply(&db, &dir).await.is_err());
    assert_eq!(
        migrations::applied(&db).await.unwrap(),
        ["20260101_000000_notes"]
    );
    let titles: Vec<String> = db
        .query("SELECT VALUE title FROM note")
        .await
        .unwrap()
        .take(0)
        .unwrap();
    assert!(titles.is_empty(), "{titles:?}");
}

#[tokio::test]
async fn reset_drops_everything() {
    let dir = migrations_dir("reset", &[("20260101_000000_notes.surql", NOTES)]);
    let test_db = TestDatabase::start();
    let db = test_db.migrate(&dir).await;
    create_note(&db, "gone").await.unwrap();

    migrations::reset(&db).await.unwrap();
    assert!(migrations::applied(&db).await.unwrap().is_empty());
    migrations::apply(&db, &dir).await.unwrap();
    let note = create_note(&db, "fresh").await.unwrap();
    assert_eq!(note.id, 1, "the sequence starts over");
}

#[tokio::test]
async fn unique_indexes_reject_duplicates() {
    let dir = migrations_dir("unique", &[("20260101_000000_notes.surql", NOTES)]);
    let test_db = TestDatabase::start();
    let db = test_db.migrate(&dir).await;

    let first = create_note(&db, "first").await.unwrap();
    assert_eq!(first.id, 1);
    let duplicate = create_note(&db, "first").await.unwrap_err();
    assert!(is_already_exists(&duplicate), "{duplicate:?}");
}

#[tokio::test]
async fn concurrent_writers_all_succeed() {
    let dir = migrations_dir("concurrent", &[("20260101_000000_notes.surql", NOTES)]);
    let test_db = TestDatabase::start();
    let db = test_db.migrate(&dir).await;

    let tasks: Vec<_> = (0..20)
        .map(|n| {
            let db = db.clone();
            tokio::spawn(async move { create_note(&db, &format!("note {n}")).await })
        })
        .collect();
    let mut ids = Vec::new();
    for task in tasks {
        ids.push(task.await.unwrap().unwrap().id);
    }
    ids.sort();
    ids.dedup();
    assert_eq!(ids.len(), 20, "ids are unique");
}

#[tokio::test]
async fn conflicting_transactions_are_reported_as_conflicts() {
    let dir = migrations_dir("conflict", &[("20260101_000000_notes.surql", NOTES)]);
    let test_db = TestDatabase::start();
    let db = test_db.migrate(&dir).await;
    db.query("CREATE note:1 SET title = 'shared'")
        .await
        .unwrap()
        .check()
        .unwrap();

    let first = db.clone().begin().await.unwrap();
    let second = db.clone().begin().await.unwrap();
    for (tx, title) in [(&first, "one"), (&second, "two")] {
        tx.query("UPDATE note:1 SET title = $title")
            .bind(("title", title))
            .await
            .unwrap()
            .check()
            .unwrap();
    }
    first.commit().await.unwrap();
    let err = second.commit().await.unwrap_err();
    assert!(is_transaction_conflict(&err), "{err:?}");
}