windows: unify non-core spawn handling across acp qmd and docker (openclaw#31750) thanks @Takhoffman

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check (fails on pre-existing unrelated src/slack/monitor/events/messages.ts typing errors)
- pnpm vitest run src/acp/client.test.ts src/memory/qmd-manager.test.ts src/agents/sandbox/docker.execDockerRaw.enoent.test.ts src/agents/sandbox/docker.windows.test.ts extensions/acpx/src/runtime-internals/process.test.ts

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Tak Hoffman
2026-03-02 08:05:39 -06:00
committed by GitHub
parent 32c7242974
commit cd653c55d7
7 changed files with 305 additions and 8 deletions

View File

@@ -1,6 +1,13 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import type { RequestPermissionRequest } from "@agentclientprotocol/sdk";
import { describe, expect, it, vi } from "vitest";
import { resolveAcpClientSpawnEnv, resolvePermissionRequest } from "./client.js";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
resolveAcpClientSpawnEnv,
resolveAcpClientSpawnInvocation,
resolvePermissionRequest,
} from "./client.js";
import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js";
function makePermissionRequest(
@@ -28,6 +35,24 @@ function makePermissionRequest(
};
}
const tempDirs: string[] = [];
async function createTempDir(): Promise<string> {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-acp-client-test-"));
tempDirs.push(dir);
return dir;
}
afterEach(async () => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (!dir) {
continue;
}
await rm(dir, { recursive: true, force: true });
}
});
describe("resolveAcpClientSpawnEnv", () => {
it("sets OPENCLAW_SHELL marker and preserves existing env values", () => {
const env = resolveAcpClientSpawnEnv({
@@ -48,6 +73,69 @@ describe("resolveAcpClientSpawnEnv", () => {
});
});
describe("resolveAcpClientSpawnInvocation", () => {
it("keeps non-windows invocation unchanged", () => {
const resolved = resolveAcpClientSpawnInvocation(
{ serverCommand: "openclaw", serverArgs: ["acp", "--verbose"] },
{
platform: "darwin",
env: {},
execPath: "/usr/bin/node",
},
);
expect(resolved).toEqual({
command: "openclaw",
args: ["acp", "--verbose"],
shell: undefined,
windowsHide: undefined,
});
});
it("unwraps .cmd shim entrypoint on windows", async () => {
const dir = await createTempDir();
const scriptPath = path.join(dir, "openclaw", "dist", "entry.js");
const shimPath = path.join(dir, "openclaw.cmd");
await mkdir(path.dirname(scriptPath), { recursive: true });
await writeFile(scriptPath, "console.log('ok')\n", "utf8");
await writeFile(shimPath, `@ECHO off\r\n"%~dp0\\openclaw\\dist\\entry.js" %*\r\n`, "utf8");
const resolved = resolveAcpClientSpawnInvocation(
{ serverCommand: shimPath, serverArgs: ["acp", "--verbose"] },
{
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
},
);
expect(resolved.command).toBe("C:\\node\\node.exe");
expect(resolved.args).toEqual([scriptPath, "acp", "--verbose"]);
expect(resolved.shell).toBeUndefined();
expect(resolved.windowsHide).toBe(true);
});
it("falls back to shell mode for unresolved wrappers on windows", async () => {
const dir = await createTempDir();
const shimPath = path.join(dir, "openclaw.cmd");
await writeFile(shimPath, "@ECHO off\r\necho wrapper\r\n", "utf8");
const resolved = resolveAcpClientSpawnInvocation(
{ serverCommand: shimPath, serverArgs: ["acp"] },
{
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
},
);
expect(resolved).toEqual({
command: shimPath,
args: ["acp"],
shell: true,
windowsHide: undefined,
});
});
});
describe("resolvePermissionRequest", () => {
it("auto-approves safe tools without prompting", async () => {
const prompt = vi.fn(async () => true);