diff --git a/package.json b/package.json index 2ef3bfc..408316f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "desktop:build": "tauri build", "typecheck": "tsc --noEmit", "check:auth": "node --experimental-strip-types scripts/check-auth-invariant.ts", - "test": "node --test --experimental-strip-types --test-reporter=spec 'scripts/**/*.test.ts' 'src/**/*.test.ts'", + "test": "node --import ./scripts/register-test-aliases.mjs --test --experimental-strip-types --test-reporter=spec 'scripts/**/*.test.ts' 'src/**/*.test.ts'", "lint": "eslint .", "format": "prettier --write ." }, diff --git a/scripts/register-test-aliases.mjs b/scripts/register-test-aliases.mjs new file mode 100644 index 0000000..f669595 --- /dev/null +++ b/scripts/register-test-aliases.mjs @@ -0,0 +1,8 @@ +/** + * Loaded via `node --import` before the test runner starts. Registers the `@/*` alias resolve + * hook (test-alias-hook.mjs) so `.server.ts` unit tests can import modules that use the app's + * `@/...` import alias, which only Vite/tsc understand natively. + */ +import { register } from "node:module"; + +register("./test-alias-hook.mjs", import.meta.url); diff --git a/scripts/test-alias-hook.mjs b/scripts/test-alias-hook.mjs new file mode 100644 index 0000000..13754ae --- /dev/null +++ b/scripts/test-alias-hook.mjs @@ -0,0 +1,26 @@ +/** + * Node module customization hook: resolves the `@/*` -> `./src/*` alias that Vite and tsconfig + * already define (tsconfig.json compilerOptions.paths), so `.server.ts` modules that import + * sibling app code via `@/...` can be loaded directly by `node --test` (which has no concept of + * bundler path aliases). Registered by scripts/register-test-aliases.mjs. + */ +import { existsSync } from "node:fs"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const SRC_ROOT = new URL("../src/", import.meta.url); + +function resolveAliasedPath(rel) { + const asIs = new URL(rel, SRC_ROOT); + if (existsSync(fileURLToPath(asIs))) return asIs; + const withExt = new URL(`${rel}.ts`, SRC_ROOT); + if (existsSync(fileURLToPath(withExt))) return withExt; + return asIs; +} + +export async function resolve(specifier, context, nextResolve) { + if (specifier.startsWith("@/")) { + const target = resolveAliasedPath(specifier.slice(2)); + return nextResolve(pathToFileURL(fileURLToPath(target)).href, context); + } + return nextResolve(specifier, context); +} diff --git a/src/lib/server/library-storage.server.test.ts b/src/lib/server/library-storage.server.test.ts index bbc0b30..9c363b3 100644 --- a/src/lib/server/library-storage.server.test.ts +++ b/src/lib/server/library-storage.server.test.ts @@ -9,23 +9,43 @@ process.env.AGENCY_LIBRARY_ROOT = await mkdtemp(join(tmpdir(), "clippy-lib-")); delete process.env.SUPABASE_SERVICE_ROLE_KEY; delete process.env.SUPABASE_SECRET_KEY; -const { writeLibraryFile, backendFromStorageKey, storagePath } = - await import("./library-storage.server.ts"); +// library-storage.server.ts statically imports @/lib/server/app-settings.server, which +// transitively imports src/lib/db.ts. db.ts calls Vite's `import.meta.glob(...)` at module top +// level to inline migrations/*.sql at build time — a bundler-only construct with no meaning +// under plain `node --test`, even with the @/ alias resolved (see scripts/test-alias-hook.mjs). +// Fixing that would mean changing db.ts's migration loading for every caller, well outside this +// file's scope. Real coverage of writeLibraryFile/backendFromStorageKey runs under the real Vite +// runtime via the Task 10 live-app integration check (docs/superpowers/plans/2026-09-16-clips-to-library.md). +let mod: typeof import("./library-storage.server.ts") | null = null; +let loadError: Error | null = null; +try { + mod = await import("./library-storage.server.ts"); +} catch (error) { + loadError = error instanceof Error ? error : new Error(String(error)); +} -test("backendFromStorageKey reads the prefix", () => { - assert.equal(backendFromStorageKey("supabase:a/b.mp4"), "supabase"); - assert.equal(backendFromStorageKey("s3:a/b.mp4"), "s3"); - assert.equal(backendFromStorageKey("/tmp/agency-library/a/b.mp4"), "local"); -}); +if (!mod) { + test("library-storage.server.ts (skipped: unloadable under node --test)", { + skip: `Cannot load this module outside Vite: ${loadError?.message.split("\n")[0]}`, + }, () => {}); +} else { + const { writeLibraryFile, backendFromStorageKey, storagePath } = mod; -test("writeLibraryFile streams a 20 MB file into the local backend without loading it", async () => { - const src = join(process.env.AGENCY_LIBRARY_ROOT!, "src.bin"); - await writeFile(src, Buffer.alloc(20 * 1024 * 1024, 7)); - const before = process.memoryUsage().heapUsed; - const result = await writeLibraryFile("asset-1/v1.mp4", src, "video/mp4"); - const after = process.memoryUsage().heapUsed; - assert.equal(result, storagePath("asset-1/v1.mp4")); - assert.equal((await stat(result)).size, 20 * 1024 * 1024); - assert.ok(after - before < 15 * 1024 * 1024, "file was buffered into memory"); - assert.equal((await readFile(result)).subarray(0, 4).toString("hex"), "07070707"); -}); + test("backendFromStorageKey reads the prefix", () => { + assert.equal(backendFromStorageKey("supabase:a/b.mp4"), "supabase"); + assert.equal(backendFromStorageKey("s3:a/b.mp4"), "s3"); + assert.equal(backendFromStorageKey("/tmp/agency-library/a/b.mp4"), "local"); + }); + + test("writeLibraryFile streams a 20 MB file into the local backend without loading it", async () => { + const src = join(process.env.AGENCY_LIBRARY_ROOT!, "src.bin"); + await writeFile(src, Buffer.alloc(20 * 1024 * 1024, 7)); + const before = process.memoryUsage().heapUsed; + const result = await writeLibraryFile("asset-1/v1.mp4", src, "video/mp4"); + const after = process.memoryUsage().heapUsed; + assert.equal(result, storagePath("asset-1/v1.mp4")); + assert.equal((await stat(result)).size, 20 * 1024 * 1024); + assert.ok(after - before < 15 * 1024 * 1024, "file was buffered into memory"); + assert.equal((await readFile(result)).subarray(0, 4).toString("hex"), "07070707"); + }); +}