From b87f33c920422e91aa52b1915315ab59a375a52e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 31 Mar 2026 19:28:11 +0100 Subject: [PATCH] test(ci): deflake windows npm exec coverage --- src/process/exec.test.ts | 24 -------------- src/process/exec.windows.test.ts | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/process/exec.test.ts b/src/process/exec.test.ts index f87a2fcc6b0..4a3cca6b681 100644 --- a/src/process/exec.test.ts +++ b/src/process/exec.test.ts @@ -1,6 +1,5 @@ import type { ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; -import fs from "node:fs"; import process from "node:process"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { OPENCLAW_CLI_ENV_VALUE } from "../infra/openclaw-exec-env.js"; @@ -120,29 +119,6 @@ describe("runCommandWithTimeout", () => { expect(result.code).not.toBe(0); }, ); - - it.runIf(process.platform === "win32")( - "on Windows spawns node + npm-cli.js for npm argv to avoid spawn EINVAL", - async () => { - const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 10_000 }); - expect(result.code).toBe(0); - expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/); - }, - ); - - it.runIf(process.platform === "win32")( - "falls back to npm.cmd when npm-cli.js is unavailable", - async () => { - const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(false); - try { - const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 10_000 }); - expect(result.code).toBe(0); - expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/); - } finally { - existsSpy.mockRestore(); - } - }, - ); }); describe("attachChildProcessBridge", () => { diff --git a/src/process/exec.windows.test.ts b/src/process/exec.windows.test.ts index cb88e866bdb..d57ce924543 100644 --- a/src/process/exec.windows.test.ts +++ b/src/process/exec.windows.test.ts @@ -1,4 +1,6 @@ import { EventEmitter } from "node:events"; +import fs from "node:fs"; +import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const spawnMock = vi.hoisted(() => vi.fn()); @@ -124,6 +126,60 @@ describe("windows command wrapper behavior", () => { } }); + it("spawns node + npm-cli.js for npm argv to avoid direct .cmd execution", async () => { + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(true); + const child = createMockChild({ closeCode: 0, exitCode: 0 }); + + spawnMock.mockImplementation(() => child); + + try { + const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); + expect(result.code).toBe(0); + const captured = spawnMock.mock.calls[0] as SpawnCall | undefined; + if (!captured) { + throw new Error("expected npm shim spawn"); + } + expect(captured[0]).toBe(process.execPath); + expect(captured[1][0]).toBe( + path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"), + ); + expect(captured[1][1]).toBe("--version"); + expect(captured[2].windowsVerbatimArguments).toBeUndefined(); + expect(captured[2].stdio).toEqual(["inherit", "pipe", "pipe"]); + } finally { + existsSpy.mockRestore(); + platformSpy.mockRestore(); + } + }); + + it("falls back to npm.cmd when npm-cli.js is unavailable", async () => { + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(false); + const expectedComSpec = process.env.ComSpec ?? "cmd.exe"; + + spawnMock.mockImplementation( + (_command: string, _args: string[], _options: Record) => createMockChild(), + ); + + try { + const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); + expect(result.code).toBe(0); + const captured = spawnMock.mock.calls[0] as SpawnCall | undefined; + if (!captured) { + throw new Error("expected npm.cmd fallback spawn"); + } + expect(captured[0]).toBe(expectedComSpec); + expect(captured[1].slice(0, 3)).toEqual(["/d", "/s", "/c"]); + expect(captured[1][3]).toContain("npm.cmd --version"); + expect(captured[2].windowsVerbatimArguments).toBe(true); + expect(captured[2].stdio).toEqual(["inherit", "pipe", "pipe"]); + } finally { + existsSpy.mockRestore(); + platformSpy.mockRestore(); + } + }); + it("waits for Windows exitCode settlement after close reports null", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const child = createMockChild({