agent: lumotia — Phase A.5 vitest scaffold + localStorageMigration unit tests

First frontend unit test framework on Lumotia. Pinned exact versions for
supply-chain hygiene (matches the rust-toolchain.toml discipline from the
27661c8 hygiene pass and the npm audit signatures pre-flight from e4d56b8):

  - vitest 4.1.6 (compatible with vite 6, supports vite 6/7/8)
  - jsdom 29.1.1

Installed with `npm install --save-dev --save-exact --ignore-scripts` per
the install discipline documented in the README — the --ignore-scripts
flag blocks the postinstall vector that npm worms (Shai-Hulud,
mini-Shai-Hulud) rely on.

vite.config.js:
  - Switched defineConfig import to vitest/config (superset of vite/config;
    production builds ignore the `test` key).
  - test.environment = "jsdom" so storage-shim tests drive real browser APIs.
  - test.include scoped to src/**/*.{test,spec}.{ts,js} — colocated with
    source, mirrors the Rust #[cfg(test)] sibling pattern.
  - test.exclude blocks src-tauri/ (owned by cargo test).
  - restoreMocks + clearMocks + unstubAllGlobals on so module-level state
    can't leak between tests.

src/lib/utils/localStorageMigration.test.ts — 12 tests:
  migrateLocalStorageKey:
    - copies value + removes old when only old exists
    - removes old + keeps new when both exist (lumotia is authoritative)
    - no-op when only new exists
    - no-op when neither exists
    - idempotent (second call after first migrates nothing)
    - preserves the value's exact bytes (no JSON round-trip)
    - preserves empty-string values (distinct from null)
    - survives DOMException / quota errors without re-raising
    - no-op when localStorage is undefined (SSR-safe)
  migrateLocalStorageKeys:
    - processes pairs in order
    - per-pair failure does not strand remaining pairs (resilience)
    - empty pairs list is a clean no-op

package.json:
  - "test": "vitest run" (one-shot, CI-friendly)
  - "test:watch": "vitest" (dev loop)

README: documents `npm run test` alongside `cargo test --workspace` and
`npm run check` in the Testing section.

Verification:
- npm run test: 12/12 pass
- npm run check: 0 errors, 0 warnings (the new .ts test type-checks clean
  against jsconfig.json's strict typescript settings)
This commit is contained in:
2026-05-14 07:29:57 +01:00
parent 18a64f5c56
commit 206ac6219d
5 changed files with 1138 additions and 4 deletions

View File

@@ -1,4 +1,7 @@
import { defineConfig } from "vite";
// Using vitest/config rather than vite/config so one config surface drives
// `vite dev` / `vite build` AND `vitest run`. Vitest's defineConfig is a
// superset of Vite's — production builds ignore the `test` key.
import { defineConfig } from "vitest/config";
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
@@ -24,4 +27,23 @@ export default defineConfig(async () => ({
ignored: ["**/src-tauri/**"],
},
},
test: {
// jsdom gives storage-shim tests a real localStorage / DOMException /
// window surface rather than hand-rolled mocks. Tests can override
// per-file with `// @vitest-environment node` if they need to.
environment: "jsdom",
// Discover *.test.{ts,js} colocated next to source. Mirrors Rust's
// #[cfg(test)] sibling convention; no separate tests/ tree.
include: ["src/**/*.{test,spec}.{ts,js}"],
// Frontend tests must never reach into the Tauri backend; that surface
// is owned by `cargo test` in src-tauri/ and crates/.
exclude: ["src-tauri/**", "node_modules/**", "build/**", ".svelte-kit/**"],
// Restore a clean global state between tests — vitest's restoreMocks
// unwinds vi.spyOn / vi.fn, clearMocks resets call history, and
// unstubAllGlobals undoes vi.stubGlobal. Prevents accidental leakage
// when tests share modules with module-level state.
restoreMocks: true,
clearMocks: true,
unstubAllGlobals: true,
},
}));