store-sqlite 0.3.10

SQLite-backed MailboxStore, ClaimPort, MemoryPort, and EventStorePort adapters (bundled SQLite).
Documentation
use std::sync::Arc;
use std::thread;

use store_sqlite::SqliteEventStore;
use substrate_core::domain::TaskState;
use substrate_core::event_store_port::{replay_task_state, EventStorePort, TaskLifecycleEvent};
use uuid::Uuid;

fn make_store() -> SqliteEventStore<TaskLifecycleEvent> {
    SqliteEventStore::open_in_memory().expect("in-memory event store")
}

#[test]
fn append_n_events_load_returns_ordered_with_monotonic_seq() {
    let store = make_store();
    let id = Uuid::new_v4();

    store
        .append(
            id,
            0,
            &TaskLifecycleEvent::Created {
                prompt: "ship feature".into(),
                cwd: "/repo".into(),
            },
        )
        .unwrap();
    store
        .append(
            id,
            1,
            &TaskLifecycleEvent::Advanced {
                to: TaskState::Working,
            },
        )
        .unwrap();
    store
        .append(
            id,
            2,
            &TaskLifecycleEvent::Advanced {
                to: TaskState::Completed,
            },
        )
        .unwrap();

    let loaded = store.load(id).unwrap();
    assert_eq!(loaded.len(), 3);
    assert_eq!(loaded[0].aggregate_seq, 0);
    assert_eq!(loaded[1].aggregate_seq, 1);
    assert_eq!(loaded[2].aggregate_seq, 2);
    assert!(loaded[0].global_seq < loaded[1].global_seq);
    assert!(loaded[1].global_seq < loaded[2].global_seq);
}

#[test]
fn replay_folds_events_into_correct_final_state() {
    let store = make_store();
    let id = Uuid::new_v4();

    store
        .append(
            id,
            0,
            &TaskLifecycleEvent::Created {
                prompt: "replay me".into(),
                cwd: "/tmp".into(),
            },
        )
        .unwrap();
    store
        .append(
            id,
            1,
            &TaskLifecycleEvent::Advanced {
                to: TaskState::Working,
            },
        )
        .unwrap();
    store
        .append(
            id,
            2,
            &TaskLifecycleEvent::Advanced {
                to: TaskState::Completed,
            },
        )
        .unwrap();

    let state = replay_task_state(&store, id).unwrap();
    assert_eq!(state.prompt, "replay me");
    assert_eq!(state.cwd, "/tmp");
    assert_eq!(state.state, TaskState::Completed);
}

#[test]
fn duplicate_expected_seq_append_rejected() {
    let store = make_store();
    let id = Uuid::new_v4();

    store
        .append(
            id,
            0,
            &TaskLifecycleEvent::Created {
                prompt: "once".into(),
                cwd: ".".into(),
            },
        )
        .unwrap();

    let err = store
        .append(
            id,
            0,
            &TaskLifecycleEvent::Advanced {
                to: TaskState::Working,
            },
        )
        .unwrap_err();
    assert!(err.to_string().contains("duplicate event seq"));
}

#[test]
fn concurrent_appends_preserve_global_ordering() {
    let store = Arc::new(make_store());

    let handles: Vec<_> = (0..8)
        .map(|i| {
            let s = Arc::clone(&store);
            let aggregate_id = Uuid::new_v4();
            thread::spawn(move || {
                s.append(
                    aggregate_id,
                    0,
                    &TaskLifecycleEvent::Created {
                        prompt: format!("task-{i}"),
                        cwd: ".".into(),
                    },
                )
                .unwrap()
            })
        })
        .collect();

    let mut globals: Vec<u64> = handles
        .into_iter()
        .map(|h| h.join().unwrap().global_seq)
        .collect();
    globals.sort_unstable();
    globals.dedup();
    assert_eq!(globals.len(), 8, "global_seq values must be unique");

    let all = store.load_all_global().unwrap();
    assert_eq!(all.len(), 8);
    for window in all.windows(2) {
        assert!(window[0].global_seq < window[1].global_seq);
    }
}

#[test]
fn task_lifecycle_projection_unit() {
    use substrate_core::event_store_port::{replay, TaskLifecycleProjection};

    let events = vec![
        TaskLifecycleEvent::Created {
            prompt: "p".into(),
            cwd: "c".into(),
        },
        TaskLifecycleEvent::Advanced {
            to: TaskState::Working,
        },
        TaskLifecycleEvent::Advanced {
            to: TaskState::Failed,
        },
    ];
    let state = replay::<TaskLifecycleProjection>(&events);
    assert_eq!(state.state, TaskState::Failed);
}