From f61a3d46a22e973d0c65fd6d5bc5d1586decdcdf Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Fri, 17 Jul 2026 07:00:20 +0800
Subject: [PATCH] fix(doctor): bound GitHub issue creation (#109253)
---
...doctor-session-sqlite-github-issue.test.ts | 59 +++++++++++++++++++
.../doctor-session-sqlite-github-issue.ts | 4 ++
2 files changed, 63 insertions(+)
create mode 100644 src/commands/doctor-session-sqlite-github-issue.test.ts
diff --git a/src/commands/doctor-session-sqlite-github-issue.test.ts b/src/commands/doctor-session-sqlite-github-issue.test.ts
new file mode 100644
index 000000000000..3b020fc1d36b
--- /dev/null
+++ b/src/commands/doctor-session-sqlite-github-issue.test.ts
@@ -0,0 +1,59 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { createSessionSqliteGithubIssue } from "./doctor-session-sqlite-github-issue.js";
+
+const spawnSyncMock = vi.hoisted(() => vi.fn());
+
+vi.mock("node:child_process", async () => {
+ const actual = await vi.importActual("node:child_process");
+ return { ...actual, spawnSync: spawnSyncMock };
+});
+
+describe("createSessionSqliteGithubIssue", () => {
+ beforeEach(() => {
+ spawnSyncMock.mockReset();
+ });
+
+ it("bounds GitHub CLI issue creation and preserves the fallback URL on timeout", () => {
+ const timeoutError = Object.assign(new Error("spawnSync gh ETIMEDOUT"), {
+ code: "ETIMEDOUT",
+ });
+ spawnSyncMock.mockReturnValue({
+ error: timeoutError,
+ status: null,
+ stderr: Buffer.alloc(0),
+ stdout: Buffer.alloc(0),
+ });
+
+ const result = createSessionSqliteGithubIssue({
+ body: "sanitized body",
+ title: "Session SQLite migration recovery report",
+ url: "https://github.com/openclaw/openclaw/issues/new?title=recovery",
+ });
+
+ expect(spawnSyncMock).toHaveBeenCalledWith(
+ "gh",
+ [
+ "issue",
+ "create",
+ "--repo",
+ "openclaw/openclaw",
+ "--title",
+ "Session SQLite migration recovery report",
+ "--body-file",
+ "-",
+ ],
+ {
+ encoding: "buffer",
+ input: "sanitized body",
+ killSignal: "SIGKILL",
+ maxBuffer: 1024 * 1024,
+ timeout: 30_000,
+ },
+ );
+ expect(result).toEqual({
+ fallbackUrl: "https://github.com/openclaw/openclaw/issues/new?title=recovery",
+ message: "spawnSync gh ETIMEDOUT",
+ ok: false,
+ });
+ });
+});
diff --git a/src/commands/doctor-session-sqlite-github-issue.ts b/src/commands/doctor-session-sqlite-github-issue.ts
index 766e7af23914..8cb449d6a960 100644
--- a/src/commands/doctor-session-sqlite-github-issue.ts
+++ b/src/commands/doctor-session-sqlite-github-issue.ts
@@ -11,6 +11,8 @@ type SpawnGh = (
options: { input: string },
) => Pick, "error" | "status" | "stderr" | "stdout">;
+const GITHUB_ISSUE_CREATE_TIMEOUT_MS = 30_000;
+
/** Creates an openclaw/openclaw issue through the GitHub CLI using sanitized stdin. */
export function createSessionSqliteGithubIssue(
issue: SessionSqliteMigrationFailureIssue,
@@ -45,6 +47,8 @@ function defaultSpawnGh(
return spawnSync("gh", [...args], {
encoding: "buffer",
input: options.input,
+ killSignal: "SIGKILL",
maxBuffer: 1024 * 1024,
+ timeout: GITHUB_ISSUE_CREATE_TIMEOUT_MS,
});
}