test(meta): scan src/**/*.svelte.{test,spec}.ts for async vi.mock factories
In-suite belt-and-braces detector for the birpc teardown race named in ADR-012 / #553. Catches `vi.mock(<arg>, 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) <noreply@anthropic.com>
This commit is contained in:
82
frontend/src/__meta__/no-async-mock-factories.test.ts
Normal file
82
frontend/src/__meta__/no-async-mock-factories.test.ts
Normal file
@@ -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(<arg>, 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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user