diff --git a/packages/cli/src/utils/clipboard.test.ts b/packages/cli/src/utils/clipboard.test.ts new file mode 100644 index 0000000000..2cf2422269 --- /dev/null +++ b/packages/cli/src/utils/clipboard.test.ts @@ -0,0 +1,31 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const spawnSync = vi.hoisted(() => vi.fn()); +vi.mock("node:child_process", () => ({ spawnSync })); +vi.mock("node:os", async (orig) => ({ + ...(await orig()), + platform: () => "darwin", +})); + +describe("copyToClipboard", () => { + beforeEach(() => { + spawnSync.mockReset(); + vi.resetModules(); + }); + + it("does not let the tool's background owner hold our output pipes or block forever", async () => { + spawnSync.mockReturnValue({ status: 0 }); + const { copyToClipboard } = await import("./clipboard.js"); + expect(copyToClipboard("snippet")).toBe(true); + const opts = spawnSync.mock.calls[0]![2]; + expect(opts.input).toBe("snippet"); + expect(opts.stdio).toEqual(["pipe", "ignore", "ignore"]); + expect(opts.timeout).toBeGreaterThan(0); + }); + + it("reports not copied when the tool times out", async () => { + spawnSync.mockReturnValue({ status: null, error: new Error("ETIMEDOUT") }); + const { copyToClipboard } = await import("./clipboard.js"); + expect(copyToClipboard("snippet")).toBe(false); + }); +}); diff --git a/packages/cli/src/utils/clipboard.ts b/packages/cli/src/utils/clipboard.ts index f20dad7252..51b3c52ced 100644 --- a/packages/cli/src/utils/clipboard.ts +++ b/packages/cli/src/utils/clipboard.ts @@ -9,6 +9,8 @@ import { spawnSync } from "node:child_process"; import { platform } from "node:os"; +const COPY_TIMEOUT_MS = 5000; + interface ClipboardProvider { cmd: string; args: string[]; @@ -49,6 +51,8 @@ export function copyToClipboard(text: string): boolean { const res = spawnSync(provider.cmd, provider.args, { input: text, encoding: "utf-8", + stdio: ["pipe", "ignore", "ignore"], + timeout: COPY_TIMEOUT_MS, }); return res.status === 0; } catch {