diff --git a/src/process/capture/ProcessScenarioRuntimeValidation.test.ts b/src/process/capture/ProcessScenarioRuntimeValidation.test.ts index 1277b753f..7e5cb7e30 100644 --- a/src/process/capture/ProcessScenarioRuntimeValidation.test.ts +++ b/src/process/capture/ProcessScenarioRuntimeValidation.test.ts @@ -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-")); diff --git a/src/process/capture/ProcessScenarioRuntimeValidation.ts b/src/process/capture/ProcessScenarioRuntimeValidation.ts index 1ac9d2228..6d2978b5b 100644 --- a/src/process/capture/ProcessScenarioRuntimeValidation.ts +++ b/src/process/capture/ProcessScenarioRuntimeValidation.ts @@ -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[] = []; diff --git a/tests/boundary/process/processCaptureNormalization.test.ts b/tests/boundary/process/processCaptureNormalization.test.ts index 2837ccf40..159f84c93 100644 --- a/tests/boundary/process/processCaptureNormalization.test.ts +++ b/tests/boundary/process/processCaptureNormalization.test.ts @@ -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 () => {