use std::sync::{Arc, Mutex};
use futures_lite::io::Cursor;
use lix::{ANONYMOUS_ACCOUNT_ID, OpenPhase, OpenProgress, OpenProgressSink, Value, open_lix};
const V72_ACCOUNT_SNAPSHOT: &[u8] =
include_bytes!("fixtures/v72_account_without_profile_uri.lixsnap");
#[derive(Default)]
struct RecordingProgress {
events: Mutex<Vec<OpenProgress>>,
}
impl OpenProgressSink for RecordingProgress {
fn report(&self, progress: OpenProgress) {
self.events.lock().expect("progress events").push(progress);
}
}
impl RecordingProgress {
fn events(&self) -> Vec<OpenProgress> {
self.events.lock().expect("progress events").clone()
}
}
#[tokio::test]
async fn fresh_open_reports_initialization_without_migration() {
let progress = Arc::new(RecordingProgress::default());
let lix = open_lix()
.with_open_progress_sink(progress.clone())
.await
.expect("fresh repository should initialize and open");
assert_eq!(
lix.open_report().format,
lix::CURRENT_STORAGE_FORMAT_VERSION
);
assert!(lix.open_report().initialized);
assert_eq!(lix.open_report().migration, None);
assert_eq!(
progress
.events()
.iter()
.map(|event| event.phase)
.collect::<Vec<_>>(),
vec![
OpenPhase::Inspecting,
OpenPhase::Opening,
OpenPhase::Complete,
],
);
assert!(
progress
.events()
.iter()
.all(|event| event.from_format.is_none()),
"initialization is not a format migration",
);
}
#[tokio::test]
#[ignore = "released v72 snapshot predates authored row metadata"]
async fn migrates_profile_uri_and_persists_updates_across_cold_reopen() {
let progress = Arc::new(RecordingProgress::default());
let storage = lix::Memory::new();
let report = lix::migration::restore_and_migrate_repository(
storage.clone(),
Cursor::new(V72_ACCOUNT_SNAPSHOT),
)
.await
.expect("explicit historical migration");
assert_eq!(report.before.format, Some(72));
assert!(report.after.current);
let lix = open_lix()
.with_storage(storage)
.with_open_progress_sink(progress.clone())
.await
.unwrap();
assert!(lix.open_report().migration.is_none());
assert!(
progress
.events()
.iter()
.all(|event| !matches!(event.phase, OpenPhase::Migrating | OpenPhase::Validating))
);
let accounts = lix
.execute("SELECT id, profile_uri FROM lix_account ORDER BY id", &[])
.await
.expect("migrated account rows should expose profile_uri");
assert!(!accounts.rows().is_empty());
assert!(
accounts
.rows()
.iter()
.all(|row| matches!(row.values(), [Value::Text(_), Value::Null])),
"historical account rows must materialize the appended nullable column as NULL"
);
let profile_uri = "https://profiles.example/anonymous.json";
let updated = lix
.execute(
"UPDATE lix_account SET profile_uri = $1 WHERE id = $2",
&[
Value::Text(profile_uri.to_owned()),
Value::Text(ANONYMOUS_ACCOUNT_ID.to_owned()),
],
)
.await
.expect("profile_uri update should succeed");
assert_eq!(updated.rows_affected(), 1);
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 lix = open_lix()
.from_snapshot(Cursor::new(migrated))
.await
.expect("migrated repository should cold-open");
assert_eq!(
lix.open_report().format,
lix::CURRENT_STORAGE_FORMAT_VERSION
);
assert_eq!(lix.open_report().migration, None);
assert!(!lix.open_report().initialized);
let result = lix
.execute(
"SELECT profile_uri FROM lix_account WHERE id = $1",
&[Value::Text(ANONYMOUS_ACCOUNT_ID.to_owned())],
)
.await
.expect("profile_uri should remain queryable after cold reopen");
assert_eq!(
result.rows()[0].values(),
&[Value::Text(profile_uri.to_owned())]
);
}