mirror of
https://github.com/morluto/rea
synced 2026-10-28 23:11:11 -04:00
280 lines
10 KiB
TypeScript
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,
|
|
});
|
|
},
|
|
);
|