mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-09 02:02:55 +00:00
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:
@@ -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 })
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user