Files
fusion/plugins/fusion-plugin-hermes-runtime/src/__tests__/windows-binary-launch.test.ts
gsxdsm 743251a5d3 FN-9099: harden Hermes Windows CLI launches
Reliably resolve and safely launch operator-installed Hermes binaries on Windows.

- Resolve Windows Hermes shims using PATH and PATHEXT-aware lookup with bounded caching
- Launch .cmd and .bat shims through escaped cmd.exe payloads while preserving direct executable launches
- Supervise CLI turns and cover Windows shim, probe, cache, timeout, and injection behavior

Files changed:
 .changeset/fn-9099-hermes-windows-launch.md        |   7 +
 plugins/fusion-plugin-hermes-runtime/README.md     |   4 +
 plugins/fusion-plugin-hermes-runtime/package.json  |   1 +
 .../src/__tests__/cli-spawn.test.ts                | 139 +++++++++++++++-
 .../src/__tests__/probe.test.ts                    |  61 +++++++
 .../src/__tests__/windows-binary-launch.test.ts    |  64 +++++++
 .../fusion-plugin-hermes-runtime/src/cli-spawn.ts  |  72 ++++----
 plugins/fusion-plugin-hermes-runtime/src/index.ts  |   8 +
 plugins/fusion-plugin-hermes-runtime/src/probe.ts  |  19 ++-
 .../src/windows-binary-launch.ts                   | 184 +++++++++++++++++++++
 pnpm-lock.yaml                                     |   3 +
 11 files changed, 508 insertions(+), 54 deletions(-)

Fusion-Task-Id: FN-9099

Fusion-Task-Lineage: 37b1f255-07ce-4952-aac6-1556890849be

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 12:38:24 -07:00

65 lines
2.8 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import {
__resetHermesLaunchCacheForTests,
escapeWindowsShellArgument,
escapeWindowsShellCommand,
resolveHermesBinaryPath,
resolveHermesLaunch,
} from "../windows-binary-launch.js";
const windows = { platform: "win32" as const, env: { PATH: "C:\\shims", PATHEXT: ".COM;.EXE;.BAT;.CMD" } };
afterEach(() => __resetHermesLaunchCacheForTests());
describe("Windows Hermes launch resolution", () => {
it("keeps POSIX launches direct and does not invoke where", async () => {
let calls = 0;
await expect(resolveHermesLaunch("hermes", ["chat"], {
platform: "linux", runWhere: async () => { calls += 1; return "C:\\shims\\hermes.cmd"; },
})).resolves.toEqual({ command: "hermes", args: ["chat"] });
expect(calls).toBe(0);
});
it("selects PATHEXT-preferred candidates in the first Windows directory on a POSIX host", async () => {
const launch = await resolveHermesLaunch("hermes", ["chat", "hi"], {
...windows,
runWhere: async () => "C:\\Shims\\hermes.CMD\r\nc:\\shims\\hermes.EXE\r\nC:\\later\\hermes.COM\r\n",
});
expect(launch).toMatchObject({ command: "c:\\shims\\hermes.EXE", args: ["chat", "hi"], resolvedBinaryPath: "c:\\shims\\hermes.EXE" });
});
it("wraps cmd shims in a hardened cmd.exe payload", async () => {
const launch = await resolveHermesLaunch("hermes.cmd", ["chat", "hi\" & calc.exe", ""], {
...windows,
env: { ...windows.env, ComSpec: "C:\\Windows\\System32\\cmd.exe" },
runWhere: async () => "C:\\Users\\A User\\hermes.cmd",
});
expect(launch.command).toBe("C:\\Windows\\System32\\cmd.exe");
expect(launch.args.slice(0, 3)).toEqual(["/d", "/s", "/c"]);
expect(launch.windowsVerbatimArguments).toBe(true);
expect(launch.resolvedBinaryPath).toBe("C:\\Users\\A User\\hermes.cmd");
expect(launch.args[3]).toContain("^&");
});
it("short-circuits Windows paths and preserves the Hermes resolved path", async () => {
for (const binary of ["C:\\dir\\hermes.cmd", "\\\\server\\share\\hermes.cmd", "C:hermes", "C:/dir/hermes.exe"]) {
const launch = await resolveHermesLaunch(binary, ["--version"], windows);
expect(launch.resolvedBinaryPath).toBe(binary);
}
});
it("escapes quote, trailing slash, and cmd metacharacter data", () => {
expect(escapeWindowsShellCommand("C:\\A User\\hermes.cmd")).toContain("^ ");
expect(escapeWindowsShellArgument("hi\" & | < > ^ ( ) % !\\")).toMatch(/^".*"$/);
expect(escapeWindowsShellArgument("")).toBe('""');
});
it("does not cache injected where runners", async () => {
let calls = 0;
const deps = { ...windows, runWhere: async () => { calls += 1; return "C:\\shims\\hermes.exe"; } };
await resolveHermesBinaryPath("hermes", deps);
await resolveHermesBinaryPath("hermes", deps);
expect(calls).toBe(2);
});
});