singularity-forge/scripts/migrate-to-vitest.mjs
2026-05-05 14:46:18 +02:00

167 lines
5 KiB
JavaScript

#!/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 { readdirSync, readFileSync, 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<typeof mock\.fn>/g,
"ReturnType<typeof vi.fn>",
);
}
// 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`);