agent: lumotia-rebrand — data dir migration shim + paths.rs rename
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 of the rebrand cascade per locked decision D1 (migrate in place).

crates/core/src/paths.rs:
- Hardcoded subdir strings renamed: magnotia/Magnotia -> lumotia/Lumotia
  across all four OS branches (Linux XDG + dot-legacy, macOS Application
  Support, Windows LOCALAPPDATA, fallback dot-dir).
- Database filename: magnotia.db -> lumotia.db.
- Test path fixtures renamed: /tmp/magnotia-test -> /tmp/lumotia-test.
- New MigrationStatus enum (Migrated / TargetAlreadyExists / NoLegacyFound).
- New migrate_legacy_data_dir() that probes the platform-correct legacy
  magnotia path, renames it to the lumotia equivalent via fs::rename, and
  also renames magnotia.db -> lumotia.db inside if found. Idempotent: safe
  to call on every boot. Refuses to overwrite an existing lumotia dir to
  protect user data.
- Four new unit tests covering all branches via the test-friendly
  migrate_legacy_data_dir_inner that takes an explicit legacy path.

crates/storage/src/database.rs:
- New migrate_legacy_setting_keys(pool) that renames any settings rows
  with key matching magnotia_* to lumotia_*. Single SQL UPDATE with
  NOT EXISTS guard so it leaves rows alone if a lumotia_ row already
  exists. Re-exported from lib.rs.

src-tauri/src/lib.rs:
- Calls migrate_legacy_data_dir(&app_data_dir()) at the start of setup()
  BEFORE database_path() resolves the now-renamed dir. Logs migration
  outcome to lumotia_startup tracing target.
- Calls migrate_legacy_setting_keys(&db) immediately after init_db
  returns. Logs only when rows are actually renamed.

src-tauri/src/{lib,commands/{diagnostics,rituals,transcripts}}.rs +
crates/storage/src/database.rs:
- All in-code references to magnotia_preferences, magnotia_history (in
  comments), and magnotia_morning_triage_last_shown renamed to lumotia_*.

