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, }); }