vitest-static-import-env-hoist-live-datadir-write
I've written the solution to ~/vitest-static-import-env-hoist-live-datadir-write.md.
Note: the 9router repository is not present in this environment, so the document is derived from the problem class description rather than executed against the codebase. It is self-contained and directly usable.
Root cause: ESM static import declarations are hoisted and evaluated before module-body statements, and vitest transforms in that order. The SQLite adapter reads DATA_DIR once at init, so process.env.DATA_DIR = tempDir ran after the adapter had already resolved the default ~/.9router/db/data.sqlite. The probe then wrote to the live DB; getUsageStats() passed for the wrong reason while fs.readdirSync(tempDir) was empty and the row count was inflated by pre-existing live rows.
Fix (test shape):
1. fs.mkdtempSync → set process.env.DATA_DIR first
2. vi.resetModules()
3. await import(...) the module under test — dynamic-only, even if another test file imports it statically
4. Assert an exact delta (after - before === 1) and keep the temp-dir non-empty check
5. Cleanup: delete process.env.DATA_DIR and fs.rmSync(tempDir)
Durable fix: make the adapter resolve the DB path lazily (getDataDir() / dbPath()) instead of capturing env at module top level.
Debugging heuristics: empty sandbox + passing persistence assertion is the tell; a count larger than your own writes means a second writer; grep for process.env.* = in any file with static imports of env-sensitive modules.
Cleanup: delete probe rows by a controlled provider/model discriminator, revert the derived usageDaily aggregate and totalRequestsLifetime counter in one transaction, then re-assert zero — phantom telemetry in a dev DB is worse than a red test.
Verification: isolated HOME, run the probe file, confirm exactly one row in the temp dir and zero probe rows in the live DB.
# Evidence - Problem class: vitest-static-import-env-hoist-live-datadir-write - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T10:57:13.890Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM. A throwaway verification test created a temp dir with fs.mkdtempSync, assigned process.env.DATA_DIR = tempDir, then imported the modules under test with top-level `import ... from` statements and called the real persistence path. getUsageStats() returned rows (so the 'did it persist?' assertion passed) but fs.readdirSync(tempDir) was EMPTY, and the row count seen was higher than the probe had written. The rows had gone into ~/.9router/db/data.sqlite - the DEVELOPER'S LIVE DATABASE - not the sandbox.\n\nROOT CAUSE. ESM static imports are hoisted and evaluated before any statement in the module body, and vitest transforms the file in that order. So `import { saveUsageStats } from \"...\"` (and everything it transitively imports, including the SQLite adapter that reads DATA_DIR once at init) already ran BEFORE the `process.env.DATA_DIR = tempDir` line executed. The adapter resolved its default path, the probe wrote real rows to the live DB, and the temp dir stayed empty. Nothing failed loudly: the persistence assertion passed for the wrong reason.\n\nFIX / CORRECT SHAPE. Set the env var(s) first, then import dynamically:\n\n const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), \"probe-\"));\n process.env.DATA_DIR = tempDir;\n const { saveUsageStats } = await import(\"../../open-sse/handlers/chatCore/requestDetail.js\");\n const { getUsageStats } = await import(\"@/lib/db/repos/usageRepo.js\");\n\n(vi.resetModules() before the dynamic imports when the reference suites do it; both static-shaped and dynamic-shaped imports of the SAME module in one file still share the first evaluation, so keep the module under test dynamic-only.)\n\nDETECTION HEURISTICS. (1) A temp-dir emptiness assertion next to a passing data assertion is the signal - never drop the 'the sandbox actually got the file' check. (2) Compare counts: a row count larger than the probe's own writes means a second writer (the live DB). (3) `process.env.X = ...` followed by static imports anywhere in the same file is the anti-pattern, regardless of where the assignment sits textually.\n\nCLEANUP WHEN IT HAS ALREADY HAPPENED. Identify the polluted rows by a discriminator you control (provider/model unique to the probe) plus the marking your code writes, delete them, and revert the derived aggregates the same write created (daily aggregate row + lifetime counter row in this case), then re-assert zero rows. Leaving phantom telemetry in a dev database is worse than a red test.\n\nVERIFICATION. After converting to dynamic imports the same probe wrote exactly one row into the temp DATA_DIR, and the empty-response case left the count unchanged.", "environment": "vitest suite run from a repo's own tests/ package (cwd=tests), Node ESM, Next.js app whose SQLite adapter resolves its data directory from DATA_DIR at module import time", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "vitest-static-import-env-hoist-live-datadir-write", "provider": "openrouter", "solved_at": "2026-09-17T10:57:13.891Z", "version": "vitest 4.1.10 / node 22 ESM"}I've written the solution to ~/vitest-static-import-env-hoist-live-datadir-write.md.
Note: the 9router repository is not present in this environment, so the document is derived from the problem class description rather than executed against the codebase. It is self-contained and directly usable.
Root cause: ESM static import declarations are hoisted and evaluated before module-body statements, and vitest transforms in that order. The SQLite adapter reads DATA_DIR once at init, so process.env.DATA_DIR = tempDir ran after the adapter had already resolved the default ~/.9router/db/data.sqlite. The probe then wrote to the live DB; getUsageStats() passed for the wrong reason while fs.readdirSync(tempDir) was empty and the row count was inflated by pre-existing live rows.
Fix (test shape):
1. fs.mkdtempSync → set process.env.DATA_DIR first
2. vi.resetModules()
3. await import(...) the module under test — dynamic-only, even if another test file imports it statically
4. Assert an exact delta (after - before === 1) and keep the temp-dir non-empty check
5. Cleanup: delete process.env.DATA_DIR and fs.rmSync(tempDir)
Durable fix: make the adapter resolve the DB path lazily (getDataDir() / dbPath()) instead of capturing env at module top level.
Debugging heuristics: empty sandbox + passing persistence assertion is the tell; a count larger than your own writes means a second writer; grep for process.env.* = in any file with static imports of env-sensitive modules.
Cleanup: delete probe rows by a controlled provider/model discriminator, revert the derived usageDaily aggregate and totalRequestsLifetime counter in one transaction, then re-assert zero — phantom telemetry in a dev DB is worse than a red test.
Verification: isolated HOME, run the probe file, confirm exactly one row in the temp dir and zero probe rows in the live DB.
# Evidence - Problem class: vitest-static-import-env-hoist-live-datadir-write - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T10:57:13.890Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM. A throwaway verification test created a temp dir with fs.mkdtempSync, assigned process.env.DATA_DIR = tempDir, then imported the modules under test with top-level `import ... from` statements and called the real persistence path. getUsageStats() returned rows (so the 'did it persist?' assertion passed) but fs.readdirSync(tempDir) was EMPTY, and the row count seen was higher than the probe had written. The rows had gone into ~/.9router/db/data.sqlite - the DEVELOPER'S LIVE DATABASE - not the sandbox.\n\nROOT CAUSE. ESM static imports are hoisted and evaluated before any statement in the module body, and vitest transforms the file in that order. So `import { saveUsageStats } from \"...\"` (and everything it transitively imports, including the SQLite adapter that reads DATA_DIR once at init) already ran BEFORE the `process.env.DATA_DIR = tempDir` line executed. The adapter resolved its default path, the probe wrote real rows to the live DB, and the temp dir stayed empty. Nothing failed loudly: the persistence assertion passed for the wrong reason.\n\nFIX / CORRECT SHAPE. Set the env var(s) first, then import dynamically:\n\n const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), \"probe-\"));\n process.env.DATA_DIR = tempDir;\n const { saveUsageStats } = await import(\"../../open-sse/handlers/chatCore/requestDetail.js\");\n const { getUsageStats } = await import(\"@/lib/db/repos/usageRepo.js\");\n\n(vi.resetModules() before the dynamic imports when the reference suites do it; both static-shaped and dynamic-shaped imports of the SAME module in one file still share the first evaluation, so keep the module under test dynamic-only.)\n\nDETECTION HEURISTICS. (1) A temp-dir emptiness assertion next to a passing data assertion is the signal - never drop the 'the sandbox actually got the file' check. (2) Compare counts: a row count larger than the probe's own writes means a second writer (the live DB). (3) `process.env.X = ...` followed by static imports anywhere in the same file is the anti-pattern, regardless of where the assignment sits textually.\n\nCLEANUP WHEN IT HAS ALREADY HAPPENED. Identify the polluted rows by a discriminator you control (provider/model unique to the probe) plus the marking your code writes, delete them, and revert the derived aggregates the same write created (daily aggregate row + lifetime counter row in this case), then re-assert zero rows. Leaving phantom telemetry in a dev database is worse than a red test.\n\nVERIFICATION. After converting to dynamic imports the same probe wrote exactly one row into the temp DATA_DIR, and the empty-response case left the count unchanged.", "environment": "vitest suite run from a repo's own tests/ package (cwd=tests), Node ESM, Next.js app whose SQLite adapter resolves its data directory from DATA_DIR at module import time", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "vitest-static-import-env-hoist-live-datadir-write", "provider": "openrouter", "solved_at": "2026-09-17T10:57:13.891Z", "version": "vitest 4.1.10 / node 22 ESM"}