Files
tech-log-frontend/tests/unit/risk-coverage-files.test.ts

373 lines
12 KiB
TypeScript

import { constants } from "node:fs";
import {
mkdir,
link,
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 mkdir(path.join(repositoryRoot, "real-config"));
await writeFile(path.join(repositoryRoot, "real-config/inside.json"), "{}\n");
await symlink(
path.join(repositoryRoot, "real-config"),
path.join(repositoryRoot, "inside-alias"),
"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);
await expect(
readRiskCoverageInput({
repositoryRoot,
relativePath: "inside-alias/inside.json",
label: "policy",
}),
).rejects.toThrow(/ancestor is a symlink/u);
});
it("rejects an input identity swap between lstat and open", async () => {
const repositoryRoot = await fixture();
const outside = await mkdtemp(path.join(tmpdir(), "risk-coverage-input-race-"));
roots.push(outside);
const replacement = path.join(outside, "replacement.json");
await writeFile(replacement, "{\"replacement\":true}\n");
await expect(
readRiskCoverageInput(
{
repositoryRoot,
relativePath: "config/testing/policy.json",
label: "policy",
},
{
openFile: async (_target, flags) => open(replacement, flags),
},
),
).rejects.toThrow(/changed during validation/u);
});
it("fails closed when an opened input has no stable file identity", async () => {
const repositoryRoot = await fixture();
await expect(
readRiskCoverageInput(
{
repositoryRoot,
relativePath: "config/testing/policy.json",
label: "policy",
},
{
openFile: async (target, flags) => {
const handle = await open(target, flags);
return {
stat: async () => {
const metadata = await handle.stat();
Object.defineProperties(metadata, {
dev: { value: 0 },
ino: { value: 0 },
});
return metadata;
},
readFile: async (encoding) => handle.readFile(encoding),
close: async () => handle.close(),
};
},
},
),
).rejects.toThrow(/stable file identity unavailable/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("rejects input overwrite through a realpath or hard-link alias", 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, "{}\n");
const hardLinkInput = path.join(repositoryRoot, "config/testing/output-alias.json");
await link(destination, hardLinkInput);
await expect(
resolveRiskCoverageArtifactPath({
repositoryRoot,
relativePath: "artifacts/quality/risk-coverage.json",
inputPaths: ["config/testing/output-alias.json"],
}),
).rejects.toThrow(/same file as an input/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.each([
["EINVAL", true],
["ENOTSUP", true],
["EIO", false],
] as const)(
"handles directory sync error %s with an explicit portability fallback",
async (code, accepted) => {
const repositoryRoot = await fixture();
const operation = writeRiskCoverageArtifactAtomic(
{
repositoryRoot,
relativePath: `artifacts/quality/sync-${code}.json`,
inputPaths: [],
value: { schemaVersion: 2 },
},
{
createNonce: () => code,
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 () => {
throw Object.assign(new Error(`sync ${code}`), { code });
},
close: async () => handle.close(),
};
},
rename,
rm,
},
},
);
if (accepted) {
await expect(operation).resolves.toBeUndefined();
} else {
await expect(operation).rejects.toMatchObject({ code });
}
},
);
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);
});
});