fix: harden CI evidence and removal contracts
This commit is contained in:
@@ -123,6 +123,7 @@ describe("CI gate contract", () => {
|
||||
["duplicate stage id", (value: Record<string, any>) => value.stages.push({ ...value.stages[0] }), /duplicate stage id/i],
|
||||
["duplicate job id", (value: Record<string, any>) => value.jobs.push({ ...value.jobs[0] }), /duplicate job id/i],
|
||||
["duplicate artifact path", (value: Record<string, any>) => value.artifacts.push({ ...value.artifacts[0], id: "duplicate-path" }), /duplicate artifact path/i],
|
||||
["missing artifact production classification", (value: Record<string, any>) => delete value.artifacts[0].production, /production/i],
|
||||
["unknown command reference", (value: Record<string, any>) => value.gates[0].commandIds.push("missing-command"), /unknown command missing-command/i],
|
||||
["unknown artifact reference", (value: Record<string, any>) => (value.gates[0].logArtifactId = "missing-artifact"), /unknown artifact missing-artifact/i],
|
||||
["unknown schema reference", (value: Record<string, any>) => (value.artifacts[0].schemaId = "missing-schema"), /unknown artifact schema missing-schema/i],
|
||||
@@ -177,6 +178,27 @@ describe("CI gate contract", () => {
|
||||
await expect(loadCiGateContract(root)).rejects.toThrow(/missing package scripts/i);
|
||||
});
|
||||
|
||||
it("rejects swapped canonical gate command ownership", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-gate-shape-"));
|
||||
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 security = contract.gates.find((gate: Record<string, any>) => gate.id === "FE-GATE-013");
|
||||
const documentation = contract.gates.find((gate: Record<string, any>) => gate.id === "FE-GATE-017");
|
||||
[security.commandIds, documentation.commandIds] = [
|
||||
documentation.commandIds,
|
||||
security.commandIds,
|
||||
];
|
||||
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
||||
await writeFile(path.join(root, "package.json"), await readFile("package.json"));
|
||||
|
||||
await expect(loadCiGateContract(root)).rejects.toThrow(
|
||||
/canonical gate semantic shape|lacks a bound producer command/i,
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["check:artifact-schemas", "check:ci-workflow"])(
|
||||
"rejects a missing nested check:ci dependency: %s",
|
||||
async (removedScript) => {
|
||||
@@ -191,11 +213,29 @@ describe("CI gate contract", () => {
|
||||
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 expect(loadCiGateContract(root)).rejects.toThrow(
|
||||
/missing package scripts|package script graph invalid/i,
|
||||
/missing package scripts|package script graph invalid|canonical check:ci dependency/i,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["check:artifact-schemas", "check:ci-workflow"])(
|
||||
"rejects a no-op nested check:ci dependency: %s",
|
||||
async (bypassedScript) => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-script-meaning-"));
|
||||
temporaryRoots.push(root);
|
||||
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
||||
const contract = await loadCiGateContract(process.cwd());
|
||||
const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
packageDocument.scripts[bypassedScript] = "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 expect(loadCiGateContract(root)).rejects.toThrow(/canonical check:ci dependency/i);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["true bypass", "true"],
|
||||
["direct self recursion", "corepack pnpm check:ci"],
|
||||
@@ -292,6 +332,79 @@ describe("CI gate contract", () => {
|
||||
expect(log.byteLength).toBeLessThanOrEqual(8_192);
|
||||
expect(log.toString("utf8")).toMatch(/aggregate output|INFRASTRUCTURE_FAILURE/i);
|
||||
}, 20_000);
|
||||
|
||||
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 workflow generation", () => {
|
||||
@@ -311,6 +424,10 @@ describe("CI workflow generation", () => {
|
||||
);
|
||||
expect(first).not.toContain("process_dist_sha256");
|
||||
expect(first).toContain("persist-credentials: false");
|
||||
expect(
|
||||
first.match(/corepack pnpm install --frozen-lockfile --ignore-scripts/gu),
|
||||
).toHaveLength(9);
|
||||
expect(first).not.toMatch(/corepack pnpm install --frozen-lockfile$/mu);
|
||||
expect(first).toContain("verify-ci-candidate-archive.ts --archive");
|
||||
expect(first).toContain("--extract-to");
|
||||
expect(first).not.toMatch(/\btar\s+[^\n]*--extract/u);
|
||||
|
||||
Reference in New Issue
Block a user