From 636d61a81b00f7de307967c09539920d0dc5a4d1 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 12 May 2026 22:05:43 +0200 Subject: [PATCH] test(meta): scan src/**/*.svelte.{test,spec}.ts for async vi.mock factories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In-suite belt-and-braces detector for the birpc teardown race named in ADR-012 / #553. Catches `vi.mock(, async ... { ... await import(...) ... })` in any browser spec on every vitest invocation — the layer hardest to disable or scope around (ESLint can be silenced; CI grep runs only in CI; this test runs whenever the suite runs). Demonstrated red→green by planting a synthetic offender locally and watching the live-scan assertion fail; removing the offender returned it to green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__meta__/no-async-mock-factories.test.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 frontend/src/__meta__/no-async-mock-factories.test.ts diff --git a/frontend/src/__meta__/no-async-mock-factories.test.ts b/frontend/src/__meta__/no-async-mock-factories.test.ts new file mode 100644 index 00000000..ef3622ea --- /dev/null +++ b/frontend/src/__meta__/no-async-mock-factories.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from 'vitest'; +import { readdirSync, readFileSync } from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Belt-and-braces detector for the birpc teardown race named in ADR-012 / #553. +// ESLint catches the pattern at save time, CI grep catches it before the test +// suite launches, and this in-suite test catches it at every vitest invocation — +// the layer hardest to disable or scope around. +// +// We scan source text rather than parsing AST: fast, no parser dependency, +// good enough for the named anti-pattern. The pattern matches +// `vi.mock(, async ... { ... await import(...) ... })`. + +const ASYNC_MOCK_WITH_DYNAMIC_IMPORT = /vi\.mock\([^)]*,\s*async[^{]*\{[\s\S]*?await\s+import\s*\(/; + +export function hasAsyncMockFactoryWithDynamicImport(source: string): boolean { + return ASYNC_MOCK_WITH_DYNAMIC_IMPORT.test(source); +} + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SRC_ROOT = path.resolve(__dirname, '..'); + +function findBrowserSpecs(): string[] { + const entries = readdirSync(SRC_ROOT, { recursive: true, withFileTypes: true }); + return entries + .filter( + (e) => + e.isFile() && (e.name.endsWith('.svelte.test.ts') || e.name.endsWith('.svelte.spec.ts')) + ) + .map((e) => path.join(e.parentPath ?? (e as { path: string }).path, e.name)); +} + +describe('scan: hasAsyncMockFactoryWithDynamicImport', () => { + it('flags async vi.mock factory with await import in body', () => { + const fixture = `vi.mock('$app/stores', async () => { + const mod = await import('./__mocks__/navigatingStore'); + return { navigating: mod.navigatingStore }; + });`; + expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(true); + }); + + it('does not flag sync vi.mock factory', () => { + const fixture = `vi.mock('$app/state', () => ({ navigating: { type: null } }));`; + expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(false); + }); + + it('does not flag async vi.mock factory without dynamic import', () => { + const fixture = `vi.mock('foo', async () => { + const x = await Promise.resolve(42); + return { bar: x }; + });`; + expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(false); + }); + + it('does not flag dynamic import outside any vi.mock', () => { + const fixture = `async function load() { + const mod = await import('./something'); + return mod.default; + }`; + expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(false); + }); + + it('flags async factory written as async function expression', () => { + const fixture = `vi.mock('foo', async function () { + const mod = await import('./bar'); + return mod; + });`; + expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(true); + }); +}); + +describe('browser specs: no async vi.mock factory contains await import', () => { + it('every src/**/*.svelte.{test,spec}.ts file is clean', () => { + const specFiles = findBrowserSpecs(); + expect(specFiles.length).toBeGreaterThan(0); + const offenders = specFiles.filter((file) => + hasAsyncMockFactoryWithDynamicImport(readFileSync(file, 'utf-8')) + ); + expect(offenders).toEqual([]); + }); +});