Files
dev/third_party/pi/packages/coding-agent/test/clipboard-paste-file-paths.test.ts
alexion 6081e3e325 chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
2026-10-15 17:58:48 -04:00

142 lines
4.9 KiB
TypeScript

import { beforeEach, expect, test, vi } from "vitest";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
const mocks = vi.hoisted(() => ({
readClipboardFilePaths: vi.fn<() => Promise<string[] | null>>(),
readClipboardImage: vi.fn<() => Promise<{ bytes: Uint8Array; mimeType: string } | null>>(),
readClipboardText: vi.fn<() => Promise<string | null>>(),
}));
vi.mock("../src/utils/clipboard.ts", () => ({
copyToClipboard: vi.fn(),
readClipboardFilePaths: mocks.readClipboardFilePaths,
readClipboardText: mocks.readClipboardText,
}));
vi.mock("../src/utils/clipboard-image.ts", () => ({
extensionForImageMimeType: () => "png",
readClipboardImage: mocks.readClipboardImage,
}));
beforeEach(() => vi.resetAllMocks());
test("Finder file paths take precedence over their icon image", async () => {
// Regression test for #9999.
const filePaths = ["/tmp/screenshot.png", "/tmp/My Photos/photo.png"];
mocks.readClipboardFilePaths.mockResolvedValue(filePaths);
mocks.readClipboardImage.mockResolvedValue({
bytes: new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
mimeType: "image/png",
});
mocks.readClipboardText.mockResolvedValue(null);
const insertTextAtCursor = vi.fn<(text: string) => void>();
const context = {
editor: { insertTextAtCursor },
showError: vi.fn(),
ui: { requestRender: vi.fn() },
};
const prototype = InteractiveMode.prototype as unknown as {
handleClipboardPaste(this: typeof context): Promise<void>;
};
await prototype.handleClipboardPaste.call(context);
expect(insertTextAtCursor).toHaveBeenCalledExactlyOnceWith(filePaths.join("\n"));
expect(mocks.readClipboardImage).not.toHaveBeenCalled();
});
test("clipboard file paths containing terminal control characters are rejected", async () => {
const insertTextAtCursor = vi.fn<(text: string) => void>();
const showError = vi.fn<(message: string) => void>();
mocks.readClipboardFilePaths.mockResolvedValue(["/tmp/photo\x1b]0;unsafe\x07.png"]);
const context = {
editor: { insertTextAtCursor },
showError,
ui: { requestRender: vi.fn() },
};
const prototype = InteractiveMode.prototype as unknown as {
handleClipboardPaste(this: typeof context): Promise<void>;
};
await prototype.handleClipboardPaste.call(context);
expect(insertTextAtCursor).not.toHaveBeenCalled();
expect(mocks.readClipboardImage).not.toHaveBeenCalled();
expect(showError).toHaveBeenCalledExactlyOnceWith(
"Failed to paste from clipboard: Clipboard file path contains control characters",
);
});
test("native file-path errors are shown without falling through to the icon image", async () => {
const showError = vi.fn<(message: string) => void>();
mocks.readClipboardFilePaths.mockRejectedValue(new Error("Native clipboard file read failed"));
const context = {
editor: { insertTextAtCursor: vi.fn() },
showError,
ui: { requestRender: vi.fn() },
};
const prototype = InteractiveMode.prototype as unknown as {
handleClipboardPaste(this: typeof context): Promise<void>;
};
await prototype.handleClipboardPaste.call(context);
expect(context.editor.insertTextAtCursor).not.toHaveBeenCalled();
expect(mocks.readClipboardImage).not.toHaveBeenCalled();
expect(showError).toHaveBeenCalledExactlyOnceWith(
"Failed to paste from clipboard: Native clipboard file read failed",
);
});
test.each([
["punctuation", "Review:", 7],
["Unicode text", "確認", 2],
])("clipboard file paths are separated from preceding %s", async (_description, editorText, cursorCol) => {
const insertTextAtCursor = vi.fn<(text: string) => void>();
mocks.readClipboardFilePaths.mockResolvedValue(["/tmp/photo.png"]);
const context = {
editor: {
getCursor: () => ({ line: 0, col: cursorCol }),
getText: () => editorText,
insertTextAtCursor,
},
showError: vi.fn(),
ui: { requestRender: vi.fn() },
};
const prototype = InteractiveMode.prototype as unknown as {
handleClipboardPaste(this: typeof context): Promise<void>;
};
await prototype.handleClipboardPaste.call(context);
expect(insertTextAtCursor).toHaveBeenCalledExactlyOnceWith(" /tmp/photo.png");
expect(mocks.readClipboardImage).not.toHaveBeenCalled();
});
test("bash mode shell-quotes file paths and inserts them as arguments", async () => {
const insertTextAtCursor = vi.fn<(text: string) => void>();
mocks.readClipboardFilePaths.mockResolvedValue([
"/tmp/My Photos/photo.png",
"/tmp/$(touch hacked).png",
"/tmp/plain.png",
]);
const context = {
editor: {
getCursor: () => ({ line: 0, col: 3 }),
getText: () => "catDEST",
insertTextAtCursor,
},
isBashMode: true,
showError: vi.fn(),
ui: { requestRender: vi.fn() },
};
const prototype = InteractiveMode.prototype as unknown as {
handleClipboardPaste(this: typeof context): Promise<void>;
};
await prototype.handleClipboardPaste.call(context);
expect(insertTextAtCursor).toHaveBeenCalledExactlyOnceWith(
" '/tmp/My Photos/photo.png' '/tmp/$(touch hacked).png' /tmp/plain.png ",
);
expect(mocks.readClipboardImage).not.toHaveBeenCalled();
});