import { execSync, spawn } from "child_process"; import { platform } from "os"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { copyToClipboard } from "../src/utils/clipboard.js"; const mocks = vi.hoisted(() => { return { clipboard: { setText: vi.fn<(text: string) => Promise>(), }, execSync: vi.fn(), spawn: vi.fn(), platform: vi.fn<() => NodeJS.Platform>(), isWaylandSession: vi.fn<() => boolean>(), }; }); vi.mock("../src/utils/clipboard-native.js", () => { return { clipboard: mocks.clipboard, }; }); vi.mock("child_process", () => { return { execSync: mocks.execSync, spawn: mocks.spawn, }; }); vi.mock("os", () => { return { platform: mocks.platform, }; }); vi.mock("../src/utils/clipboard-image.js", () => { return { isWaylandSession: mocks.isWaylandSession, }; }); const mockedExecSync = vi.mocked(execSync); const mockedSpawn = vi.mocked(spawn); const mockedPlatform = vi.mocked(platform); let originalWrite: typeof process.stdout.write; let stdoutWrites: string[]; let nativeResolved = false; function osc52Writes(): string[] { return stdoutWrites.filter((write) => write.startsWith("\x1b]52;c;")); } beforeEach(() => { vi.unstubAllEnvs(); vi.stubEnv("SSH_CONNECTION", ""); vi.stubEnv("SSH_CLIENT", ""); vi.stubEnv("MOSH_CONNECTION", ""); stdoutWrites = []; nativeResolved = false; mocks.clipboard.setText.mockReset(); mocks.execSync.mockReset(); mocks.spawn.mockReset(); mocks.platform.mockReset(); mocks.isWaylandSession.mockReset(); mockedPlatform.mockReturnValue("darwin"); mocks.isWaylandSession.mockReturnValue(false); mocks.clipboard.setText.mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 1)); nativeResolved = true; }); originalWrite = process.stdout.write.bind(process.stdout); process.stdout.write = ((...args: Parameters) => { const [chunk] = args; if (typeof chunk === "string" && chunk.startsWith("\x1b]52;c;")) { stdoutWrites.push(chunk); return true; } return originalWrite(...args); }) as typeof process.stdout.write; }); afterEach(() => { process.stdout.write = originalWrite; vi.unstubAllEnvs(); }); describe("copyToClipboard", () => { test("local native success skips OSC 52 and shell fallbacks", async () => { await copyToClipboard("hello"); expect(mocks.clipboard.setText).toHaveBeenCalledWith("hello"); expect(osc52Writes()).toHaveLength(0); expect(mockedExecSync).not.toHaveBeenCalled(); expect(mockedSpawn).not.toHaveBeenCalled(); }); test("remote native success emits OSC 52 after native write", async () => { vi.stubEnv("SSH_CONNECTION", "client server"); mocks.clipboard.setText.mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 1)); expect(osc52Writes()).toHaveLength(0); nativeResolved = true; }); await copyToClipboard("hello"); expect(nativeResolved).toBe(true); expect(osc52Writes()).toHaveLength(1); expect(mockedExecSync).not.toHaveBeenCalled(); }); test("local shell fallback success skips OSC 52", async () => { mocks.clipboard.setText.mockRejectedValue(new Error("native failed")); mockedExecSync.mockReturnValue(Buffer.alloc(0)); await copyToClipboard("hello"); expect(mockedExecSync).toHaveBeenCalledWith("pbcopy", { input: "hello", stdio: ["pipe", "ignore", "ignore"], timeout: 5000, }); expect(osc52Writes()).toHaveLength(0); }); test("uses OSC 52 fallback when native and shell tools fail", async () => { mocks.clipboard.setText.mockRejectedValue(new Error("native failed")); mockedExecSync.mockImplementation(() => { throw new Error("pbcopy failed"); }); await copyToClipboard("hello"); expect(osc52Writes()).toHaveLength(1); }); test("does not emit oversized OSC 52 payloads", async () => { mocks.clipboard.setText.mockRejectedValue(new Error("native failed")); mockedExecSync.mockImplementation(() => { throw new Error("pbcopy failed"); }); await expect(copyToClipboard("x".repeat(80_000))).rejects.toThrow("Failed to copy to clipboard"); expect(osc52Writes()).toHaveLength(0); }); });