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");
}