pi-mono/packages/coding-agent/test/clipboard-command.test.ts
Petr Houška 6454d07ec3
fix(coding-agent): start WSL interop helpers from / and report external editor failures (#11627)
Fixes #11592.

## Problem

WSL starts Windows programs in the current directory, translated to
`\\wsl.localhost\<distro>\<cwd>`. When that path exceeds MAX_PATH, every
Windows program fails to start with `Invalid argument`. pi inherits its
cwd when spawning `powershell.exe`, so in a deep directory:

- clipboard image paste returns `null` (the PowerShell backend fails,
stderr is discarded)
- copy via `powershell.exe` (WSL without WSLg) fails
- opening the browser for sign-in fails (`xdg-open` on WSL runs
`explorer.exe`)

Reproduced on WSL (FedoraLinux-44): `powershell.exe` and `cmd.exe` work
from a 225-char cwd and fail from 230. With an image on the Windows
clipboard, `readClipboardImage()` returns the PNG from `/tmp` and `null`
from a 245-char cwd; with this change it returns the PNG from both.
`LongPathsEnabled=1` does not help.

## Changes

- `runClipboardCommand` and `openBrowser` run from `/` on POSIX. These
tools never use the cwd (temp files are passed by absolute path), and
`/` is always the shortest UNC path. `tmpdir()` was not used because
`$TMPDIR` can itself be deep.
- The external editor keeps pi's cwd on purpose, so project-relative
paths and integrated terminals keep working. A Windows editor such as
`code` can still fail from a deep WSL directory, as with git's
`core.editor`. A comment documents this.
- External editor failures were silent: the UI redrew over the error
immediately. pi now shows `Failed to launch external editor: <cmd>`,
`External editor exited with code N; prompt unchanged`, or the signal
name. This shows in the status line for the main editor and inline in
the extension editor dialog.

## Testing

- New tests: clipboard commands run from `/` (POSIX); an editor that
cannot be launched is reported (POSIX); the existing failure test checks
the message.
- Verified on WSL by running the real modules: clipboard image paste
from a deep cwd, and editor success, exit code 1, launch failure, and
SIGTERM.
- On Windows, the 3 existing `external-editor.test.ts` tests already
fail on `main`: the editor command is split on spaces, so `C:\Program
Files\nodejs\node.exe` breaks. Not addressed here.

This PR is AI-generated.
2026-10-30 21:47:58 +01:00

55 lines
2.5 KiB
TypeScript

import { resolve } from "node:path";
import { describe, expect, test } from "vitest";
import { runClipboardCommand } from "../src/utils/clipboard-command.ts";
describe("clipboard commands", () => {
test("preserves binary output and distinguishes empty success from failure", async () => {
expect(
await runClipboardCommand(process.execPath, ["-e", "process.stdout.write(Buffer.from([0, 255, 10]))"]),
).toEqual(Buffer.from([0, 255, 10]));
expect(await runClipboardCommand(process.execPath, ["-e", ""])).toEqual(Buffer.alloc(0));
expect(await runClipboardCommand(process.execPath, ["-e", "process.exit(1)"])).toBeUndefined();
expect(await runClipboardCommand("pi-clipboard-command-does-not-exist", [])).toBeUndefined();
});
test("sends Unicode input to clipboard writers", async () => {
const script =
"let text = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', c => text += c); process.stdin.on('end', () => process.exit(text === 'café 日本語' ? 0 : 1));";
expect(await runClipboardCommand(process.execPath, ["-e", script], { input: "café 日本語" })).toEqual(
Buffer.alloc(0),
);
});
test("times out without blocking the event loop", async () => {
let ticks = 0;
const timer = setInterval(() => ticks++, 10);
try {
expect(
await runClipboardCommand(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { timeoutMs: 200 }),
).toBeUndefined();
expect(ticks).toBeGreaterThan(5);
} finally {
clearInterval(timer);
}
});
// #11179: clipboard daemons must not share pi's process group, or Ctrl+Z suspend stops them.
test.skipIf(process.platform === "win32")("runs commands outside pi's process group", async () => {
const childPgid = (await runClipboardCommand("sh", ["-c", "ps -o pgid= -p $$"]))?.toString("utf8").trim();
const ownPgid = (await runClipboardCommand("ps", ["-o", "pgid=", "-p", String(process.pid)]))
?.toString("utf8")
.trim();
expect(childPgid).toMatch(/^\d+$/);
expect(ownPgid).toMatch(/^\d+$/);
expect(childPgid).not.toBe(ownPgid);
});
// #11592: WSL cannot start Windows executables from a cwd whose UNC path exceeds MAX_PATH.
test("runs commands from the filesystem root", async () => {
const cwd = await runClipboardCommand(process.execPath, ["-e", "process.stdout.write(process.cwd())"]);
expect(cwd?.toString("utf8")).toBe(resolve("/"));
});
test("rejects output above the buffer limit", async () => {
expect(
await runClipboardCommand(process.execPath, ["-e", "process.stdout.write(Buffer.alloc(1024))"], {
maxBufferBytes: 16,
}),
).toBeUndefined();
});
});