chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
import { mkdtemp, mkdir, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildRepositoryFileInventory,
|
||||
parseRepositoryFileInventoryPolicy,
|
||||
type GitFileListResult,
|
||||
} from "../../scripts/lib/repository-file-inventory.ts";
|
||||
|
||||
function gitResult(
|
||||
stdout: Buffer | string,
|
||||
overrides: Partial<GitFileListResult> = {},
|
||||
): GitFileListResult {
|
||||
return {
|
||||
status: 0,
|
||||
signal: null,
|
||||
stdout: typeof stdout === "string" ? Buffer.from(stdout) : stdout,
|
||||
stderr: Buffer.alloc(0),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function repositoryFixture() {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "repository-inventory-"));
|
||||
await mkdir(path.join(root, "src"));
|
||||
await mkdir(path.join(root, "docs"));
|
||||
await writeFile(path.join(root, "src", "tracked.ts"), "tracked\n");
|
||||
await writeFile(path.join(root, "src", "untracked.ts"), "untracked\n");
|
||||
await writeFile(path.join(root, "README.md"), "readme\n");
|
||||
await writeFile(path.join(root, "docs", "outside-policy.md"), "docs\n");
|
||||
return root;
|
||||
}
|
||||
|
||||
describe("repository file inventory", () => {
|
||||
it("rejects malformed root policies instead of filtering invalid entries", () => {
|
||||
expect(() =>
|
||||
parseRepositoryFileInventoryPolicy({
|
||||
trackedRoots: ["src", 42],
|
||||
generatedRoots: [],
|
||||
}),
|
||||
).toThrow(/trackedRoots/u);
|
||||
expect(() =>
|
||||
parseRepositoryFileInventoryPolicy({
|
||||
trackedRoots: [],
|
||||
generatedRoots: [],
|
||||
}),
|
||||
).toThrow(/trackedRoots/u);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["spawn failure", { error: new Error("spawn ENOENT") }],
|
||||
["non-zero exit", { status: 2, stderr: Buffer.from("fatal") }],
|
||||
["signal", { status: null, signal: "SIGTERM" }],
|
||||
["stderr output", { stderr: Buffer.from("warning") }],
|
||||
] as const)("fails closed on git %s", async (_name, failure) => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
runGit: () => gitResult("src/tracked.ts\0", failure),
|
||||
}),
|
||||
).rejects.toThrow(/git ls-files/u);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["malformed UTF-8", Buffer.from([0xc3, 0x28, 0])],
|
||||
["embedded empty NUL row", Buffer.from("src/tracked.ts\0\0")],
|
||||
["missing terminal NUL", Buffer.from("src/tracked.ts")],
|
||||
])("rejects %s output", async (_name, stdout) => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
runGit: () => gitResult(stdout),
|
||||
}),
|
||||
).rejects.toThrow(/git ls-files/u);
|
||||
});
|
||||
|
||||
it("uses only tracked files and sorts the normalized inventory", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
const inventory = await buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
runGit: () => gitResult("src/tracked.ts\0"),
|
||||
});
|
||||
|
||||
expect(inventory.trackedFiles).toEqual(["src/tracked.ts"]);
|
||||
expect(inventory.files).not.toContain("src/untracked.ts");
|
||||
});
|
||||
|
||||
it("includes every Git-tracked file outside mandatory policy roots", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
const inventory = await buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
runGit: () =>
|
||||
gitResult("src/tracked.ts\0README.md\0docs/outside-policy.md\0"),
|
||||
});
|
||||
|
||||
expect(inventory.trackedFiles).toEqual([
|
||||
"README.md",
|
||||
"docs/outside-policy.md",
|
||||
"src/tracked.ts",
|
||||
]);
|
||||
expect(inventory.files).toEqual(inventory.trackedFiles);
|
||||
});
|
||||
|
||||
it("rejects duplicate tracked paths instead of silently deduplicating", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
runGit: () => gitResult("src/tracked.ts\0src/tracked.ts\0"),
|
||||
}),
|
||||
).rejects.toThrow(/duplicate/u);
|
||||
});
|
||||
|
||||
it("fails when a required root is missing", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["missing"],
|
||||
runGit: () => gitResult(""),
|
||||
}),
|
||||
).rejects.toThrow(/required repository root.*missing/u);
|
||||
});
|
||||
|
||||
it("fails when an existing required root has no tracked match", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
runGit: () => gitResult(""),
|
||||
}),
|
||||
).rejects.toThrow(/tracked file.*src/u);
|
||||
});
|
||||
|
||||
it("ignores only exact ENOENT for configured optional generated roots", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
generatedRoots: ["optional-output"],
|
||||
optionalRoots: ["optional-output"],
|
||||
runGit: () => gitResult("src/tracked.ts\0"),
|
||||
}),
|
||||
).resolves.toMatchObject({ generatedFiles: [] });
|
||||
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
generatedRoots: ["optional-output"],
|
||||
optionalRoots: ["optional-output"],
|
||||
runGit: () => gitResult("src/tracked.ts\0"),
|
||||
lstatPath: async (target) => {
|
||||
if (target.endsWith("optional-output")) {
|
||||
throw Object.assign(new Error("denied"), { code: "EACCES" });
|
||||
}
|
||||
const { lstat } = await import("node:fs/promises");
|
||||
return lstat(target);
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/optional-output/u);
|
||||
});
|
||||
|
||||
it.each(["/absolute", "../escape", "src\\windows.ts"])(
|
||||
"rejects unsafe configured path %s",
|
||||
async (unsafePath) => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: [unsafePath],
|
||||
runGit: () => gitResult(""),
|
||||
}),
|
||||
).rejects.toThrow(/repository-relative POSIX path/u);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects tracked traversal and non-regular files", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
for (const trackedPath of ["../escape", "src"]) {
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
runGit: () => gitResult(`${trackedPath}\0`),
|
||||
}),
|
||||
).rejects.toThrow(/git ls-files|regular file/u);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a tracked symlink whose real path escapes the repository", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
const outside = await mkdtemp(path.join(tmpdir(), "inventory-outside-"));
|
||||
await writeFile(path.join(outside, "secret.ts"), "secret\n");
|
||||
await symlink(
|
||||
path.join(outside, "secret.ts"),
|
||||
path.join(repositoryRoot, "src", "link.ts"),
|
||||
);
|
||||
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
runGit: () => gitResult("src/link.ts\0"),
|
||||
}),
|
||||
).rejects.toThrow(/symlink|regular file/u);
|
||||
});
|
||||
|
||||
it("adds only explicitly configured generated regular files", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await mkdir(path.join(repositoryRoot, "dist"));
|
||||
await writeFile(path.join(repositoryRoot, "dist", "asset.js"), "asset\n");
|
||||
|
||||
const inventory = await buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
generatedRoots: ["dist"],
|
||||
runGit: () => gitResult("src/tracked.ts\0"),
|
||||
});
|
||||
|
||||
expect(inventory.generatedFiles).toEqual(["dist/asset.js"]);
|
||||
expect(inventory.files).toEqual(["dist/asset.js", "src/tracked.ts"]);
|
||||
});
|
||||
|
||||
it("rejects an exact tracked and generated path collision", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await writeFile(path.join(repositoryRoot, "package.json"), "{}\n");
|
||||
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["package.json"],
|
||||
generatedRoots: ["package.json"],
|
||||
runGit: () => gitResult("package.json\0"),
|
||||
}),
|
||||
).rejects.toThrow(/tracked.*generated.*package\.json/u);
|
||||
});
|
||||
|
||||
it("accepts a legal child whose name begins with two dots", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await mkdir(path.join(repositoryRoot, "..assets"));
|
||||
await writeFile(path.join(repositoryRoot, "..assets", "legal.ts"), "legal\n");
|
||||
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["..assets"],
|
||||
runGit: () => gitResult("..assets/legal.ts\0"),
|
||||
}),
|
||||
).resolves.toMatchObject({ trackedFiles: ["..assets/legal.ts"] });
|
||||
});
|
||||
|
||||
it("fails closed when an inventoried file cannot be read", async () => {
|
||||
const repositoryRoot = await repositoryFixture();
|
||||
await expect(
|
||||
buildRepositoryFileInventory({
|
||||
repositoryRoot,
|
||||
trackedRoots: ["src"],
|
||||
runGit: () => gitResult("src/tracked.ts\0"),
|
||||
assertReadable: async () => {
|
||||
throw Object.assign(new Error("denied"), { code: "EACCES" });
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/src\/tracked\.ts/u);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user