nexus / scripts /ci-workflow.test.mjs
betterwithage's picture
deploy(hf): sync szl-holdings/nexus@00edebc993282efd5b43b0144b3d3d27ab43e264 derived COPY set
31b3c47 verified
Raw History Blame Contribute Delete
7.37 kB
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import ts from "typescript";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
// CRLF-normalized so the line-anchored checks hold on a Windows checkout.
const CI = readFileSync(join(ROOT, ".github/workflows/ci.yml"), "utf8").replace(/\r\n/g, "\n");
const PACKAGE = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
const LOCK = JSON.parse(readFileSync(join(ROOT, "package-lock.json"), "utf8"));
// The SHAs szl-holdings/.github already runs, so the org has one pin per action.
const APPROVED_PINS = {
"actions/checkout": "3d3c42e5aac5ba805825da76410c181273ba90b1", // v7.0.1
"actions/setup-node": "820762786026740c76f36085b0efc47a31fe5020", // v7.0.0
"actions/setup-python": "5fda3b95a4ea91299a34e894583c3862153e4b97", // v7.0.0
"step-security/harden-runner": "e14015d583714f6e62063499dc959a02595150a1", // v2.21.1
};
function onBlock() {
const match = CI.match(/^on:\n([\s\S]*?)^(?=\S)/m);
assert.ok(match, "missing on: block");
return match[1];
}
function jobSteps(job) {
const match = CI.match(
new RegExp(String.raw`^ ${job}:\n([\s\S]*?)(?=^ [A-Za-z_][\w-]*:\n|(?![\s\S]))`, "m"),
);
assert.ok(match, `missing job ${job}`);
return match[1];
}
test("CI runs on every pull request and every push to main, with no path filter", () => {
const on = onBlock();
assert.match(on, /^ {2}push:\n {4}branches: \[main\]$/m);
assert.match(on, /^ {2}pull_request:$/m);
assert.doesNotMatch(on, /paths(?:-ignore)?:/);
assert.doesNotMatch(on, /branches-ignore:/);
});
test("CI is read-only", () => {
assert.match(CI, /^permissions:\n {2}contents: read\n/m);
assert.doesNotMatch(CI, /:\s*write\b/);
assert.doesNotMatch(CI, /secrets\./);
assert.doesNotMatch(CI, /persist-credentials: true/);
});
test("every action is pinned to the SHA the org already uses", () => {
const uses = [...CI.matchAll(/^\s*uses:\s*(\S+)/gm)].map((match) => match[1]);
assert.ok(uses.length >= 6);
for (const value of uses) {
const [action, ref] = value.split("@");
assert.ok(action in APPROVED_PINS, `unapproved action: ${value}`);
assert.equal(ref, APPROVED_PINS[action], `wrong pin for ${action}`);
}
});
test("each job hardens the runner before anything else runs", () => {
for (const job of ["node", "python", "space"]) {
const steps = jobSteps(job);
const firstUses = steps.match(/^\s*uses:\s*(\S+)/m);
assert.ok(firstUses, `${job}: no steps`);
assert.match(firstUses[1], /^step-security\/harden-runner@/, `${job}: first step`);
}
});
test("the node job runs the full npm gate", () => {
const steps = jobSteps("node");
const order = ["npm ci", "npm run typecheck", "npm run lint", "npm test", "npm run build"];
let last = -1;
for (const command of order) {
const at = steps.indexOf(`run: ${command}\n`);
assert.notEqual(at, -1, `node job does not run: ${command}`);
assert.ok(at > last, `${command} is out of order`);
last = at;
}
for (const script of ["typecheck", "lint", "test", "build"]) {
assert.ok(PACKAGE.scripts[script], `package.json has no "${script}" script`);
}
});
test("TypeScript stays on the supported 6.0 patch line with an exact matching lock", () => {
// typescript-eslint 8.70.1 and 8.71.0 admit >=4.8.4 <6.1.0, not TS 7.
// A future minor/major migration must qualify its lint peer contract too.
assert.match(PACKAGE.devDependencies.typescript, /^~6\.0\.\d+$/);
assert.equal(LOCK.packages[""].devDependencies.typescript, PACKAGE.devDependencies.typescript);
assert.match(LOCK.packages["node_modules/typescript"].version, /^6\.0\.\d+$/);
assert.equal(
LOCK.packages["node_modules/typescript-eslint"].peerDependencies.typescript,
">=4.8.4 <6.1.0",
);
});
test("CI never overrides incompatible dependency peers to pass installation", () => {
assert.doesNotMatch(CI, /--legacy-peer-deps|--force\b|strict-peer-deps:\s*false/);
});
test("Nitro's optional cache peer is explicit without overriding Babel's major", () => {
assert.equal(PACKAGE.devDependencies["lru-cache"], "11.5.3");
assert.equal(LOCK.packages[""].devDependencies["lru-cache"], "11.5.3");
assert.equal(LOCK.packages["node_modules/lru-cache"].version, "11.5.3");
assert.equal(
LOCK.packages["node_modules/@babel/helper-compilation-targets/node_modules/lru-cache"].version,
"5.1.1",
);
});
test("the Space smoke retries health, root and source readback", () => {
const steps = jobSteps("space");
assert.match(steps, /fetch_with_retry\(\)/);
assert.match(
steps,
/fetch_with_retry http:\/\/127\.0\.0\.1:7860\/healthz \/tmp\/nexus-health\.json/,
);
assert.match(steps, /fetch_with_retry http:\/\/127\.0\.0\.1:7860\/ \/tmp\/nexus-root\.html/);
assert.match(
steps,
/fetch_with_retry http:\/\/127\.0\.0\.1:7860\/api\/build-info \/tmp\/nexus-build\.json/,
);
assert.match(steps, /--connect-timeout 2 --max-time 10/);
});
test("the Space smoke waits for Docker health instead of sampling once", () => {
const steps = jobSteps("space");
assert.match(steps, /wait_for_docker_health\(\)/);
assert.match(steps, /for attempt in \$\(seq 1 60\)/);
assert.match(steps, /if \[ "\$state" = healthy \]/);
assert.match(steps, /if \[ "\$state" = unhealthy \]/);
assert.match(steps, /container health did not stabilize/);
assert.doesNotMatch(
steps,
/test "\$\(docker inspect --format '\{\{\.State\.Health\.Status\}\}' "\$name"\)" = healthy/,
);
});
test("the source alias resolves without deprecated baseUrl or a suppression", () => {
const { config, error } = ts.readConfigFile(join(ROOT, "tsconfig.json"), ts.sys.readFile);
assert.equal(error, undefined);
assert.equal(config.compilerOptions.baseUrl, undefined);
assert.equal(config.compilerOptions.ignoreDeprecations, undefined);
const parsed = ts.parseJsonConfigFileContent(config, ts.sys, ROOT);
assert.equal(parsed.errors.length, 0);
const resolved = ts.resolveModuleName(
"@/lib/nexus/types",
join(ROOT, "src/routes/index.tsx"),
parsed.options,
ts.sys,
).resolvedModule;
assert.ok(resolved, "the compiler must retain the @/ source alias");
assert.equal(
resolved.resolvedFileName.replaceAll("\\", "/"),
join(ROOT, "src/lib/nexus/types.ts").replaceAll("\\", "/"),
);
});
test("the node job runs on both Node LTS lines the repository uses", () => {
const steps = jobSteps("node");
assert.match(steps, /^ {8}node: \["22", "24"\]$/m);
assert.match(steps, /node-version: \$\{\{ matrix\.node \}\}/);
assert.match(steps, /fail-fast: false/);
});
test("npm test runs the cross-platform runner, not a shell glob", () => {
assert.equal(PACKAGE.scripts.test, "node --experimental-strip-types scripts/run-tests.mjs");
assert.doesNotMatch(PACKAGE.scripts.test, /[*']/);
});
test("the python job compiles, self-tests and runs pytest when tests exist", () => {
const steps = jobSteps("python");
assert.match(steps, /git ls-files -z -- '\*\.py' \| xargs -0 -r python -m py_compile/);
assert.match(steps, /run: python server\.py --selftest\n/);
assert.match(steps, /run: python source_bound_server\.py --selftest\n/);
assert.match(steps, /python -m pytest -q/);
});