169 lines
6.1 KiB
TypeScript
169 lines
6.1 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { loadCiGateContract } from "../../../scripts/contracts/ci-gates.ts";
|
|
|
|
const temporaryRoots: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryRoots.splice(0).map((root) =>
|
|
rm(root, { recursive: true, force: true }),
|
|
),
|
|
);
|
|
});
|
|
|
|
describe("CI-runner command-generated evidence freshness", () => {
|
|
it("rejects stale command-generated evidence from a successful no-op producer", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-gate-stale-evidence-"));
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
|
await mkdir(path.join(root, "artifacts/tests"), { recursive: true });
|
|
const contract = JSON.parse(
|
|
JSON.stringify(await loadCiGateContract(process.cwd())),
|
|
) as Record<string, any>;
|
|
const command = contract.commands.find(
|
|
(entry: Record<string, any>) => entry.id === "test-runtime-schema",
|
|
);
|
|
command.script = "test:stale-evidence-noop";
|
|
const evidence = contract.artifacts.find(
|
|
(entry: Record<string, any>) =>
|
|
entry.path === "artifacts/tests/runtime-schema.xml",
|
|
);
|
|
const packageDocument = JSON.parse(
|
|
await readFile("package.json", "utf8"),
|
|
) as { scripts: Record<string, string> };
|
|
packageDocument.scripts[command.script] = "true";
|
|
await writeFile(
|
|
path.join(root, "config/ci/gates.json"),
|
|
`${JSON.stringify(contract)}\n`,
|
|
);
|
|
await writeFile(
|
|
path.join(root, "package.json"),
|
|
`${JSON.stringify(packageDocument)}\n`,
|
|
);
|
|
await writeFile(
|
|
path.join(root, evidence.path),
|
|
'<testsuite name="stale" tests="0" failures="0"/>\n',
|
|
);
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[path.resolve("scripts/run-ci-gate.ts"), "FE-GATE-004"],
|
|
{ cwd: root, encoding: "utf8", env: { ...process.env, CI: "false" } },
|
|
);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(
|
|
await readFile(
|
|
path.join(root, "artifacts/quality/gates/FE-GATE-004.txt"),
|
|
"utf8",
|
|
),
|
|
).toMatch(/not freshly produced/i);
|
|
});
|
|
|
|
it("accepts a fresh deterministic rewrite with identical evidence bytes", async () => {
|
|
const root = await mkdtemp(
|
|
path.join(tmpdir(), "ci-gate-identical-rewrite-"),
|
|
);
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
|
await mkdir(path.join(root, "artifacts/tests"), { recursive: true });
|
|
const contract = JSON.parse(
|
|
JSON.stringify(await loadCiGateContract(process.cwd())),
|
|
) as Record<string, any>;
|
|
const command = contract.commands.find(
|
|
(entry: Record<string, any>) => entry.id === "test-runtime-schema",
|
|
);
|
|
command.script = "test:identical-evidence-rewrite";
|
|
const evidence = contract.artifacts.find(
|
|
(entry: Record<string, any>) =>
|
|
entry.path === "artifacts/tests/runtime-schema.xml",
|
|
);
|
|
const evidenceBytes =
|
|
'<testsuite name="deterministic" tests="0" failures="0"/>\n';
|
|
const packageDocument = JSON.parse(
|
|
await readFile("package.json", "utf8"),
|
|
) as { scripts: Record<string, string> };
|
|
packageDocument.scripts[command.script] =
|
|
`node -e 'require("node:fs").writeFileSync("${evidence.path}", Buffer.from("${Buffer.from(evidenceBytes).toString("base64")}", "base64"))'`;
|
|
await writeFile(
|
|
path.join(root, "config/ci/gates.json"),
|
|
`${JSON.stringify(contract)}\n`,
|
|
);
|
|
await writeFile(
|
|
path.join(root, "package.json"),
|
|
`${JSON.stringify(packageDocument)}\n`,
|
|
);
|
|
await writeFile(path.join(root, evidence.path), evidenceBytes);
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[path.resolve("scripts/run-ci-gate.ts"), "FE-GATE-004"],
|
|
{ cwd: root, encoding: "utf8", env: { ...process.env, CI: "false" } },
|
|
);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toMatch(/FE-GATE-004 runtime-schema: PASS/);
|
|
});
|
|
});
|
|
|
|
describe("CI-runner gate output budget", () => {
|
|
it("caps aggregate gate output at the log schema before later commands can accumulate", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-gate-output-budget-"));
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
|
const contract = JSON.parse(
|
|
JSON.stringify(await loadCiGateContract(process.cwd())),
|
|
) as Record<string, any>;
|
|
const gate = contract.gates.find(
|
|
(entry: Record<string, any>) => entry.id === "FE-GATE-001",
|
|
);
|
|
const command = contract.commands.find(
|
|
(entry: Record<string, any>) => entry.id === gate.commandIds[0],
|
|
);
|
|
command.script = "test:huge-output";
|
|
const logArtifact = contract.artifacts.find(
|
|
(entry: Record<string, any>) => entry.id === gate.logArtifactId,
|
|
);
|
|
const logSchema = contract.artifactSchemas.find(
|
|
(entry: Record<string, any>) => entry.id === logArtifact.schemaId,
|
|
);
|
|
logSchema.maxBytes = 8_192;
|
|
const packageDocument = JSON.parse(
|
|
await readFile("package.json", "utf8"),
|
|
) as { scripts: Record<string, string> };
|
|
packageDocument.scripts["test:huge-output"] =
|
|
"node -e \"process.stdout.write('x'.repeat(20000))\"";
|
|
await writeFile(
|
|
path.join(root, "config/ci/gates.json"),
|
|
`${JSON.stringify(contract)}\n`,
|
|
);
|
|
await writeFile(
|
|
path.join(root, "package.json"),
|
|
`${JSON.stringify(packageDocument)}\n`,
|
|
);
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[path.resolve("scripts/run-ci-gate.ts"), "FE-GATE-001"],
|
|
{
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: { ...process.env, CI: "false" },
|
|
timeout: 15_000,
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe(1);
|
|
const log = await readFile(path.join(root, logArtifact.path));
|
|
expect(log.byteLength).toBeLessThanOrEqual(8_192);
|
|
expect(log.toString("utf8")).toMatch(
|
|
/aggregate output|INFRASTRUCTURE_FAILURE/i,
|
|
);
|
|
}, 20_000);
|
|
});
|