fix: harden repository coverage evidence

This commit is contained in:
DongHyeonka
2026-08-02 08:20:25 +09:00
parent 5a73f7a1b5
commit 67cd37659d
6 changed files with 1178 additions and 509 deletions
+240
View File
@@ -0,0 +1,240 @@
import { constants } from "node:fs";
import {
mkdir,
mkdtemp,
open,
readFile,
readdir,
rename,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
readRiskCoverageInput,
resolveRiskCoverageArtifactPath,
writeRiskCoverageArtifactAtomic,
} from "../../scripts/lib/risk-coverage-files.ts";
const roots: string[] = [];
async function fixture(): Promise<string> {
const root = await mkdtemp(path.join(tmpdir(), "risk-coverage-files-"));
roots.push(root);
await mkdir(path.join(root, "config/testing"), { recursive: true });
await mkdir(path.join(root, "artifacts/tests/coverage"), { recursive: true });
await writeFile(path.join(root, "config/testing/policy.json"), "{\"policy\":true}\n");
await writeFile(path.join(root, "artifacts/tests/coverage/summary.json"), "{\"total\":{}}\n");
return root;
}
afterEach(async () => {
await Promise.all(
roots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
);
});
describe("risk coverage CLI files", () => {
it("reads only exact contained regular input files", async () => {
const repositoryRoot = await fixture();
await expect(
readRiskCoverageInput({
repositoryRoot,
relativePath: "config/testing/policy.json",
label: "policy",
}),
).resolves.toMatchObject({
relativePath: "config/testing/policy.json",
text: "{\"policy\":true}\n",
});
await expect(
readRiskCoverageInput({
repositoryRoot,
relativePath: path.join(repositoryRoot, "config/testing/policy.json"),
label: "policy",
}),
).rejects.toThrow(/repository-relative POSIX/u);
await expect(
readRiskCoverageInput({
repositoryRoot,
relativePath: "config\\testing\\policy.json",
label: "policy",
}),
).rejects.toThrow(/repository-relative POSIX/u);
});
it("rejects final and ancestor input symlinks", async () => {
const repositoryRoot = await fixture();
const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-input-outside-"));
roots.push(outside);
await writeFile(path.join(outside, "outside.json"), "{}\n");
await symlink(
path.join(outside, "outside.json"),
path.join(repositoryRoot, "config/testing/link.json"),
);
await symlink(outside, path.join(repositoryRoot, "linked-config"), "dir");
await expect(
readRiskCoverageInput({
repositoryRoot,
relativePath: "config/testing/link.json",
label: "policy",
}),
).rejects.toThrow(/symlink/u);
await expect(
readRiskCoverageInput({
repositoryRoot,
relativePath: "linked-config/outside.json",
label: "policy",
}),
).rejects.toThrow(/outside repository|symlink/u);
});
it("confines artifact output and rejects input overwrite or symlink ancestors", async () => {
const repositoryRoot = await fixture();
await expect(
resolveRiskCoverageArtifactPath({
repositoryRoot,
relativePath: "artifacts/quality/risk-coverage.json",
inputPaths: ["config/testing/policy.json", "artifacts/tests/coverage/summary.json"],
}),
).resolves.toBe(path.join(repositoryRoot, "artifacts/quality/risk-coverage.json"));
await expect(
resolveRiskCoverageArtifactPath({
repositoryRoot,
relativePath: "config/testing/result.json",
inputPaths: [],
}),
).rejects.toThrow(/artifacts\/quality/u);
await expect(
resolveRiskCoverageArtifactPath({
repositoryRoot,
relativePath: "artifacts/quality/risk-coverage.json",
inputPaths: ["artifacts/quality/risk-coverage.json"],
}),
).rejects.toThrow(/must not overwrite an input/u);
const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-output-outside-"));
roots.push(outside);
await rm(path.join(repositoryRoot, "artifacts/quality"), { recursive: true, force: true });
await symlink(outside, path.join(repositoryRoot, "artifacts/quality"), "dir");
await expect(
resolveRiskCoverageArtifactPath({
repositoryRoot,
relativePath: "artifacts/quality/risk-coverage.json",
inputPaths: [],
}),
).rejects.toThrow(/symlink/u);
});
it("syncs an exclusive sibling temp before atomic rename", async () => {
const repositoryRoot = await fixture();
const observed: string[] = [];
let observedFlags = 0;
await writeRiskCoverageArtifactAtomic(
{
repositoryRoot,
relativePath: "artifacts/quality/risk-coverage.json",
inputPaths: ["config/testing/policy.json", "artifacts/tests/coverage/summary.json"],
value: { schemaVersion: 2, status: "PASS" },
},
{
createNonce: () => "owned",
fileSystem: {
openFile: async (target, flags, mode) => {
observedFlags = flags;
const handle = await open(target, flags, mode);
return {
writeFile: async (data) => handle.writeFile(data, "utf8"),
sync: async () => {
observed.push("file-sync");
await handle.sync();
},
close: async () => handle.close(),
};
},
openDirectory: async (target) => {
const handle = await open(target, constants.O_RDONLY);
return {
sync: async () => {
observed.push("directory-sync");
await handle.sync();
},
close: async () => handle.close(),
};
},
rename: async (source, destination) => {
observed.push("rename");
await rename(source, destination);
},
rm,
},
},
);
expect(observedFlags & constants.O_EXCL).toBe(constants.O_EXCL);
expect(observedFlags & constants.O_NOFOLLOW).toBe(constants.O_NOFOLLOW);
expect(observed).toEqual(["file-sync", "rename", "directory-sync"]);
expect(
JSON.parse(
await readFile(
path.join(repositoryRoot, "artifacts/quality/risk-coverage.json"),
"utf8",
),
),
).toEqual({ schemaVersion: 2, status: "PASS" });
});
it("cleans its owned temp and preserves destination when publication fails", async () => {
const repositoryRoot = await fixture();
const outputDirectory = path.join(repositoryRoot, "artifacts/quality");
await mkdir(outputDirectory, { recursive: true });
const destination = path.join(outputDirectory, "risk-coverage.json");
await writeFile(destination, "previous\n");
await expect(
writeRiskCoverageArtifactAtomic(
{
repositoryRoot,
relativePath: "artifacts/quality/risk-coverage.json",
inputPaths: [],
value: { schemaVersion: 2 },
},
{
createNonce: () => "owned",
fileSystem: {
openFile: async (target, flags, mode) => {
const handle = await open(target, flags, mode);
return {
writeFile: async (data) => handle.writeFile(data, "utf8"),
sync: async () => handle.sync(),
close: async () => handle.close(),
};
},
openDirectory: async (target) => {
const handle = await open(target, constants.O_RDONLY);
return { sync: async () => handle.sync(), close: async () => handle.close() };
},
rename: async () => {
throw new Error("injected rename failure");
},
rm,
},
},
),
).rejects.toThrow(/injected rename failure/u);
await expect(readFile(destination, "utf8")).resolves.toBe("previous\n");
expect(await readdir(outputDirectory)).toEqual(["risk-coverage.json"]);
});
it("has no changed-files gate in the executable", async () => {
const source = await readFile("scripts/check-risk-coverage.ts", "utf8");
expect(source).not.toMatch(/changedFiles|changed-files/u);
});
});