agent: lumotia-rebrand — fix QC blockers for phase 5
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

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:
2026-05-13 09:43:45 +01:00
parent 86f83b7a45
commit 9336286e3c
3 changed files with 247 additions and 95 deletions

View File

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