cargo build --workspace passes. cargo test --workspace: 334 pass, 0 fail
(up from 330; +4 paths::tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 09:21:45 +01:00
parent e2a5feb718
commit 86f83b7a45
7 changed files with 335 additions and 26 deletions

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use crate::types::ModelId;
@@ -19,7 +19,7 @@ impl AppPaths {
}
pub fn database_path(&self) -> PathBuf {
self.app_data_dir.join("magnotia.db")
self.app_data_dir.join("lumotia.db")
}
pub fn recordings_dir(&self) -> PathBuf {
@@ -67,7 +67,7 @@ fn resolve_app_data_dir() -> PathBuf {
#[cfg(target_os = "windows")]
{
let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| ".".to_string());
return PathBuf::from(local_app_data).join("magnotia");
return PathBuf::from(local_app_data).join("lumotia");
}
#[cfg(target_os = "macos")]
@@ -76,56 +76,293 @@ fn resolve_app_data_dir() -> PathBuf {
return PathBuf::from(home)
.join("Library")
.join("Application Support")
.join("Magnotia");
.join("Lumotia");
}
#[cfg(target_os = "linux")]
{
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
let legacy = PathBuf::from(&home).join(".magnotia");
if legacy.exists() {
return legacy;
let legacy_dot = PathBuf::from(&home).join(".lumotia");
if legacy_dot.exists() {
return legacy_dot;
}
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
if !xdg.is_empty() {
return PathBuf::from(xdg).join("magnotia");
return PathBuf::from(xdg).join("lumotia");
}
}
PathBuf::from(home)
.join(".local")
.join("share")
.join("magnotia")
.join("lumotia")
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
{
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home).join(".magnotia")
PathBuf::from(home).join(".lumotia")
}
}
/// Outcome of attempting to migrate an existing magnotia-era data directory
/// to its lumotia equivalent on first launch after the rebrand.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MigrationStatus {
/// Renamed legacy dir to the new path. Includes optional db-file rename
/// if `magnotia.db` was found inside.
Migrated {
from: PathBuf,
to: PathBuf,
renamed_db: bool,
},
/// New path already exists. Did not touch legacy (if any) to avoid
/// destroying user data on the new path.
TargetAlreadyExists { target: PathBuf },
/// No legacy data dir on disk. Nothing to do.
NoLegacyFound,
}
/// Probe the legacy magnotia data dir paths on the current platform and
/// return the first one that exists, if any. Mirrors the search order
/// inside `resolve_app_data_dir()` but for the old name.
fn legacy_magnotia_data_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
let local_app_data = std::env::var("LOCALAPPDATA").ok()?;
let candidate = PathBuf::from(local_app_data).join("magnotia");
return candidate.exists().then_some(candidate);
}
#[cfg(target_os = "macos")]
{
let home = std::env::var("HOME").ok()?;
let candidate = PathBuf::from(home)
.join("Library")
.join("Application Support")
.join("Magnotia");
return candidate.exists().then_some(candidate);
}
#[cfg(target_os = "linux")]
{
let home = std::env::var("HOME").ok()?;
let dot_legacy = PathBuf::from(&home).join(".magnotia");
if dot_legacy.exists() {
return Some(dot_legacy);
}
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
if !xdg.is_empty() {
let xdg_legacy = PathBuf::from(xdg).join("magnotia");
if xdg_legacy.exists() {
return Some(xdg_legacy);
}
}
}
let xdg_default = PathBuf::from(home)
.join(".local")
.join("share")
.join("magnotia");
xdg_default.exists().then_some(xdg_default)
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
{
let home = std::env::var("HOME").ok()?;
let candidate = PathBuf::from(home).join(".magnotia");
candidate.exists().then_some(candidate)
}
}
/// Migrate a legacy magnotia data directory to the new lumotia path on
/// first launch. Idempotent: safe to call on every boot.
///
/// Rules:
/// * If the new path already exists, do nothing and return
/// `TargetAlreadyExists`. We do not destroy lumotia data, even if a
/// stale legacy dir is also present.
/// * If only the legacy path exists, rename it to the new path and
/// rename `magnotia.db` -> `lumotia.db` inside it if found.
/// * If neither exists, return `NoLegacyFound` — the first launch on a
/// clean system will create the new path itself.
pub fn migrate_legacy_data_dir(new_path: &Path) -> Result<MigrationStatus, std::io::Error> {
migrate_legacy_data_dir_inner(new_path, legacy_magnotia_data_dir())
}
/// Test-friendly inner shape: takes the legacy path explicitly so tests
/// don't depend on platform-specific HOME / LOCALAPPDATA env vars.
fn migrate_legacy_data_dir_inner(
new_path: &Path,
legacy: Option<PathBuf>,
) -> Result<MigrationStatus, std::io::Error> {
if new_path.exists() {
return Ok(MigrationStatus::TargetAlreadyExists {
target: new_path.to_path_buf(),
});
}
let Some(from) = legacy else {
return Ok(MigrationStatus::NoLegacyFound);
};
if let Some(parent) = new_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::rename(&from, new_path)?;
let renamed_db = rename_db_file_if_present(new_path)?;
Ok(MigrationStatus::Migrated {
from,
to: new_path.to_path_buf(),
renamed_db,
})
}
fn rename_db_file_if_present(dir: &Path) -> Result<bool, std::io::Error> {
let legacy_db = dir.join("magnotia.db");
if !legacy_db.exists() {
return Ok(false);
}
let new_db = dir.join("lumotia.db");
if new_db.exists() {
return Ok(false);
}
std::fs::rename(&legacy_db, &new_db)?;
Ok(true)
}
#[cfg(test)]
mod tests {
use super::AppPaths;
use super::*;
use crate::types::ModelId;
use std::path::PathBuf;
#[test]
fn derives_all_paths_from_one_base() {
let paths = AppPaths {
app_data_dir: PathBuf::from("/tmp/magnotia-test"),
app_data_dir: PathBuf::from("/tmp/lumotia-test"),
};
assert_eq!(
paths.database_path(),
PathBuf::from("/tmp/magnotia-test/magnotia.db")
PathBuf::from("/tmp/lumotia-test/lumotia.db")
);
assert_eq!(
paths.speech_model_dir(&ModelId::new("whisper-base-en")),
PathBuf::from("/tmp/magnotia-test/models/whisper-base-en")
PathBuf::from("/tmp/lumotia-test/models/whisper-base-en")
);
assert_eq!(
paths.llm_models_dir(),
PathBuf::from("/tmp/magnotia-test/models/llm")
PathBuf::from("/tmp/lumotia-test/models/llm")
);
}
fn unique_tmp(base: &str) -> PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!("lumotia-paths-test-{base}-{pid}-{nanos}"))
}
#[test]
fn migrate_with_legacy_present_renames_dir_and_db() {
let root = unique_tmp("legacy-present");
let legacy = root.join("magnotia");
let new_path = root.join("lumotia");
std::fs::create_dir_all(&legacy).unwrap();
std::fs::write(legacy.join("magnotia.db"), b"sqlite-stub").unwrap();
std::fs::write(legacy.join("recordings.placeholder"), b"x").unwrap();
let result =
migrate_legacy_data_dir_inner(&new_path, Some(legacy.clone())).expect("migrate ok");
match result {
MigrationStatus::Migrated {
from,
to,
renamed_db,
} => {
assert_eq!(from, legacy);
assert_eq!(to, new_path);
assert!(renamed_db, "expected db file to be renamed");
}
other => panic!("expected Migrated, got {other:?}"),
}
assert!(!legacy.exists(), "legacy dir should be gone");
assert!(new_path.exists(), "new dir should exist");
assert!(new_path.join("lumotia.db").exists(), "db at new name");
assert!(!new_path.join("magnotia.db").exists(), "old db gone");
assert!(
new_path.join("recordings.placeholder").exists(),
"other files preserved"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn migrate_with_both_present_returns_target_exists_and_preserves_lumotia() {
let root = unique_tmp("both-present");
let legacy = root.join("magnotia");
let new_path = root.join("lumotia");
std::fs::create_dir_all(&legacy).unwrap();
std::fs::create_dir_all(&new_path).unwrap();
std::fs::write(new_path.join("lumotia.db"), b"new-data").unwrap();
std::fs::write(legacy.join("magnotia.db"), b"legacy-data").unwrap();
let result =
migrate_legacy_data_dir_inner(&new_path, Some(legacy.clone())).expect("migrate ok");
assert_eq!(
result,
MigrationStatus::TargetAlreadyExists {
target: new_path.clone()
}
);
assert!(legacy.exists(), "legacy dir preserved");
assert!(new_path.exists(), "new dir preserved");
assert_eq!(
std::fs::read(new_path.join("lumotia.db")).unwrap(),
b"new-data".to_vec(),
"lumotia.db not overwritten"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn migrate_with_neither_present_returns_no_legacy() {
let root = unique_tmp("neither-present");
let new_path = root.join("lumotia");
let result = migrate_legacy_data_dir_inner(&new_path, None).expect("migrate ok");
assert_eq!(result, MigrationStatus::NoLegacyFound);
assert!(!new_path.exists(), "no dir created");
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn migrate_with_legacy_present_but_no_db_inside() {
let root = unique_tmp("legacy-no-db");
let legacy = root.join("magnotia");
let new_path = root.join("lumotia");
std::fs::create_dir_all(&legacy).unwrap();
std::fs::write(legacy.join("recordings.placeholder"), b"x").unwrap();
let result =
migrate_legacy_data_dir_inner(&new_path, Some(legacy.clone())).expect("migrate ok");
match result {
MigrationStatus::Migrated { renamed_db, .. } => {
assert!(!renamed_db, "no db to rename");
}
other => panic!("expected Migrated, got {other:?}"),
}
assert!(new_path.exists());
assert!(new_path.join("recordings.placeholder").exists());
std::fs::remove_dir_all(&root).ok();
}
}