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 ): Map> { const byCanonical = new Map>(); for (const source of Object.values(specSources)) { for (const raw of extractMockIds(source)) { const canonical = canonicalise(raw); const existing = byCanonical.get(canonical) ?? new Set(); existing.add(raw); byCanonical.set(canonical, existing); } } const duplicates = new Map>(); 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({}); }); });