From 0030a192c8e4c7501263ecc7f8165c8ac633ca21 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 21 Jun 2026 06:42:35 +0200 Subject: [PATCH] fix(qa): kill telegram credential trees on windows --- scripts/e2e/telegram-user-credential-io.ts | 30 ++++++++++++++++-- test/scripts/telegram-user-credential.test.ts | 31 ++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/scripts/e2e/telegram-user-credential-io.ts b/scripts/e2e/telegram-user-credential-io.ts index d3553fbe4c80..0201d1804eb7 100644 --- a/scripts/e2e/telegram-user-credential-io.ts +++ b/scripts/e2e/telegram-user-credential-io.ts @@ -1,5 +1,5 @@ // Telegram User Credential Io script supports OpenClaw repository automation. -import { spawn } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import { readBoundedResponseText } from "../lib/bounded-response.ts"; export type JsonObject = Record; @@ -244,8 +244,22 @@ async function finishTimedOutChildProcessTree( } } -function signalChildProcessTree(child: ReturnType, signal: NodeJS.Signals) { - if (process.platform !== "win32" && child.pid) { +type ChildProcessTreeTarget = Pick, "kill" | "pid">; + +export function signalChildProcessTree( + child: ChildProcessTreeTarget, + signal: NodeJS.Signals, + { + platform = process.platform, + runTaskkill = spawnSync, + useProcessGroup = platform !== "win32", + }: { + platform?: NodeJS.Platform; + runTaskkill?: typeof spawnSync; + useProcessGroup?: boolean; + } = {}, +) { + if (useProcessGroup && child.pid) { try { process.kill(-child.pid, signal); return; @@ -253,6 +267,16 @@ function signalChildProcessTree(child: ReturnType, signal: NodeJS. // The process group can disappear between timeout and cleanup. } } + if (platform === "win32" && typeof child.pid === "number") { + const args = ["/PID", String(child.pid), "/T"]; + if (signal === "SIGKILL") { + args.push("/F"); + } + const result = runTaskkill("taskkill", args, { stdio: "ignore" }); + if (!result.error && result.status === 0) { + return; + } + } child.kill(signal); } diff --git a/test/scripts/telegram-user-credential.test.ts b/test/scripts/telegram-user-credential.test.ts index 81f33e1cd8f9..1f1974add58c 100644 --- a/test/scripts/telegram-user-credential.test.ts +++ b/test/scripts/telegram-user-credential.test.ts @@ -5,7 +5,11 @@ import { readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path, { win32 } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { fetchJsonWithTimeout, runCommand } from "../../scripts/e2e/telegram-user-credential-io.ts"; +import { + fetchJsonWithTimeout, + runCommand, + signalChildProcessTree, +} from "../../scripts/e2e/telegram-user-credential-io.ts"; import { expandHome, resolvePrivateJsonDirectory, @@ -403,6 +407,31 @@ setInterval(() => {}, 1000); } }); + it("signals Windows credential helper process trees with taskkill", () => { + const child = { + kill: vi.fn(), + pid: 12345, + }; + const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 })); + + signalChildProcessTree(child, "SIGTERM", { + platform: "win32", + runTaskkill, + }); + expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], { + stdio: "ignore", + }); + + signalChildProcessTree(child, "SIGKILL", { + platform: "win32", + runTaskkill, + }); + expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], { + stdio: "ignore", + }); + expect(child.kill).not.toHaveBeenCalled(); + }); + it.runIf(process.platform !== "win32")( "exits promptly after forwarded SIGTERM children exit cleanly", async () => {