Skip to content

Commit 72b9bc7

Browse files
committed
fix(core): resolve Windows PATH locator tools
1 parent d3b4444 commit 72b9bc7

4 files changed

Lines changed: 84 additions & 2 deletions

File tree

src/daemon/program-args.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Daemon program argument tests cover CLI argument construction for services.
22
import path from "node:path";
33
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { resetWindowsInstallRootsForTests } from "../infra/windows-install-roots.js";
5+
import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js";
46

57
const childProcessMocks = vi.hoisted(() => ({
68
execFileSync: vi.fn(),
@@ -43,6 +45,8 @@ const originalArgv = [...process.argv];
4345
afterEach(() => {
4446
process.argv = [...originalArgv];
4547
vi.resetAllMocks();
48+
vi.unstubAllEnvs();
49+
resetWindowsInstallRootsForTests();
4650
});
4751

4852
describe("resolveGatewayProgramArguments", () => {
@@ -179,6 +183,39 @@ describe("resolveGatewayProgramArguments", () => {
179183
expect(result.workingDirectory).toBe(path.resolve("/repo"));
180184
});
181185

186+
it("uses trusted Windows where.exe when resolving dev runtime binaries", async () => {
187+
const repoIndexPath = path.resolve("/repo/src/index.ts");
188+
const repoEntryPath = path.resolve("/repo/src/entry.ts");
189+
process.argv = [String.raw`D:\nodejs\node.exe`, repoIndexPath];
190+
vi.stubEnv("SystemRoot", String.raw`D:\Windows`);
191+
resetWindowsInstallRootsForTests({ queryRegistryValue: () => null });
192+
fsMocks.realpath.mockResolvedValue(repoIndexPath);
193+
fsMocks.access.mockResolvedValue(undefined);
194+
childProcessMocks.execFileSync.mockReturnValue(String.raw`D:\Tools\bun.exe` + "\r\n");
195+
196+
let result: Awaited<ReturnType<typeof resolveGatewayProgramArguments>> | undefined;
197+
await withMockedWindowsPlatform(async () => {
198+
result = await resolveGatewayProgramArguments({
199+
dev: true,
200+
port: 18789,
201+
runtime: "bun",
202+
});
203+
});
204+
205+
expect(childProcessMocks.execFileSync).toHaveBeenCalledWith(
206+
path.win32.join(String.raw`D:\Windows`, "System32", "where.exe"),
207+
["bun"],
208+
{ encoding: "utf8" },
209+
);
210+
expect(result?.programArguments).toEqual([
211+
String.raw`D:\Tools\bun.exe`,
212+
repoEntryPath,
213+
"gateway",
214+
"--port",
215+
"18789",
216+
]);
217+
});
218+
182219
it("uses an executable wrapper when provided", async () => {
183220
const wrapperPath = path.resolve("/usr/local/bin/openclaw-doppler");
184221
fsMocks.stat.mockResolvedValue({ isFile: () => true } as never);

src/daemon/program-args.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
33
import { constants as fsConstants } from "node:fs";
44
import fs from "node:fs/promises";
55
import path from "node:path";
6+
import { getWindowsSystem32ExePath } from "../infra/windows-install-roots.js";
67
import {
78
buildGatewayDistEntrypointCandidates,
89
findFirstAccessibleGatewayEntrypoint,
@@ -165,7 +166,7 @@ async function resolveNodePath(): Promise<string> {
165166
}
166167

167168
async function resolveBinaryPath(binary: string): Promise<string> {
168-
const cmd = process.platform === "win32" ? "where" : "which";
169+
const cmd = process.platform === "win32" ? getWindowsSystem32ExePath("where.exe") : "which";
169170
try {
170171
const output = execFileSync(cmd, [binary], { encoding: "utf8" }).trim();
171172
const resolved = output.split(/\r?\n/)[0]?.trim();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Covers TUI Codex CLI lookup command selection.
2+
import path from "node:path";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { resetWindowsInstallRootsForTests } from "../infra/windows-install-roots.js";
5+
import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js";
6+
7+
const execFileSyncMock = vi.hoisted(() => vi.fn());
8+
9+
vi.mock("node:child_process", async () => {
10+
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
11+
return {
12+
...actual,
13+
execFileSync: execFileSyncMock,
14+
};
15+
});
16+
17+
import { resolveCodexCliBin } from "./tui.js";
18+
19+
afterEach(() => {
20+
vi.restoreAllMocks();
21+
vi.unstubAllEnvs();
22+
execFileSyncMock.mockReset();
23+
resetWindowsInstallRootsForTests();
24+
});
25+
26+
describe("resolveCodexCliBin", () => {
27+
it("uses the trusted Windows where.exe when resolving codex", async () => {
28+
vi.stubEnv("SystemRoot", "D:\\Windows");
29+
resetWindowsInstallRootsForTests({ queryRegistryValue: () => null });
30+
execFileSyncMock.mockReturnValue("D:\\Tools\\codex.exe\r\n");
31+
32+
await withMockedWindowsPlatform(async () => {
33+
expect(resolveCodexCliBin()).toBe("D:\\Tools\\codex.exe");
34+
});
35+
36+
expect(execFileSyncMock).toHaveBeenCalledWith(
37+
path.win32.join("D:\\Windows", "System32", "where.exe"),
38+
["codex"],
39+
{ encoding: "utf8" },
40+
);
41+
});
42+
});

src/tui/tui.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { resolveAgentIdByWorkspacePath, resolveDefaultAgentId } from "../agents/
1919
import { getRuntimeConfig, type OpenClawConfig } from "../config/config.js";
2020
import { isChatStopCommandText } from "../gateway/chat-abort.js";
2121
import { registerUncaughtExceptionHandler } from "../infra/unhandled-rejections.js";
22+
import { getWindowsSystem32ExePath } from "../infra/windows-install-roots.js";
2223
import { setConsoleSubsystemFilter } from "../logging/console.js";
2324
import { loggingState } from "../logging/state.js";
2425
import {
@@ -98,7 +99,8 @@ type RunTuiOptions = TuiOptions & {
9899
/** Resolve the absolute path to the `codex` CLI binary, or `null` if not installed. */
99100
export function resolveCodexCliBin(): string | null {
100101
try {
101-
const lookupCmd = process.platform === "win32" ? "where" : "which";
102+
const lookupCmd =
103+
process.platform === "win32" ? getWindowsSystem32ExePath("where.exe") : "which";
102104
// `where` on Windows can return multiple lines; take the first match.
103105
const raw = execFileSync(lookupCmd, ["codex"], { encoding: "utf8" }).trim();
104106
return raw.split(/\r?\n/)[0] || null;

0 commit comments

Comments
 (0)