fix(infra): bridge WSL clipboard through shell

* fix(infra): bridge WSL2 clipboard through shell

* test(infra): assert wsl clipboard argv stays token-free

* fix(infra): keep wsl clipboard timeout ownership
This commit is contained in:
Vincent Koc
2026-06-01 02:22:08 +01:00
committed by GitHub
parent f22e39823d
commit 52c809a759
2 changed files with 48 additions and 1 deletions

View File

@@ -1,16 +1,22 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
const isWSL2SyncMock = vi.hoisted(() => vi.fn(() => false));
vi.mock("../process/exec.js", () => ({
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
}));
vi.mock("./wsl.js", () => ({
isWSL2Sync: isWSL2SyncMock,
}));
const { copyToClipboard } = await import("./clipboard.js");
describe("copyToClipboard", () => {
beforeEach(() => {
runCommandWithTimeoutMock.mockReset();
isWSL2SyncMock.mockReturnValue(false);
});
it("returns true on the first successful clipboard command", async () => {
@@ -38,6 +44,41 @@ describe("copyToClipboard", () => {
]);
});
it("uses a startup-free WSL2 shell bridge for clip.exe without putting the value in argv", async () => {
isWSL2SyncMock.mockReturnValue(true);
runCommandWithTimeoutMock.mockResolvedValueOnce({ code: 0, killed: false });
const tokenUrl = "http://127.0.0.1:18789/#token=secret-token";
await expect(copyToClipboard(tokenUrl)).resolves.toBe(true);
expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(
["/bin/sh", "-c", "exec /mnt/c/Windows/System32/clip.exe"],
{
timeoutMs: 3000,
input: tokenUrl,
},
);
const invokedArgv = runCommandWithTimeoutMock.mock.calls[0]?.[0] as string[];
expect(invokedArgv.join("\0")).not.toContain("secret-token");
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
});
it("does not prepend the WSL2 bridge outside WSL2", async () => {
runCommandWithTimeoutMock
.mockRejectedValueOnce(new Error("missing pbcopy"))
.mockResolvedValueOnce({ code: 0, killed: true })
.mockRejectedValueOnce(new Error("missing wl-copy"))
.mockResolvedValueOnce({ code: 0, killed: false });
await expect(copyToClipboard("hello")).resolves.toBe(true);
expect(runCommandWithTimeoutMock.mock.calls.map((call) => call[0])).toEqual([
["pbcopy"],
["xclip", "-selection", "clipboard"],
["wl-copy"],
["clip.exe"],
]);
});
it("returns false when every clipboard backend fails or is killed", async () => {
runCommandWithTimeoutMock
.mockResolvedValueOnce({ code: 0, killed: true })

View File

@@ -1,11 +1,17 @@
import { runCommandWithTimeout } from "../process/exec.js";
import { isWSL2Sync } from "./wsl.js";
// WSL interop needs a shell to launch Windows PE binaries; exec keeps the
// clipboard process as the timeout-owned child while values stay on stdin.
const WSL_CLIPBOARD_ARGV = ["/bin/sh", "-c", "exec /mnt/c/Windows/System32/clip.exe"];
export async function copyToClipboard(value: string): Promise<boolean> {
const attempts: Array<{ argv: string[] }> = [
...(isWSL2Sync() ? [{ argv: WSL_CLIPBOARD_ARGV }] : []),
{ argv: ["pbcopy"] },
{ argv: ["xclip", "-selection", "clipboard"] },
{ argv: ["wl-copy"] },
{ argv: ["clip.exe"] }, // WSL / Windows
{ argv: ["clip.exe"] },
{ argv: ["powershell", "-NoProfile", "-Command", "Set-Clipboard"] },
];
for (const attempt of attempts) {