#!/usr/bin/env node /** * Migrate test files from node:test to vitest. * * Changes: * 1. Replace `from "node:test"` → `from 'vitest'` in all imports * 2. Files using mock.fn(): replace `mock.fn` → `vi.fn` and add `vi` to imports * 3. auto-loop.test.ts: replace mock.timers with vi fake timers API */ import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const ROOT = join(process.cwd(), "src"); const FILES_WITH_MOCK_FN = new Set([ "src/resources/extensions/sf/tests/pre-execution-fail-closed.test.ts", "src/resources/extensions/sf/tests/pre-execution-pause-wiring.test.ts", "src/resources/extensions/sf/tests/post-exec-retry-bypass.test.ts", "src/resources/extensions/sf/tests/validate-milestone-stuck-guard.test.ts", "src/resources/extensions/sf/tests/claude-import-tui.test.ts", ]); const AUTO_LOOP_FILE = "/home/mhugo/code/singularity-forge/src/resources/extensions/sf/tests/auto-loop.test.ts"; function collectTestFiles(dir) { const results = []; const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const full = join(dir, entry.name); if (entry.isDirectory()) { results.push(...collectTestFiles(full)); } else if ( (entry.name.endsWith(".test.ts") || entry.name.endsWith(".test.mjs")) && !entry.name.endsWith(".d.ts") ) { results.push(full); } } return results; } function migrateImport(content, { isAutoLoop, isMockFn }) { // Case: import test from "node:test"; if (isAutoLoop || isMockFn) { // These files don't have plain default imports (they have named imports too) // but keep the check for safety } if (!content.includes("from 'node:test'")) { // Replace: import test from "node:test"; content = content.replace( /^import test from "node:test";$/gm, "import { test } from 'vitest';", ); } if (isAutoLoop) { // import test, { mock } from "node:test" // → import { test, vi } from 'vitest' return content.replace( /import\s+(test,)?\s*\{\s*([^}]+)\}\s+from\s+"node:test";?/, (match, hasDefault, named) => { const namedList = named.split(",").map((s) => s.trim()).filter((s) => s !== "mock"); const vitestNamed = ["vi", ...namedList.filter((s) => s !== "test")].join(", "); const defaultImport = hasDefault ? "test, " : ""; return `import { ${defaultImport}${vitestNamed} } from 'vitest';`; }, ); } else if (isMockFn) { // import { ..., mock, ... } from "node:test" // → import { ..., vi, ... } from 'vitest' return content.replace( /import\s+(test,)?\s*\{\s*([^}]+)\}\s+from\s+"node:test";?/, (match, hasDefault, named) => { const namedList = named.split(",").map((s) => s.trim()).filter((s) => s !== "mock"); const vitestNamed = ["vi", ...namedList.filter((s) => s !== "test")].join(", "); const defaultImport = hasDefault ? "test, " : ""; return `import { ${defaultImport}${vitestNamed} } from 'vitest';`; }, ); } else { // Simple case: just swap the source return content.replace( /import\s+(test,)?\s*\{\s*([^}]+)\}\s+from\s+"node:test";?/, (match, hasDefault, named) => { const defaultImport = hasDefault ? "test, " : ""; return `import { ${defaultImport}${named.trim()} } from 'vitest';`; }, ); } } const files = collectTestFiles(ROOT); console.log(`Found ${files.length} test files`); let updated = 0; let errors = 0; for (const file of files) { try { const content = readFileSync(file, "utf-8"); if (!content.includes('from "node:test"')) continue; let newContent = content; const isAutoLoop = file === AUTO_LOOP_FILE; const relPath = file.replace(process.cwd() + "/", ""); const isMockFn = FILES_WITH_MOCK_FN.has(relPath); // Step 1: migrate the import newContent = migrateImport(newContent, { isAutoLoop, isMockFn }); // Step 2: migrate mock.fn → vi.fn (for mock-fn files and auto-loop) if (isAutoLoop || isMockFn) { newContent = newContent.replace(/\bmock\.fn\b/g, "vi.fn"); newContent = newContent.replace(/ReturnType/g, "ReturnType"); } // Step 3: migrate mock.timers (auto-loop only) if (isAutoLoop) { // mock.timers.enable() → vi.useFakeTimers() newContent = newContent.replace(/\bmock\.timers\.enable\(\)/g, "vi.useFakeTimers()"); // mock.timers.tick(ms) → vi.advanceTimersByTime(ms) newContent = newContent.replace(/\bmock\.timers\.tick\(([^)]+)\)/g, "vi.advanceTimersByTime($1)"); // mock.timers.reset() → vi.useRealTimers() newContent = newContent.replace(/\bmock\.timers\.reset\(\)/g, "vi.useRealTimers()"); } if (newContent !== content) { writeFileSync(file, newContent, "utf-8"); updated++; const rel = file.replace(process.cwd() + "/", ""); if (isAutoLoop || isMockFn) { console.log(` Migrated (mock): ${rel}`); } } } catch (err) { console.error(`Error: ${file}: ${err.message}`); errors++; } } console.log(`Updated ${updated} files, ${errors} errors`);