agent: lumotia-rebrand — fix QC blockers for phase 5
Phase 5 QC found two blockers + four advisories. All addressed: B1 (FATAL) — Migration error now aborts startup instead of silently continuing past it. Without this fix a transient EACCES / EXDEV / ENOSPC would log a warning, init_db would create a fresh empty lumotia dir, and the user would appear to lose their transcripts. B2 (FATAL) — Linux dot-home vs XDG mismatch. The old probe returned ~/.magnotia as legacy but the caller passed app_data_dir() as the new path — which could be $XDG_DATA_HOME/lumotia. fs::rename across filesystems would EXDEV-fail; even when it succeeded the user's storage convention silently changed. Refactored: legacy_and_target_paths() returns the (legacy, target) pair together. Dot-home legacy lands in ~/.lumotia; XDG-set legacy lands in $XDG_DATA_HOME/lumotia; XDG-default legacy lands in ~/.local/share/lumotia. macOS / Windows / non-tier-1 unchanged. migrate_legacy_data_dir() now takes no argument; src-tauri caller updated. A1 — Removed dead new_db.exists() check inside rename_db_file_if_present (unreachable: rename_db is called only AFTER the legacy dir was just renamed to the new path, so a stray lumotia.db there is impossible). A2 — Added 4 unit tests for migrate_legacy_setting_keys: lone-magnotia, both-present (orphan delete), no-magnotia, idempotent. A3 — migrate_legacy_setting_keys now returns (renamed, orphans_deleted). When both keys exist, the legacy magnotia row is DELETED in the same transaction (lumotia row is authoritative). A4 — Added dot-home convention regression test in paths::tests. cargo build --workspace passes. cargo test --workspace: 339 pass, 0 fail (up from 334 in the original Phase 5 commit; +5 new tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -908,11 +908,18 @@ pub async fn get_setting(pool: &SqlitePool, key: &str) -> Result<Option<String>>
|
||||
/// One-shot key rename for settings rows carried over from the magnotia era
|
||||
/// (`magnotia_preferences`, `magnotia_morning_triage_last_shown`, etc.).
|
||||
///
|
||||
/// Idempotent: rows already on the new key are left alone; rows on the old
|
||||
/// key are renamed only if the new key does not already exist. Returns the
|
||||
/// number of rows actually renamed so the caller can log on first launch.
|
||||
pub async fn migrate_legacy_setting_keys(pool: &SqlitePool) -> Result<u64> {
|
||||
let result = sqlx::query(
|
||||
/// Idempotent. Returns `(renamed, orphans_deleted)`:
|
||||
/// * `renamed` — rows where only the magnotia key existed; the row's key
|
||||
/// is updated to the lumotia equivalent.
|
||||
/// * `orphans_deleted` — rows where BOTH keys existed; the lumotia row is
|
||||
/// authoritative and the magnotia row is deleted to avoid silent debt.
|
||||
pub async fn migrate_legacy_setting_keys(pool: &SqlitePool) -> Result<(u64, u64)> {
|
||||
let mut tx = pool.begin().await.map_err(|source| Error::Query {
|
||||
operation: "migrate_legacy_setting_keys:begin".into(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let renamed = sqlx::query(
|
||||
"UPDATE settings \
|
||||
SET key = 'lumotia_' || substr(key, length('magnotia_') + 1) \
|
||||
WHERE key LIKE 'magnotia_%' \
|
||||
@@ -921,13 +928,36 @@ pub async fn migrate_legacy_setting_keys(pool: &SqlitePool) -> Result<u64> {
|
||||
WHERE dst.key = 'lumotia_' || substr(settings.key, length('magnotia_') + 1)\
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|source| Error::Query {
|
||||
operation: "migrate_legacy_setting_keys".into(),
|
||||
operation: "migrate_legacy_setting_keys:rename".into(),
|
||||
source,
|
||||
})?
|
||||
.rows_affected();
|
||||
|
||||
let deleted = sqlx::query(
|
||||
"DELETE FROM settings \
|
||||
WHERE key LIKE 'magnotia_%' \
|
||||
AND EXISTS (\
|
||||
SELECT 1 FROM settings AS dst \
|
||||
WHERE dst.key = 'lumotia_' || substr(settings.key, length('magnotia_') + 1)\
|
||||
)",
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|source| Error::Query {
|
||||
operation: "migrate_legacy_setting_keys:delete_orphans".into(),
|
||||
source,
|
||||
})?
|
||||
.rows_affected();
|
||||
|
||||
tx.commit().await.map_err(|source| Error::Query {
|
||||
operation: "migrate_legacy_setting_keys:commit".into(),
|
||||
source,
|
||||
})?;
|
||||
Ok(result.rows_affected())
|
||||
|
||||
Ok((renamed, deleted))
|
||||
}
|
||||
|
||||
// --- Row types ---
|
||||
@@ -2740,4 +2770,94 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(remaining, vec!["recent".to_string()]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn migrate_legacy_setting_keys_renames_lone_magnotia_rows() {
|
||||
let pool = test_pool().await;
|
||||
set_setting(&pool, "magnotia_preferences", "blob-A")
|
||||
.await
|
||||
.unwrap();
|
||||
set_setting(&pool, "magnotia_morning_triage_last_shown", "2026-05-12")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (renamed, deleted) = migrate_legacy_setting_keys(&pool).await.unwrap();
|
||||
|
||||
assert_eq!(renamed, 2);
|
||||
assert_eq!(deleted, 0);
|
||||
assert_eq!(
|
||||
get_setting(&pool, "lumotia_preferences")
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("blob-A")
|
||||
);
|
||||
assert_eq!(
|
||||
get_setting(&pool, "magnotia_preferences").await.unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
get_setting(&pool, "lumotia_morning_triage_last_shown")
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("2026-05-12")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn migrate_legacy_setting_keys_deletes_orphans_when_both_present() {
|
||||
let pool = test_pool().await;
|
||||
set_setting(&pool, "magnotia_preferences", "legacy-blob")
|
||||
.await
|
||||
.unwrap();
|
||||
set_setting(&pool, "lumotia_preferences", "current-blob")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (renamed, deleted) = migrate_legacy_setting_keys(&pool).await.unwrap();
|
||||
|
||||
assert_eq!(renamed, 0);
|
||||
assert_eq!(deleted, 1);
|
||||
assert_eq!(
|
||||
get_setting(&pool, "lumotia_preferences")
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("current-blob"),
|
||||
"lumotia row preserved verbatim"
|
||||
);
|
||||
assert_eq!(
|
||||
get_setting(&pool, "magnotia_preferences").await.unwrap(),
|
||||
None,
|
||||
"orphan magnotia row deleted"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn migrate_legacy_setting_keys_no_op_when_no_magnotia_rows() {
|
||||
let pool = test_pool().await;
|
||||
set_setting(&pool, "lumotia_preferences", "blob")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (renamed, deleted) = migrate_legacy_setting_keys(&pool).await.unwrap();
|
||||
|
||||
assert_eq!(renamed, 0);
|
||||
assert_eq!(deleted, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn migrate_legacy_setting_keys_is_idempotent() {
|
||||
let pool = test_pool().await;
|
||||
set_setting(&pool, "magnotia_preferences", "blob")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first = migrate_legacy_setting_keys(&pool).await.unwrap();
|
||||
let second = migrate_legacy_setting_keys(&pool).await.unwrap();
|
||||
|
||||
assert_eq!(first, (1, 0));
|
||||
assert_eq!(second, (0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user