Files
Lumotia/crates/storage/Cargo.toml
Jake 43d319fd5a agent: lumotia — Phase A.1+A.2 rebrand migration tests + copy_dir_recursive hardening
Phase A of dogfood verification for the Magnotia -> Lumotia rebrand
cascade. The existing in-crate unit tests prove the migration copies
bytes correctly; this commit closes the gaps an atomiser-grade review
would flag.

Phase A.1 — end-to-end integration test (crates/storage/tests/legacy_db_migration.rs):

  Seeds a real on-disk magnotia.db via lumotia_storage::init (which runs
  every schema migration head-to-tail), inserts a transcript via the
  public API, drops the pool, runs migrate_legacy_data_dir_with_pairs,
  then re-opens the migrated lumotia.db and asserts the transcript is
  queryable. Three scenarios covered:
    1. Legacy-only -> migrate -> reopen -> row survives. Also verifies a
       non-DB companion file is carried along by the directory rename.
    2. Idempotency: first boot migrates, user writes new data, second
       boot is a no-op and BOTH rows survive.
    3. Both-paths-present: refuses to merge, target's empty DB is
       preserved, legacy retained on disk as a backup.

  Wires the test surface by renaming the previously-private
  migrate_legacy_data_dir_inner to pub migrate_legacy_data_dir_with_pairs
  (mirroring migrate_tauri_app_data_dir_with_paths in the sibling
  tauri_app_data_migration module).

Phase A.2a — copy_dir_recursive hardening (crates/core/src/paths.rs):

  Pre-existing footgun: the fall-through branch called std::fs::copy()
  on any DirEntry that was not a symlink or a directory. On Unix that
  includes FIFOs, sockets, and char/block device nodes. Opening a FIFO
  for read with no writer attached blocks forever — a stale debug FIFO
  in the user's ~/.magnotia tree would silently hang first launch.

  The branch now explicitly distinguishes is_file() (real regular file
  -> copy) from anything else (-> Err with ErrorKind::Unsupported,
  naming the offending path). Migration becomes re-runnable once the
  user cleans up the offending node. Same-filesystem rename via
  std::fs::rename is atomic and unaffected; only the EXDEV fallback path
  touches the new guard.

Phase A.2b — three adversarial probes (crates/core/src/paths.rs tests):

  - FIFO inside the legacy tree: copy_dir_recursive must return an
    Unsupported error WITHOUT hanging. Test bounded by a 5s wall clock
    + a worker thread so a regression to the old fall-through would
    surface as a panic, not a stalled CI job.
  - Unreadable file (mode 0000): copy_dir_recursive must surface
    PermissionDenied, not silently skip. Skips its core assertion under
    euid 0 (root bypasses DAC permissions, would mask the regression).
  - Dangling symlink (target nonexistent): symlink is recreated at
    destination with link target preserved verbatim; the migration
    does NOT try to dereference and does NOT abort the rest of the copy.

Verification:
- cargo fmt --check: clean
- cargo clippy --workspace --all-targets -- -D warnings: clean
- cargo test --workspace: 409 passed, 0 failed (up from 405 pre-commit;
  3 storage integration tests + 3 paths adversarial + 1 net carry-over)
2026-05-14 07:20:18 +01:00

41 lines
1.6 KiB
TOML

[package]
name = "lumotia-storage"
version = "0.1.0"
edition = "2021"
description = "SQLite persistence, BM25 search, and file storage for Lumotia"
[dependencies]
lumotia-core = { path = "../core" }
# SQLite with compile-time checked queries
# default-features = false strips sqlx's `any`, `macros`, `migrate`, `json` —
# none of which this crate uses (it calls sqlx::query() / query_scalar()
# directly and runs its own migration machinery). Cuts ~40% of sqlx's
# compile graph, most visibly on Windows MSVC where each proc-macro crate
# (which `macros` pulls in) becomes a slow .dll link.
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite"] }
# Async runtime
tokio = { version = "1", features = ["rt", "sync", "macros"] }
# Serialisation (DailyCompletionCount exposed to frontend via Tauri commands)
serde = { version = "1", features = ["derive"] }
# Logging
log = "0.4"
# Structured error derivation for lumotia_storage::Error.
thiserror = "1"
# UUIDs for profile + profile_terms ids (v7 random).
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]
# Real-file tempdirs for integration tests that need an on-disk DB (the
# in-memory `sqlite::memory:` connection in src/database.rs unit tests
# doesn't cover the rename-then-reopen path the rebrand migration exercises).
tempfile = "3"
# `rt-multi-thread` is required for the `#[tokio::test(flavor = "multi_thread")]`
# variants that drive blocking lumotia_core::paths migrations from async tests.
tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "macros"] }