import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { access, cp, lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; import test from "node:test"; const execFileAsync = promisify(execFile); const backendRoot = fileURLToPath(new URL("../", import.meta.url)); const ownedRoots = []; function packageBuildInvocation(platform = process.platform, environment = process.env) { if (platform === "win32") { const comspec = environment.ComSpec ?? environment.COMSPEC; if (!comspec) throw new Error("ComSpec is required to run npm on Windows."); return { executable: comspec, args: ["/d", "/s", "/c", "npm.cmd run build"] }; } return { executable: "npm", args: ["run", "build"] }; } async function isMissing(path) { try { await access(path); return false; } catch (error) { if (error?.code === "ENOENT") return true; throw error; } } async function createOwnedRoot(prefix) { const root = await mkdtemp(join(tmpdir(), prefix)); ownedRoots.push(root); return root; } async function copyCleaner(fixtureRoot) { await mkdir(join(fixtureRoot, "scripts"), { recursive: true }); const cleaner = join(fixtureRoot, "scripts", "clean-dist.mjs"); await cp(join(backendRoot, "scripts", "clean-dist.mjs"), cleaner); return cleaner; } async function createBackendFixture() { const fixtureRoot = await createOwnedRoot("thoth-backend-clean-dist-"); await Promise.all([ cp(join(backendRoot, "package.json"), join(fixtureRoot, "package.json")), cp(join(backendRoot, "tsconfig.json"), join(fixtureRoot, "tsconfig.json")), cp(join(backendRoot, "src"), join(fixtureRoot, "src"), { recursive: true }), copyCleaner(fixtureRoot), ]); const dependencyRoot = join(backendRoot, "node_modules"); const dependencyEntry = await lstat(dependencyRoot); if (!dependencyEntry.isDirectory() || dependencyEntry.isSymbolicLink()) { throw new Error("Backend node_modules must be a real directory."); } await symlink( dependencyRoot, join(fixtureRoot, "node_modules"), process.platform === "win32" ? "junction" : "dir", ); return fixtureRoot; } async function removeOwnedRoot(root) { for (const childName of ["node_modules", "dist"]) { const child = join(root, childName); try { const entry = await lstat(child); if (entry.isSymbolicLink()) { await rm(child, { recursive: true, force: true }); } else if (childName === "node_modules") { throw new Error(`Refusing to clean fixture with a non-link node_modules: ${root}`); } } catch (error) { if (error?.code !== "ENOENT") throw error; } } await rm(root, { recursive: true, force: true }); } test.afterEach(async () => { for (const root of ownedRoots.splice(0)) await removeOwnedRoot(root); }); test("Windows package builds use ComSpec instead of executing npm.cmd directly", () => { assert.deepEqual( packageBuildInvocation("win32", { ComSpec: "C:\\Windows\\System32\\cmd.exe" }), { executable: "C:\\Windows\\System32\\cmd.exe", args: ["/d", "/s", "/c", "npm.cmd run build"], }, ); assert.throws(() => packageBuildInvocation("win32", {}), /ComSpec is required/); }); test("cleaner is idempotent and removes a dist link without following it", async () => { const fixtureRoot = await createOwnedRoot("thoth-backend-cleaner-"); const cleaner = await copyCleaner(fixtureRoot); const fixtureDist = join(fixtureRoot, "dist"); await execFileAsync(process.execPath, [cleaner], { cwd: fixtureRoot }); assert.equal(await isMissing(fixtureDist), true); await execFileAsync(process.execPath, [cleaner], { cwd: fixtureRoot }); assert.equal(await isMissing(fixtureDist), true); const outsideRoot = await createOwnedRoot("thoth-backend-cleaner-outside-"); const outsideSentinel = join(outsideRoot, "sentinel.txt"); await writeFile(outsideSentinel, "outside-owned-data\n", "utf8"); await symlink(outsideRoot, fixtureDist, process.platform === "win32" ? "junction" : "dir"); await execFileAsync(process.execPath, [cleaner], { cwd: fixtureRoot }); assert.equal(await isMissing(fixtureDist), true, "dist link survived cleaner"); assert.equal(await readFile(outsideSentinel, "utf8"), "outside-owned-data\n"); }); test("package build replaces the complete backend distribution in an owned fixture", async () => { const fixtureRoot = await createBackendFixture(); const copiedPackage = JSON.parse(await readFile(join(fixtureRoot, "package.json"), "utf8")); assert.equal(copiedPackage.scripts.prebuild, "node scripts/clean-dist.mjs"); const workspacesDist = join(fixtureRoot, "dist", "workspaces"); const staleModules = [ "stale-build-sentinel.js", "migrate-legacy.js", "migrate-v2-qdrant.js", ].map((name) => join(workspacesDist, name)); await mkdir(workspacesDist, { recursive: true }); await Promise.all(staleModules.map((path) => writeFile(path, "export const stale = true;\n", "utf8"))); const { executable, args } = packageBuildInvocation(); await execFileAsync(executable, args, { cwd: fixtureRoot }); for (const path of staleModules) { assert.equal(await isMissing(path), true, `stale module survived the package build: ${path}`); } assert.equal( await isMissing(join(fixtureRoot, "dist", "server.js")), false, "server output was not compiled", ); });