test(meta): add duplicate-id vi.mock detector under __meta__/

Scans every src/**/*.svelte.{spec,test}.ts file for vi.mock first-arg
strings, canonicalises each by stripping a trailing .js/.ts after
.svelte, groups by canonical id, and fails if any canonical id is
referenced under two or more distinct raw spellings.

Mirrors the shape of src/__meta__/no-async-mock-factories.test.ts:
source-text regex scan (no AST parser dependency), red/green self-test
fixtures inline, then one corpus assertion that the whole suite is
clean.

This is the in-suite defence-in-depth layer for the duplicate-id birpc
race named in ADR-012 / #553 and fixed upstream by vitest PR #10267.
Harder to disable than ESLint (cross-file invariant ESLint cannot
express anyway) and harder to scope around than a CI grep.

Refs: #553

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-13 08:06:14 +02:00
parent 9030a7d031
commit 0cf4a488bb

View File

@@ -0,0 +1,130 @@
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 duplicate-id birpc race named in
// ADR-012 / #553. When the same resolved module URL is mocked via two
// distinct vi.mock id strings (e.g. '$lib/foo.svelte' and
// '$lib/foo.svelte.js'), @vitest/browser-playwright registers two
// Playwright routes against one cleanup slot — the orphan survives, fires
// after the next session's birpc closes, and crashes the run with
// "[birpc] rpc is closed, cannot call resolveManualMock".
//
// Fixed upstream in vitest PR #10267; until that fix reaches a published
// release, normalisation in user-land is the practical guard. This test
// catches the pattern at every vitest invocation — the layer hardest to
// disable or scope around.
const VI_MOCK_ID = /vi\.mock\(\s*['"]([^'"]+)['"]/g;
function extractMockIds(source: string): string[] {
const ids: string[] = [];
for (const match of source.matchAll(VI_MOCK_ID)) {
ids.push(match[1]);
}
return ids;
}
function canonicalise(id: string): string {
if (id.endsWith('.svelte.js')) return id.slice(0, -3);
if (id.endsWith('.svelte.ts')) return id.slice(0, -3);
return id;
}
export function findDuplicateMockIds(
specSources: Record<string, string>
): Map<string, Set<string>> {
const byCanonical = new Map<string, Set<string>>();
for (const source of Object.values(specSources)) {
for (const raw of extractMockIds(source)) {
const canonical = canonicalise(raw);
const existing = byCanonical.get(canonical) ?? new Set<string>();
existing.add(raw);
byCanonical.set(canonical, existing);
}
}
const duplicates = new Map<string, Set<string>>();
for (const [canonical, raws] of byCanonical) {
if (raws.size >= 2) duplicates.set(canonical, raws);
}
return duplicates;
}
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: findDuplicateMockIds', () => {
it('flags two specs mocking the same module under .svelte and .svelte.js', () => {
const dup = findDuplicateMockIds({
'a.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`,
'b.spec.ts': `vi.mock('$lib/foo.svelte.js', () => ({}));`
});
expect(dup.get('$lib/foo.svelte')).toEqual(new Set(['$lib/foo.svelte', '$lib/foo.svelte.js']));
});
it('does not flag two specs both using $lib/foo.svelte', () => {
const dup = findDuplicateMockIds({
'a.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`,
'b.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`
});
expect(dup.size).toBe(0);
});
it('does not flag $app/state and $app/stores (different modules)', () => {
const dup = findDuplicateMockIds({
'a.spec.ts': `vi.mock('$app/state', () => ({}));`,
'b.spec.ts': `vi.mock('$app/stores', () => ({}));`
});
expect(dup.size).toBe(0);
});
it('does not flag $lib/foo and $lib/bar (different canonical paths)', () => {
const dup = findDuplicateMockIds({
'a.spec.ts': `vi.mock('$lib/foo', () => ({}));`,
'b.spec.ts': `vi.mock('$lib/bar', () => ({}));`
});
expect(dup.size).toBe(0);
});
it('flags both spellings within a single file', () => {
const dup = findDuplicateMockIds({
'a.spec.ts': `
vi.mock('$lib/foo.svelte', () => ({}));
vi.mock('$lib/foo.svelte.js', () => ({}));
`
});
expect(dup.get('$lib/foo.svelte')?.size).toBe(2);
});
it('canonicalises .svelte.ts the same way as .svelte.js', () => {
const dup = findDuplicateMockIds({
'a.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`,
'b.spec.ts': `vi.mock('$lib/foo.svelte.ts', () => ({}));`
});
expect(dup.get('$lib/foo.svelte')?.size).toBe(2);
});
});
describe('browser specs: no duplicate-id vi.mock calls across the suite', () => {
it('every mocked module is referenced under exactly one id string', () => {
const specFiles = findBrowserSpecs();
expect(specFiles.length).toBeGreaterThan(0);
const sources = Object.fromEntries(
specFiles.map((file) => [file, readFileSync(file, 'utf-8')])
);
const duplicates = findDuplicateMockIds(sources);
const report = Object.fromEntries([...duplicates].map(([k, v]) => [k, [...v]]));
expect(report).toEqual({});
});
});