kaizen-cli 0.1.42

Distributable agent observability: real-time-tailable sessions, agile-style retros, and repo-level improvement (Cursor, Claude Code, Codex). SQLite, redact before any sync you enable.
Documentation
use super::super::Store;
use super::outbox_support::{
    create_legacy, legacy_path, migrated_path, migration_marker, pending_rows, seed_sqlite,
};
use tempfile::TempDir;

#[test]
fn migration_orders_legacy_before_reconciled_sqlite_rows() {
    let dir = TempDir::new().unwrap();
    let db = dir.path().join("kaizen.db");
    let store = Store::open(&db).unwrap();
    seed_sqlite(
        &store,
        &[
            ("event-owner", "events", "duplicate"),
            ("sqlite-only", "events", "sqlite-event"),
            ("replace", "tool_spans", "stale-span"),
            ("snapshot", "repo_snapshots", "stale-snapshot"),
            ("workspace", "workspace_facts", "stale-facts"),
            ("sqlite-replace", "tool_spans", "sqlite-span"),
            ("unknown", "custom_kind", "duplicate-custom"),
            ("event-owner", "events", "duplicate"),
            ("event-owner", "events", "duplicate"),
        ],
    );
    drop(store);
    drop(
        create_legacy(
            dir.path(),
            &[
                ("event-owner", "events", "duplicate"),
                ("replace", "tool_spans", "legacy-span-a"),
                ("snapshot", "repo_snapshots", "legacy-snapshot"),
                ("unknown", "custom_kind", "duplicate-custom"),
                ("workspace", "workspace_facts", "legacy-facts"),
                ("event-owner", "events", "duplicate"),
                ("replace", "tool_spans", "legacy-span-b"),
            ],
        )
        .unwrap(),
    );

    let store = Store::open(&db).unwrap();
    assert_eq!(
        pending_rows(&store),
        vec![
            row("event-owner", "events", "duplicate"),
            row("replace", "tool_spans", "legacy-span-a"),
            row("snapshot", "repo_snapshots", "legacy-snapshot"),
            row("unknown", "custom_kind", "duplicate-custom"),
            row("workspace", "workspace_facts", "legacy-facts"),
            row("event-owner", "events", "duplicate"),
            row("replace", "tool_spans", "legacy-span-b"),
            row("sqlite-only", "events", "sqlite-event"),
            row("sqlite-replace", "tool_spans", "sqlite-span"),
            row("event-owner", "events", "duplicate"),
        ]
    );
    assert!(migration_marker(&store).is_some());
    assert!(!legacy_path(dir.path()).exists());
    assert!(migrated_path(dir.path()).exists());
}

#[test]
fn committed_migration_retries_rename_without_duplicating_rows() {
    let dir = TempDir::new().unwrap();
    let db = dir.path().join("kaizen.db");
    let store = Store::open(&db).unwrap();
    seed_sqlite(&store, &[("sqlite", "events", "sqlite-row")]);
    drop(store);
    drop(create_legacy(dir.path(), &[("legacy", "events", "legacy-row")]).unwrap());
    std::fs::create_dir(migrated_path(dir.path())).unwrap();

    assert!(Store::open(&db).is_err());
    let store = Store::open_read_only(&db).unwrap();
    assert_eq!(
        pending_rows(&store),
        vec![
            row("legacy", "events", "legacy-row"),
            row("sqlite", "events", "sqlite-row"),
        ]
    );
    assert!(migration_marker(&store).is_some());
    drop(store);

    std::fs::remove_dir(migrated_path(dir.path())).unwrap();
    let store = Store::open(&db).unwrap();
    assert_eq!(
        pending_rows(&store),
        vec![
            row("legacy", "events", "legacy-row"),
            row("sqlite", "events", "sqlite-row"),
        ]
    );
    assert!(migrated_path(dir.path()).is_file());
}

#[test]
fn empty_legacy_outbox_preserves_sqlite_row_ids() {
    let dir = TempDir::new().unwrap();
    let db = dir.path().join("kaizen.db");
    let store = Store::open(&db).unwrap();
    seed_sqlite(&store, &[("sqlite", "events", "sqlite-row")]);
    let id = store.list_outbox_pending(1).unwrap()[0].0;
    drop(store);
    drop(create_legacy(dir.path(), &[]).unwrap());

    let store = Store::open(&db).unwrap();
    assert_eq!(store.list_outbox_pending(1).unwrap()[0].0, id);
    assert!(migration_marker(&store).is_some());
    assert!(migrated_path(dir.path()).is_file());
}

fn row(owner: &str, kind: &str, payload: &str) -> (String, String, String) {
    (owner.into(), kind.into(), payload.into())
}