|
1 | 1 | // OpenClaw SDK tests cover package behavior. |
2 | | -import { spawn, type SpawnOptionsWithoutStdio } from "node:child_process"; |
| 2 | +import { spawn, spawnSync, type SpawnOptionsWithoutStdio } from "node:child_process"; |
3 | 3 | import { createReadStream } from "node:fs"; |
4 | 4 | import fs from "node:fs/promises"; |
5 | 5 | import { createServer, type Server } from "node:http"; |
6 | 6 | import os from "node:os"; |
7 | 7 | import path from "node:path"; |
8 | | -import { afterEach, describe, expect, it } from "vitest"; |
| 8 | +import { afterEach, describe, expect, it, vi } from "vitest"; |
9 | 9 | import { createPnpmRunnerSpawnSpec } from "../../../scripts/pnpm-runner.mjs"; |
10 | 10 | import { createNodeEvalArgs } from "../../../src/test-utils/node-process.js"; |
11 | 11 |
|
@@ -89,8 +89,35 @@ function runCommand( |
89 | 89 | }); |
90 | 90 | } |
91 | 91 |
|
92 | | -function signalCommandProcess(child: ReturnType<typeof spawn>, signal: NodeJS.Signals): void { |
93 | | - if (process.platform !== "win32" && typeof child.pid === "number") { |
| 92 | +function signalCommandProcess( |
| 93 | + child: ReturnType<typeof spawn>, |
| 94 | + signal: NodeJS.Signals, |
| 95 | + runTaskkill: typeof spawnSync = spawnSync, |
| 96 | +): void { |
| 97 | + if (process.platform === "win32") { |
| 98 | + if (typeof child.pid === "number") { |
| 99 | + const args = ["/PID", String(child.pid), "/T"]; |
| 100 | + if (signal === "SIGKILL") { |
| 101 | + args.push("/F"); |
| 102 | + } |
| 103 | + const result = runTaskkill("taskkill", args, { stdio: "ignore", windowsHide: true }); |
| 104 | + if (!result.error && result.status === 0) { |
| 105 | + return; |
| 106 | + } |
| 107 | + if (signal !== "SIGKILL") { |
| 108 | + const forceResult = runTaskkill("taskkill", [...args, "/F"], { |
| 109 | + stdio: "ignore", |
| 110 | + windowsHide: true, |
| 111 | + }); |
| 112 | + if (!forceResult.error && forceResult.status === 0) { |
| 113 | + return; |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | + child.kill(signal); |
| 118 | + return; |
| 119 | + } |
| 120 | + if (typeof child.pid === "number") { |
94 | 121 | try { |
95 | 122 | process.kill(-child.pid, signal); |
96 | 123 | return; |
@@ -260,6 +287,38 @@ describe("OpenClaw SDK package e2e", () => { |
260 | 287 | ); |
261 | 288 | }); |
262 | 289 |
|
| 290 | + it("force-kills Windows package command process trees when graceful taskkill fails", () => { |
| 291 | + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); |
| 292 | + Object.defineProperty(process, "platform", { value: "win32", configurable: true }); |
| 293 | + try { |
| 294 | + const killMock = vi.fn(); |
| 295 | + const child = { |
| 296 | + pid: 12345, |
| 297 | + kill: killMock, |
| 298 | + } as unknown as ReturnType<typeof spawn>; |
| 299 | + const runTaskkill = vi |
| 300 | + .fn() |
| 301 | + .mockReturnValueOnce({ status: 1 }) |
| 302 | + .mockReturnValueOnce({ status: 0 }); |
| 303 | + |
| 304 | + signalCommandProcess(child, "SIGTERM", runTaskkill); |
| 305 | + |
| 306 | + expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { |
| 307 | + stdio: "ignore", |
| 308 | + windowsHide: true, |
| 309 | + }); |
| 310 | + expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], { |
| 311 | + stdio: "ignore", |
| 312 | + windowsHide: true, |
| 313 | + }); |
| 314 | + expect(killMock).not.toHaveBeenCalled(); |
| 315 | + } finally { |
| 316 | + if (platformDescriptor) { |
| 317 | + Object.defineProperty(process, "platform", platformDescriptor); |
| 318 | + } |
| 319 | + } |
| 320 | + }); |
| 321 | + |
263 | 322 | it("packs and imports from an external temp consumer", async () => { |
264 | 323 | const repoRoot = process.cwd(); |
265 | 324 | const packageRoots = [ |
|
0 commit comments