Files
rea/tests/boundary/filesystem/verifierFileSnapshots.test.ts
T

280 lines
10 KiB
TypeScript

import {
access,
chmod,
readFile,
symlink,
unlink,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createPackageVerifierCommands } from "../../../scripts/lib/verify-package-commands.mjs";
import {
assertVerifierFilesUnchanged,
captureVerifierFileSnapshot,
} from "../../../scripts/lib/verifier-file-snapshots.mjs";
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
import {
verifyInteractiveSetup,
verifyRedirectedStdoutSetup,
} from "../../../scripts/lib/verify-setup-terminal.mjs";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { processTest } from "../../support/process/processFixture.js";
const snapshotCommands = (root: string) =>
createPackageVerifierCommands({
workspace: { path: root, close: async () => {} },
});
it("preserves selected path spelling, UTF-8 bytes and missing-file facts", async () => {
const root = await createTestTempDirectory("rea-verifier-snapshot-");
const path = `${root}/./selected file.json`;
const missing = join(root, "missing file.json");
const content = '\uFEFF{"label":"保留","value":"\\u0000"}\r\n';
await writeFile(path, content);
const commands = await snapshotCommands(root);
const snapshot = await withVerifierCleanup(
() => captureVerifierFileSnapshot(commands, [missing, path]),
[() => commands.close()],
);
expect(snapshot).toEqual([
{ path: missing, exists: false, content: null },
{ path, exists: true, content: Buffer.from(content, "utf8") },
]);
expect(
Buffer.compare(await readFile(path), Buffer.from(content, "utf8")),
).toBe(0);
});
it("detects distinct invalid UTF-8 bytes in verifier file readback", async () => {
const root = await createTestTempDirectory("rea-verifier-byte-readback-");
const path = join(root, "selected.json");
await writeFile(path, Buffer.from([0x80]));
const commands = await snapshotCommands(root);
const rejection = await withVerifierCleanup(async () => {
const snapshot = await captureVerifierFileSnapshot(commands, [path]);
await writeFile(path, Buffer.from([0x81]));
return assertVerifierFilesUnchanged(commands, snapshot, "Setup").then(
() => undefined,
(cause: unknown) => cause,
);
}, [() => commands.close()]);
expect(rejection).toBeInstanceOf(Error);
expect(rejection).toHaveProperty("message", `Setup changed ${path}`);
});
it.each(["content", "removed", "created"] as const)(
"detects a %s change after an unchanged readback",
async (change) => {
const root = await createTestTempDirectory("rea-verifier-readback-");
const path = join(root, "selected.json");
if (change !== "created") await writeFile(path, "original\n");
const commands = await snapshotCommands(root);
const rejection = await withVerifierCleanup(async () => {
const snapshot = await captureVerifierFileSnapshot(commands, [path]);
await assertVerifierFilesUnchanged(commands, snapshot, "Setup");
if (change === "removed") await unlink(path);
else await writeFile(path, "changed\n");
return assertVerifierFilesUnchanged(commands, snapshot, "Setup").then(
() => undefined,
(cause: unknown) => cause,
);
}, [() => commands.close()]);
expect(rejection).toBeInstanceOf(Error);
expect(rejection).toHaveProperty("message", `Setup changed ${path}`);
},
);
it.skipIf(process.platform === "win32")(
"rejects a direct broken symlink instead of recording an absent file",
async () => {
const root = await createTestTempDirectory("rea-verifier-broken-link-");
const path = join(root, "selected.json");
await symlink(join(root, "missing-target.json"), path);
const commands = await snapshotCommands(root);
const rejection = await withVerifierCleanup(
() =>
captureVerifierFileSnapshot(commands, [path]).then(
() => undefined,
(cause: unknown) => cause,
),
[() => commands.close()],
);
expect(rejection).toHaveProperty("code", "ENOENT");
},
);
it.skipIf(process.platform === "win32").each([
["interactive", verifyInteractiveSetup],
["redirected stdout", verifyRedirectedStdoutSetup],
] as const)(
"finishes the %s baseline before launching setup",
async (_mode, verify) => {
const root = await createTestTempDirectory("rea-verifier-setup-baseline-");
const path = join(root, "selected.json");
const marker = join(root, "launched");
const command = join(root, "setup-command");
await symlink(join(root, "missing-target.json"), path);
await writeFile(
command,
'#!/bin/sh\nprintf launched > "$REA_SETUP_LAUNCH_MARKER"\n',
);
await chmod(command, 0o700);
const commands = await snapshotCommands(root);
const rejection = await withVerifierCleanup(
() =>
verify({
commands,
command,
environment: { ...process.env, REA_SETUP_LAUNCH_MARKER: marker },
root,
unchangedPaths: [path],
}).then(
() => undefined,
(cause: unknown) => cause,
),
[() => commands.close()],
);
const launched = await access(marker).then(
() => true,
(cause: unknown) => {
if (
cause instanceof Error &&
"code" in cause &&
cause.code === "ENOENT"
)
return false;
throw cause;
},
);
expect(rejection).toHaveProperty("code", "ENOENT");
expect(launched).toBe(false);
},
);
processTest(
"retains the exact failed file scope and directory until descriptor close is retried",
async ({ processes }) => {
const program = String.raw`
import assert from "node:assert/strict";
import fs from "node:fs";
import { access, writeFile } from "node:fs/promises";
import { syncBuiltinESMExports } from "node:module";
import { join } from "node:path";
import { tsImport } from "tsx/esm/api";
import { createPackageVerifierCommands } from ${JSON.stringify(new URL("../../../scripts/lib/verify-package-commands.mjs", import.meta.url).href)};
import { captureVerifierFileSnapshot } from ${JSON.stringify(new URL("../../../scripts/lib/verifier-file-snapshots.mjs", import.meta.url).href)};
import { withVerifierCleanup } from ${JSON.stringify(new URL("../../../scripts/lib/verifier-cleanup.mjs", import.meta.url).href)};
const { createTestWorkspace, removeTestWorkspace } = await tsImport(
${JSON.stringify(new URL("../../support/workspace/workspaceFixture.ts", import.meta.url).href)},
${JSON.stringify(import.meta.url)},
);
const workspace = await createTestWorkspace("rea-verifier-snapshot-owner-");
const path = join(workspace.root, "input.json");
const originalOpen = fs.promises.open;
let commands;
let handle;
let realClose;
let reads = 0;
let contents;
let attempts = 0;
const scopes = [];
const closeFailure = new Error("controlled real descriptor close failure");
try {
await writeFile(path, "source bytes\n");
fs.promises.open = async (...arguments_) => {
const opened = await originalOpen(...arguments_);
if (arguments_[0] === path) {
handle = opened;
const close = opened.close.bind(opened);
realClose = close;
const read = opened.readFile.bind(opened);
opened.readFile = async (...options) => {
reads++;
contents = await read(...options);
return contents;
};
opened.close = async () => {
attempts++;
if (attempts === 1) throw closeFailure;
await close();
};
}
return opened;
};
syncBuiltinESMExports();
commands = await createPackageVerifierCommands({
workspace: { path: workspace.root, close: () => removeTestWorkspace(workspace.root) },
});
const adopt = commands.adopt.bind(commands);
commands.adopt = (scope, resource) => {
scopes.push(scope);
adopt(scope, resource);
};
const observed = await withVerifierCleanup(async () => {
const failure = await captureVerifierFileSnapshot(commands, [path]).then(
() => undefined,
error => error,
);
await access(workspace.root);
return {
failure,
scope: scopes[0],
retained: scopes[0]?.hasPending,
descriptor: handle?.fd,
attempts,
reads,
contents,
};
}, [() => commands.close()]);
const directoryExists = await access(workspace.root).then(
() => true,
error => { if (error.code === "ENOENT") return false; throw error; },
);
assert.ok(observed.failure instanceof AggregateError);
assert.equal(observed.failure.errors.length, 1);
const retainedFailure = observed.failure.errors[0];
assert.equal(scopes.length, 1);
assert.equal(observed.retained, true);
assert.ok(observed.descriptor >= 0);
assert.equal(observed.attempts, 1);
assert.equal(observed.reads, 1);
assert.deepEqual(observed.contents, Buffer.from("source bytes\n"));
assert.equal(retainedFailure.failedOwners[0].handle.handle, handle);
assert.deepEqual(retainedFailure.cleanup.resources, [path]);
assert.deepEqual(retainedFailure.cause.errors, [closeFailure]);
assert.equal(observed.scope.hasPending, false);
assert.equal(attempts, 2);
assert.equal(handle.fd, -1);
assert.equal(directoryExists, false);
console.log(JSON.stringify({ exactHandleRetried: true, directoryReleasedAfterRetry: true }));
} finally {
fs.promises.open = originalOpen;
syncBuiltinESMExports();
if (handle !== undefined) handle.close = realClose;
if (commands !== undefined) await commands.close();
else await removeTestWorkspace(workspace.root);
}
`;
const result = await processes.run(
process.execPath,
["--input-type=module", "--eval", program],
{ timeoutMs: 20_000 },
);
expect(result.exitCode, result.stderr).toBe(0);
expect(result.signal, result.stderr).toBeNull();
expect(JSON.parse(result.stdout)).toEqual({
exactHandleRetried: true,
directoryReleasedAfterRetry: true,
});
},
);