Files
rea/src/process/capture/FilesystemCoverage.test.ts
T
morluto 415145d04f fix(process)!: retain per-file digest open failures
BREAKING CHANGE: Filesystem digest omission entries now require system_code; budget and changed-file omissions use null, and unavailable-file omissions preserve the operating-system error code.
2026-10-10 03:52:35 +08:00

151 lines
5.7 KiB
TypeScript

import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { parseProcessScenario } from "../../domain/process/processScenario.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { snapshotRoots } from "./FilesystemSnapshot.js";
import { classifyFilesystemEffects } from "./ProcessFilesystemEffects.js";
it.each(["after", "before"] as const)(
"keeps absence unknown when the %s snapshot omits an existing file",
async (partialSide) => {
const root = await createTestTempDirectory("rea-fs-partial-absence-");
await writeFile(join(root, "z.txt"), "unchanged");
const scenario = parseProcessScenario({
executable: process.execPath,
working_directory: root,
filesystem_observation_paths: [root],
limits: { files: 2 },
});
if (partialSide === "before") await writeFile(join(root, "a.txt"), "other");
const before = await snapshotRoots(scenario);
if (partialSide === "after") await writeFile(join(root, "a.txt"), "other");
else await rm(join(root, "a.txt"));
const after = await snapshotRoots(scenario);
expect(await readFile(join(root, "z.txt"), "utf8")).toBe("unchanged");
const effect = classifyFilesystemEffects(before, after).find(
({ path }) => path === "root_0:z.txt",
);
expect(effect?.status).toBe("unknown");
expect(effect).toMatchObject(
partialSide === "after"
? { before: { path: "root_0:z.txt" }, after: null }
: { before: null, after: { path: "root_0:z.txt" } },
);
},
);
it("preserves proven deletion in an exhausted root while another root is partial", async () => {
const root = await createTestTempDirectory("rea-fs-multiple-roots-");
const complete = join(root, "complete");
const partial = join(root, "partial");
await mkdir(complete);
await mkdir(partial);
await writeFile(join(complete, "removed.txt"), "removed");
await writeFile(join(partial, "first.txt"), "first");
await writeFile(join(partial, "second.txt"), "second");
const scenario = parseProcessScenario({
executable: process.execPath,
working_directory: root,
filesystem_observation_paths: [complete, partial],
limits: { files: 3 },
});
const before = await snapshotRoots(scenario);
await rm(join(complete, "removed.txt"));
const after = await snapshotRoots(scenario);
expect(before.truncated && after.truncated).toBe(true);
expect(
classifyFilesystemEffects(before, after).find(
({ path }) => path === "root_0:removed.txt",
)?.status,
).toBe("deleted");
});
it("distinguishes unavailable content hashes from completed path enumeration", async () => {
const root = await createTestTempDirectory("rea-fs-hash-coverage-");
await writeFile(join(root, "removed.txt"), "larger than the byte budget");
const scenario = parseProcessScenario({
executable: process.execPath,
working_directory: root,
filesystem_observation_paths: [root],
limits: { file_bytes: 1 },
});
const before = await snapshotRoots(scenario);
await rm(join(root, "removed.txt"));
const after = await snapshotRoots(scenario);
expect(before.truncated).toBe(true);
expect(
classifyFilesystemEffects(before, after).find(
({ path }) => path === "root_0:removed.txt",
),
).toMatchObject({ status: "deleted", before: { sha256: null }, after: null });
});
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"retains unreadable-file and sibling observations in a complete snapshot",
async () => {
const root = await createTestTempDirectory("rea-fs-unreadable-file-");
const unreadable = join(root, "unreadable.txt");
const readable = join(root, "readable.txt");
await writeFile(unreadable, "permission denied");
await writeFile(readable, "still observed");
await chmod(unreadable, 0);
const scenario = parseProcessScenario({
executable: process.execPath,
working_directory: root,
filesystem_observation_paths: [root],
});
try {
const snapshot = await snapshotRoots(scenario);
expect(snapshot.files.map(({ path }) => path)).toEqual([
"root_0:.",
"root_0:readable.txt",
"root_0:unreadable.txt",
]);
expect(
snapshot.files.find(({ path }) => path === "root_0:readable.txt")
?.sha256,
).not.toBeNull();
expect(
snapshot.files.find(({ path }) => path === "root_0:unreadable.txt"),
).toMatchObject({ type: "file", size: 17, sha256: null });
expect(snapshot.coverage.hash_omissions).toContainEqual({
path: "root_0:unreadable.txt",
size_bytes: 17,
remaining_budget_bytes:
scenario.limits.file_bytes - Buffer.byteLength("still observed"),
reason: "file_unavailable",
system_code: "EACCES",
});
expect(snapshot.completeRoots).toEqual(["root_0"]);
} finally {
await chmod(unreadable, 0o600);
await rm(root, { recursive: true, force: true });
}
},
);
it("preserves creation when the selected root was observed missing before capture", async () => {
const root = await createTestTempDirectory("rea-fs-missing-root-");
const file = join(root, "created.txt");
const scenario = parseProcessScenario({
executable: process.execPath,
working_directory: root,
filesystem_observation_paths: [file],
});
const before = await snapshotRoots(scenario);
await writeFile(file, "created");
const after = await snapshotRoots(scenario);
expect(before.files).toEqual([]);
expect(before.truncated).toBe(false);
expect(classifyFilesystemEffects(before, after)).toMatchObject([
{
path: "root_0:.",
status: "created",
before: null,
after: { type: "file" },
},
]);
});