Files
ThothII/backend/scripts/clean-dist.test.mjs
T

148 lines
5.4 KiB
JavaScript

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",
);
});