Files
reverse-engineering-anything/tests/boundary/filesystem/doctorSystemHostFilesystem.test.ts
T
gensokyo-devandClaude Opus 5.5 5557d9b3eb fix(doctor): count installations by resolved launcher directory
On Windows `where rea` lists the npm shims rea, rea.cmd and rea.ps1 that
one global install writes side by side, and doctor counted each listed
path as an installation, so a single install reported "multiple".

The system host now pairs each PATH launcher with the installation it
resolves to: its directory after symlink resolution, compared
case-insensitively on Windows. Doctor counts distinct installations and
still lists every launcher path. Two POSIX launchers that resolve to one
package also count once; an unresolvable launcher keeps its own path.

Fixes #8122

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 05:36:32 +01:00

407 lines
13 KiB
TypeScript

import { access, mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { systemDoctorHost } from "../../../src/application/Doctor.js";
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
import { ExecFileOutputCancelledError } from "../../../src/process/ExecFileOutput.js";
it("threads the selected host and environment into system diagnostics", async () => {
const environment = {
HOME: "/selected/home",
HOPPER_LAUNCHER_PATH: "/selected/bin/hopper",
PATH: "/selected/bin",
};
const calls: Array<{
command: string;
arguments: readonly string[];
environment: NodeJS.ProcessEnv | undefined;
}> = [];
const host = systemDoctorHost({
platform: "linux",
architecture: "x64",
environment,
execFileOutput: async (command, arguments_, options) => {
expect(options).toMatchObject({
timeout: 10_000,
maxBuffer: 64 * 1024,
stopSignal: "SIGTERM",
signal: expect.any(AbortSignal),
});
calls.push({ command, arguments: arguments_, environment: options?.env });
if (command === "/usr/bin/sw_vers")
return {
stdout: "15.0\n",
stderr: "",
stdoutBytes: 5,
stderrBytes: 0,
};
if (command === "ldd")
return {
stdout: "linux-vdso.so.1",
stderr: "",
stdoutBytes: Buffer.byteLength("linux-vdso.so.1"),
stderrBytes: 0,
};
if (command === "which")
return {
stdout: "/selected/bin/rea\n",
stderr: "",
stdoutBytes: 18,
stderrBytes: 0,
};
throw new Error(`Unexpected diagnostic command: ${command}`);
},
});
expect(host.homeDirectory).toBe("/selected/home");
expect(host.platform).toBe("linux");
expect(host.architecture).toBe("x64");
await host.installationLaunchers?.();
expect(await host.macosVersion()).toBe("15.0");
await host.executable(process.execPath);
expect(calls.map(({ command }) => command)).toEqual([
"which",
"/usr/bin/sw_vers",
"ldd",
]);
expect(
calls.every(({ environment: observed }) => observed === environment),
).toBe(true);
});
it.each([
"macosVersion",
"installationLaunchers",
"brewHopperPath",
"executable",
"ilspyCmdVersion",
] as const)(
"preserves cancellation through the %s fallback",
async (operation) => {
const reason = { request: "stop diagnostics" };
const host = systemDoctorHost({
platform: "linux",
environment: {},
execFileOutput: async () => {
throw new ExecFileOutputCancelledError(reason);
},
});
try {
await expect(host[operation]?.(process.execPath)).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
});
} finally {
await host.close?.();
}
},
);
it.each(["complete", "failed"] as const)(
"retains a %s probe's outcome when cancellation arrives at completion",
async (outcome) => {
const controller = new AbortController();
const reason = { request: "late cancellation" };
const host = systemDoctorHost({
signal: controller.signal,
environment: {},
execFileOutput: async () => {
controller.abort(reason);
if (outcome === "failed")
throw Object.assign(new Error("probe timed out"), {
code: "ETIMEDOUT",
});
return {
stdout: "observed",
stderr: "warning",
stdoutBytes: 8,
stderrBytes: 7,
};
},
});
try {
const pending = host.installationLaunchers?.();
if (outcome === "failed") await expect(pending).resolves.toEqual([]);
else
await expect(pending).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
capturedOutput: {
stdout: "observed",
stderr: "warning",
stdout_bytes: 8,
stderr_bytes: 7,
exit_code: 0,
},
});
} finally {
await host.close?.();
}
},
);
it.skipIf(process.platform === "win32").each(["abort", "close"] as const)(
"%s joins an active diagnostic probe's exit and preserves cancellation",
async (action) => {
const home = await createTestTempDirectory("rea-doctor-probe-stop-");
const started = join(home, "started");
const cleaned = join(home, "cleaned");
await writeFile(
join(home, "which"),
`#!${process.execPath}
const fs = require("node:fs");
const timer = setTimeout(() => process.exit(91), 8000);
process.on("SIGTERM", () => {
clearTimeout(timer);
setTimeout(() => { fs.writeFileSync(${JSON.stringify(cleaned)}, "cleaned"); process.exit(0); }, 50);
});
fs.writeFileSync(${JSON.stringify(started)}, "ready");
`,
{ mode: 0o700 },
);
const controller = new AbortController();
const reason = { request: "cancel running probe" };
const host = systemDoctorHost({
environment: { HOME: home, PATH: home },
signal: controller.signal,
});
const pending = host.installationLaunchers!().then(
(value) => value,
(cause: unknown) => cause,
);
try {
await expect.poll(async () => readFile(started, "utf8")).toBe("ready");
if (action === "abort") controller.abort(reason);
else await host.close?.();
const failure = await pending;
expect(failure).toBeInstanceOf(AnalysisCancelledError);
if (action === "abort") expect((failure as Error).cause).toBe(reason);
expect(await readFile(cleaned, "utf8")).toBe("cleaned");
} finally {
controller.abort(reason);
await pending;
await host.close?.();
}
},
);
it.skipIf(process.platform === "win32")(
"does not launch a diagnostic for an already cancelled request",
async () => {
const home = await createTestTempDirectory("rea-doctor-pre-abort-");
const marker = join(home, "launched");
await writeFile(
join(home, "which"),
`#!${process.execPath}\nrequire("node:fs").writeFileSync(${JSON.stringify(marker)}, "launched");\n`,
{ mode: 0o700 },
);
const controller = new AbortController();
const reason = { request: "already cancelled" };
controller.abort(reason);
const host = systemDoctorHost({
environment: { HOME: home, PATH: home },
signal: controller.signal,
});
try {
await expect(host.installationLaunchers?.()).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
});
await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
await host.close?.();
}
},
);
it("does not report a directory as an available executable", async () => {
const host = systemDoctorHost({
platform: "darwin",
architecture: "arm64",
environment: {},
execFileOutput: () => Promise.reject(new Error("unexpected command")),
});
await expect(host.executable(dirname(process.execPath))).resolves.toBe(false);
await expect(host.executable(process.execPath)).resolves.toBe(true);
});
it.each([
[{ default: { appdir: "/Applications" } }, "/Applications"],
[
{
default: { appdir: "/Applications" },
env: { appdir: "/Env Apps" },
explicit: { appdir: "~/Tools" },
},
"HOME/Tools",
],
[
{ default: { appdir: "/Applications" }, env: { appdir: "/Env Apps" } },
"/Env Apps",
],
])(
"locates the Hopper cask from its Caskroom receipt without a cask command (%j)",
async (config, expectedAppdir) => {
const root = await createTestTempDirectory("rea-doctor-caskroom-");
const caskroom = join(root, "Caskroom");
const metadata = join(caskroom, "hopper-disassembler", ".metadata");
await mkdir(metadata, { recursive: true });
await writeFile(join(metadata, "config.json"), JSON.stringify(config));
const calls: (readonly string[])[] = [];
const host = systemDoctorHost({
platform: "darwin",
environment: { HOME: join(root, "home") },
execFileOutput: async (_command, arguments_) => {
calls.push(arguments_);
return {
stdout: `${caskroom}\n`,
stderr: "",
stdoutBytes: Buffer.byteLength(`${caskroom}\n`),
stderrBytes: 0,
};
},
});
expect(await host.brewHopperPath()).toBe(
join(
expectedAppdir.replace("HOME", join(root, "home")),
"Hopper Disassembler.app/Contents/MacOS/hopper",
),
);
// A cask argument makes Homebrew fetch and cache its API data.
expect(calls).toEqual([["--caskroom"]]);
},
);
it.skipIf(process.platform === "win32").each(["absolute", "home"])(
"preserves symlink traversal in the cask appdir (%s)",
async (spelling) => {
const root = await createTestTempDirectory("rea-doctor-cask-path-");
const home = join(root, "home");
const caskroom = join(root, "Caskroom");
const metadata = join(caskroom, "hopper-disassembler", ".metadata");
const target = join(root, "selected", "nested");
await mkdir(home, { recursive: true });
await mkdir(target, { recursive: true });
await mkdir(metadata, { recursive: true });
await symlink(target, join(home, "apps"));
const suffix = "Hopper Disassembler.app/Contents/MacOS/hopper";
const selected = join(root, "selected", suffix);
await mkdir(dirname(selected), { recursive: true });
await writeFile(selected, "selected Hopper");
const appdir = spelling === "home" ? "~/apps/.." : `${home}/apps/..`;
await writeFile(
join(metadata, "config.json"),
JSON.stringify({ explicit: { appdir } }),
);
const host = systemDoctorHost({
platform: "darwin",
environment: { HOME: home },
execFileOutput: async () => ({
stdout: `${caskroom}\n`,
stderr: "",
stdoutBytes: Buffer.byteLength(`${caskroom}\n`),
stderrBytes: 0,
}),
});
const discovered = await host.brewHopperPath();
expect(discovered).toBe(`${home}/apps/../${suffix}`);
expect(await readFile(discovered!, "utf8")).toBe("selected Hopper");
},
);
it("reports no Homebrew Hopper when the cask has no receipt", async () => {
const caskroom = await createTestTempDirectory("rea-doctor-caskroom-");
const calls: (readonly string[])[] = [];
const host = systemDoctorHost({
platform: "darwin",
environment: { HOME: caskroom },
execFileOutput: async (_command, arguments_) => {
calls.push(arguments_);
return {
stdout: `${caskroom}\n`,
stderr: "",
stdoutBytes: Buffer.byteLength(`${caskroom}\n`),
stderrBytes: 0,
};
},
});
expect(await host.brewHopperPath()).toBeUndefined();
expect(new Set(calls.map((arguments_) => arguments_.join(" ")))).toEqual(
new Set(["--caskroom"]),
);
});
it("groups the Windows npm shims `where` lists under one installation", async () => {
const npm = "C:\\Users\\user\\AppData\\Roaming\\npm";
const other = "D:\\tools\\rea";
const listed = [
`${npm}\\rea`,
`${npm}\\rea.cmd`,
`${npm}\\REA.ps1`,
`${other}\\rea.cmd`,
];
const host = systemDoctorHost({
platform: "win32",
architecture: "x64",
environment: { PATH: "C:\\Windows" },
execFileOutput: async (command) => {
if (command !== "where")
throw new Error(`Unexpected diagnostic command: ${command}`);
const stdout = `${listed.join("\r\n")}\r\n`;
return {
stdout,
stderr: "",
stdoutBytes: Buffer.byteLength(stdout),
stderrBytes: 0,
};
},
});
const launchers = (await host.installationLaunchers?.()) ?? [];
// Every listed launcher stays visible; identity groups them by directory.
expect(launchers.map(({ path }) => path)).toEqual(listed);
expect(new Set(launchers.map(({ installation }) => installation)).size).toBe(
2,
);
});
it.skipIf(process.platform === "win32")(
"resolves POSIX launchers that reach one package through symlinks",
async () => {
const root = await createTestTempDirectory("rea-doctor-launchers-");
const scripts = join(root, "lib/node_modules/rea-agents/scripts");
await mkdir(scripts, { recursive: true });
await writeFile(join(scripts, "rea.mjs"), "", { mode: 0o755 });
const launchers = [join(root, "bin/rea"), join(root, "home/bin/rea")];
for (const launcher of launchers) {
await mkdir(dirname(launcher), { recursive: true });
await symlink(join(scripts, "rea.mjs"), launcher);
}
const host = systemDoctorHost({
platform: "linux",
architecture: "x64",
environment: { PATH: "/selected/bin" },
execFileOutput: async () => {
const stdout = `${launchers.join("\n")}\n`;
return {
stdout,
stderr: "",
stdoutBytes: Buffer.byteLength(stdout),
stderrBytes: 0,
};
},
});
const resolved = (await host.installationLaunchers?.()) ?? [];
expect(resolved.map(({ path }) => path)).toEqual(launchers);
expect(new Set(resolved.map(({ installation }) => installation)).size).toBe(
1,
);
},
);