lix 0.18.1

Embeddable version control for apps and AI agents.
Documentation
//! Golden coverage for the exact repository format shipped immediately before
//! engine-owned storage epochs.
//!
//! `fixtures/v75_released_repository.lixsnap` is a `LIXSNAP` artifact converted
//! from state generated by an isolated clean checkout of `origin/main` at
//! `939901cc215b42ba7606432cee40088abb1b3671`. It contains divergent main and
//! draft histories, three explicit checkpoints, JSON key-value rows, filesystem
//! directory/file rows, and a 65,537-byte content-addressed blob.
//! SHA-256: `1072330958f35668cd988ff5e8ec5ac8d45c7a784af448c32a3daf99cfc00a7c`.

use futures_lite::io::Cursor;
use lix::{Lix, Memory, SwitchBranchOptions, Value, open_lix};

const V75_RELEASED_SNAPSHOT: &[u8] = include_bytes!("fixtures/v75_released_repository.lixsnap");
const DRAFT_BRANCH_ID: &str = "01930000-0000-7000-8000-000000000075";

fn released_blob() -> Vec<u8> {
    (0..65_537)
        .map(|index| ((index * 31 + index / 251) % 256) as u8)
        .collect()
}

async fn assert_json_value(lix: &Lix<Memory>, key: &str, expected: serde_json::Value) {
    let result = lix
        .execute(
            "SELECT value FROM lix_key_value WHERE key = $1",
            &[Value::Text(key.to_string())],
        )
        .await
        .unwrap_or_else(|error| panic!("key-value read for {key} failed: {error:?}"));
    assert_eq!(result.rows().len(), 1, "expected one row for {key}");
    assert_eq!(
        result.rows()[0]
            .get::<serde_json::Value>("value")
            .expect("fixture value should decode as JSON"),
        expected,
        "unexpected value for {key}",
    );
}

async fn assert_key_absent(lix: &Lix<Memory>, key: &str) {
    let result = lix
        .execute(
            "SELECT value FROM lix_key_value WHERE key = $1",
            &[Value::Text(key.to_string())],
        )
        .await
        .unwrap_or_else(|error| panic!("key-value absence read for {key} failed: {error:?}"));
    assert!(result.rows().is_empty(), "{key} should be absent");
}

async fn assert_file(lix: &Lix<Memory>, path: &str, expected: Vec<u8>) {
    let result = lix
        .execute(
            "SELECT content FROM lix_file WHERE path = $1",
            &[Value::Text(path.to_string())],
        )
        .await
        .unwrap_or_else(|error| panic!("file read for {path} failed: {error:?}"));
    assert_eq!(result.rows().len(), 1, "expected one file at {path}");
    assert_eq!(
        result.rows()[0].values(),
        &[Value::Blob(expected.into())],
        "unexpected content at {path}",
    );
}

async fn assert_file_absent(lix: &Lix<Memory>, path: &str) {
    let result = lix
        .execute(
            "SELECT content FROM lix_file WHERE path = $1",
            &[Value::Text(path.to_string())],
        )
        .await
        .unwrap_or_else(|error| panic!("file absence read for {path} failed: {error:?}"));
    assert!(result.rows().is_empty(), "{path} should be absent");
}

async fn assert_main_state(lix: &Lix<Memory>) {
    assert_json_value(
        lix,
        "fixture-shared",
        serde_json::json!({ "generation": 75, "lane": "main" }),
    )
    .await;
    assert_json_value(
        lix,
        "fixture-array",
        serde_json::json!(["released", 75, true, null]),
    )
    .await;
    assert_json_value(
        lix,
        "fixture-main-after-branch",
        serde_json::json!({ "present": true }),
    )
    .await;
    assert_file(lix, "/docs/released-v75.bin", released_blob()).await;
    assert_file_absent(lix, "/draft/only.txt").await;
}

async fn assert_draft_state(lix: &Lix<Memory>) {
    assert_json_value(
        lix,
        "fixture-shared",
        serde_json::json!({ "generation": 75, "lane": "draft" }),
    )
    .await;
    assert_key_absent(lix, "fixture-main-after-branch").await;
    assert_file(lix, "/docs/released-v75.bin", released_blob()).await;
    assert_file(lix, "/draft/only.txt", b"draft-only-v75".to_vec()).await;
}

#[tokio::test]
async fn released_v75_repository_explicitly_migrates_with_semantics_intact() {
    let storage = Memory::new();
    let migration = lix::migration::restore_and_migrate_repository(
        storage.clone(),
        Cursor::new(V75_RELEASED_SNAPSHOT),
    )
    .await
    .expect("explicit v75 migration");
    assert_eq!(migration.before.format, Some(75));
    assert_eq!(
        migration.after.format,
        Some(lix::CURRENT_STORAGE_FORMAT_VERSION)
    );
    let lix = open_lix()
        .with_storage(storage)
        .await
        .expect("open migrated current repository");
    assert!(lix.open_report().migration.is_none());

    let main_branch_id = lix
        .active_branch_id()
        .await
        .expect("main branch id should load");
    assert_main_state(&lix).await;

    let draft = lix
        .execute(
            "SELECT name FROM lix_branch WHERE id = $1",
            &[Value::Text(DRAFT_BRANCH_ID.to_string())],
        )
        .await
        .expect("draft descriptor should remain queryable");
    assert_eq!(draft.rows().len(), 1);
    assert_eq!(
        draft.rows()[0].values(),
        &[Value::Text("Released v75 draft".to_string())]
    );

    lix.switch_branch(SwitchBranchOptions {
        branch_id: DRAFT_BRANCH_ID.to_string(),
    })
    .await
    .expect("migrated draft should open");
    assert_draft_state(&lix).await;

    lix.switch_branch(SwitchBranchOptions {
        branch_id: main_branch_id,
    })
    .await
    .expect("migrated main should reopen");
    lix.execute(
        "INSERT INTO lix_key_value (key, value) VALUES \
         ('fixture-post-upgrade', CAST('{\"writable\":true}' AS JSONB))",
        &[],
    )
    .await
    .expect("migrated repository should remain writable");
    let mut migrated = Vec::new();
    lix.export_snapshot()
        .write_to(&mut migrated)
        .await
        .expect("migrated Lix should export");
    lix.close().await.expect("migrated Lix should close");
    let reopened = open_lix()
        .from_snapshot(Cursor::new(migrated))
        .await
        .expect("migrated repository should cold-open");
    assert_eq!(
        reopened.open_report().format,
        lix::CURRENT_STORAGE_FORMAT_VERSION
    );
    assert!(!reopened.open_report().initialized);
    assert_eq!(reopened.open_report().migration, None);

    assert_main_state(&reopened).await;
    assert_json_value(
        &reopened,
        "fixture-post-upgrade",
        serde_json::json!({ "writable": true }),
    )
    .await;
    reopened
        .switch_branch(SwitchBranchOptions {
            branch_id: DRAFT_BRANCH_ID.to_string(),
        })
        .await
        .expect("draft should survive cold reopen");
    assert_draft_state(&reopened).await;
    reopened
        .close()
        .await
        .expect("cold-reopened repository should close");
}