Files
reverse-engineering-anything/tests/boundary/process/filesystemObservationRaceCli.test.ts
morlutoandrudrakshagarwal4200-ai c85b44e1b1 fix(process)!: retain partial filesystem snapshot observations
Keep expected readlink and follow-up lstat failures as typed entry omissions,
including the observed path and errno. Retain directory metadata and sibling
observations, distinguish known identity replacements, and keep cancellation
and unexpected I/O failures terminal. Reuse admitted root metadata and test
cancellation by descriptor state rather than a count of named lookups.

Adapt the symlink race handling proposed in #4587 at fae4e25539.

Closes #4575

BREAKING CHANGE: Filesystem enumeration omission reasons now also include
entry_unavailable and identity_changed. Clients validating the old literal
reason must accept the expanded capture schema.

Co-authored-by: rudrakshagarwal4200-ai <rudrakshagarwal4200@gmail.com>
2026-10-19 08:34:39 +08:00

167 lines
5.8 KiB
TypeScript

import { lstat, mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { CAPTURE_SKIP_REASON } from "./processCaptureCapability.js";
const captureCliTest = cliTest.skipIf(
CAPTURE_SKIP_REASON ||
(process.platform === "win32" ? "requires POSIX symlink semantics" : false),
);
const racePreload = `
import fs from "node:fs";
import { resolve } from "node:path";
import { syncBuiltinESMExports } from "node:module";
const cliEntry = process.env.REA_TEST_CLI_ENTRY;
if (cliEntry !== undefined && resolve(process.argv[1] ?? "") === cliEntry) {
const target = resolve(process.env.REA_TEST_SYMLINK ?? "");
const countPath = process.env.REA_TEST_READLINK_COUNT;
const removeOnSecondRead = process.env.REA_TEST_REMOVE_ON_SECOND_READ === "1";
let matchingReads = 0;
const originalReadlink = fs.promises.readlink;
fs.promises.readlink = async function (path, ...options) {
if (resolve(String(path)) === target) {
matchingReads += 1;
if (countPath !== undefined) fs.appendFileSync(countPath, "read\\n");
if (removeOnSecondRead && matchingReads === 2) {
await fs.promises.unlink(path);
}
}
return originalReadlink.call(this, path, ...options);
};
syncBuiltinESMExports();
}
`;
const createScenario = async (removeOnSecondRead: boolean) => {
const root = await createTestTempDirectory("rea-snapshot-race-cli-");
const observed = join(root, "observed");
const link = join(observed, "vanishing-link");
const scenario = join(root, "scenario.json");
const preload = join(root, "readlink-race-preload.mjs");
const readlinkCount = join(root, "readlink-count.txt");
await mkdir(observed);
await writeFile(join(observed, "sibling.txt"), "sibling evidence\n");
await symlink("target-does-not-need-to-exist", link);
await writeFile(preload, racePreload);
await writeFile(
scenario,
JSON.stringify({
executable: process.execPath,
arguments: ["-e", "process.stdout.write('controlled capture\\n')"],
working_directory: root,
filesystem_observation_paths: [observed],
}),
);
return { root, link, scenario, preload, readlinkCount, removeOnSecondRead };
};
captureCliTest(
"keeps a stable symlink observation complete through the public CLI",
async ({ cli }) => {
const fixture = await createScenario(false);
const result = await cli.run({
arguments: ["capture-process", fixture.scenario, "--json"],
cwd: fixture.root,
environment: {
NODE_OPTIONS: `--import=${pathToFileURL(fixture.preload).href}`,
REA_TEST_CLI_ENTRY: resolve("scripts/rea.mjs"),
REA_TEST_SYMLINK: fixture.link,
REA_TEST_READLINK_COUNT: fixture.readlinkCount,
REA_TEST_REMOVE_ON_SECOND_READ: fixture.removeOnSecondRead ? "1" : "0",
},
});
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(await readFile(fixture.readlinkCount, "utf8")).toBe("read\nread\n");
expect((await lstat(fixture.link)).isSymbolicLink()).toBe(true);
expect(result.json).toMatchObject({
normalized_result: {
truncated: false,
files_before: expect.arrayContaining([
expect.objectContaining({
path: "root_0:vanishing-link",
type: "symlink",
symlink_target: "target-does-not-need-to-exist",
}),
]),
files_after: expect.arrayContaining([
expect.objectContaining({
path: "root_0:vanishing-link",
type: "symlink",
symlink_target: "target-does-not-need-to-exist",
}),
]),
truncation_details: {
filesystem_after: { enumeration_truncated: false },
},
},
});
},
);
captureCliTest(
"retains sibling evidence and marks a vanished symlink unknown",
async ({ cli }) => {
const fixture = await createScenario(true);
const result = await cli.run({
arguments: ["capture-process", fixture.scenario, "--json"],
cwd: fixture.root,
environment: {
NODE_OPTIONS: `--import=${pathToFileURL(fixture.preload).href}`,
REA_TEST_CLI_ENTRY: resolve("scripts/rea.mjs"),
REA_TEST_SYMLINK: fixture.link,
REA_TEST_READLINK_COUNT: fixture.readlinkCount,
REA_TEST_REMOVE_ON_SECOND_READ: "1",
},
});
expect(await readFile(fixture.readlinkCount, "utf8")).toBe("read\nread\n");
await expect(readFile(fixture.link)).rejects.toMatchObject({
code: "ENOENT",
});
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(result.json).toMatchObject({
normalized_result: {
truncated: true,
files_before: expect.arrayContaining([
expect.objectContaining({
path: "root_0:vanishing-link",
type: "symlink",
}),
expect.objectContaining({ path: "root_0:sibling.txt", type: "file" }),
]),
files_after: expect.arrayContaining([
expect.objectContaining({ path: "root_0:sibling.txt", type: "file" }),
]),
filesystem_effects: expect.arrayContaining([
expect.objectContaining({
path: "root_0:vanishing-link",
status: "unknown",
after: null,
}),
]),
truncation_details: {
filesystem_after: {
enumeration_truncated: true,
enumeration_reasons: expect.arrayContaining(["entry_unavailable"]),
enumeration_omissions: [
{
path: "root_0:vanishing-link",
reason: "entry_unavailable",
system_code: "ENOENT",
},
],
},
},
},
});
},
);