fix(process): resolve POSIX backslash basenames through PATH (#2471)

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Rudy Mizrahi Celekli 2026-10-13 08:51:44 -04:00 committed by GitHub
parent 0ca70e121c
commit 03ab521db3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 55 additions and 2 deletions

View File

@ -17,6 +17,28 @@ import {
resolveProcessScenarioRuntimePaths,
} from "./ProcessScenarioRuntimeValidation.js";
import { parseProcessScenario } from "../../domain/process/processScenario.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
it.skipIf(process.platform === "win32")(
"resolves a literal backslash in a POSIX executable basename through PATH",
async () => {
const root = await createTestTempDirectory("rea-process-posix-basename-");
const bin = join(root, "bin");
await mkdir(bin);
const command = "selected\\node";
const selected = join(bin, command);
await symlink(process.execPath, selected);
await expect(resolveExecutable(command, root, bin)).resolves.toBe(selected);
const resolved = await resolveProcessScenarioRuntimePaths(
parseProcessScenario({
executable: command,
working_directory: root,
environment: { PATH: bin },
}),
);
expect(resolved.executable).toBe(selected);
},
);
it("preserves the caller-selected executable symlink for process invocation", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-process-executable-alias-"));

View File

@ -1,6 +1,6 @@
import { constants as fsConstants } from "node:fs";
import { access, realpath, stat } from "node:fs/promises";
import { delimiter, isAbsolute, join, resolve } from "node:path";
import { delimiter, isAbsolute, join, resolve, sep } from "node:path";
import type { ProcessScenario } from "../../domain/process/processScenario.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
@ -71,7 +71,9 @@ export const resolveExecutable = async (
: resolve(workingDirectory, directory),
);
const candidates =
isAbsolute(command) || command.includes("/") || command.includes("\\")
isAbsolute(command) ||
command.includes("/") ||
(sep === "\\" && command.includes("\\"))
? [resolve(workingDirectory, command)]
: directories.map((directory) => join(directory, command));
const failures: string[] = [];

View File

@ -28,6 +28,35 @@ const identitySchema = z.object({
const itWithLinuxCaptureCapability =
process.platform === "linux" ? itWithCaptureCapability : it.skip;
itWithCaptureCapability(
"captures a POSIX PATH executable with a literal backslash in its basename",
async () => {
const root = await createTestTempDirectory("rea-process-backslash-name-");
const bin = join(root, "bin");
await mkdir(bin);
const command = "selected\\node";
const selected = join(bin, command);
await symlink(process.execPath, selected);
const scenario = parseProcessScenario({
executable: command,
working_directory: root,
environment: { PATH: bin },
arguments: ["-e", 'process.stdout.write("selected executable")'],
settle_ms: 100,
});
const result = await captureProcessScenario(scenario);
if (!result.ok) throw result.error;
expect(result.value.frames.map(({ data }) => data).join("")).toBe(
"selected executable",
);
expect(result.value.exit).toMatchObject({ code: 0, reason: "exited" });
expect(result.value.cleanup).toMatchObject({
owned_process_group: "verified",
temporary_root: "removed",
});
},
);
itWithCaptureCapability(
"preserves numeric reports and original normalized PTY text in Evidence",
async () => {