diff --git a/packages/app/e2e/browser/terminal-protocol-query.spec.ts b/packages/app/e2e/browser/terminal-protocol-query.spec.ts index d5a0071189..4b2fc06d41 100644 --- a/packages/app/e2e/browser/terminal-protocol-query.spec.ts +++ b/packages/app/e2e/browser/terminal-protocol-query.spec.ts @@ -45,6 +45,11 @@ process.stdin.on("data", (chunk) => { for (;;) { const paste = /\x1b\[200~([^]*?)\x1b\[201~/.exec(buffer); if (!paste) break; + const pasted = JSON.stringify(paste[1]).replace( + /\s/g, + (space) => "\\u" + space.charCodeAt(0).toString(16).padStart(4, "0"), + ); + process.stdout.write("PASTED:" + pasted + "\r\n"); for (const [, path] of paste[1].matchAll(/'([^']*)'/g)) { const digest = crypto.createHash("sha256").update(fs.readFileSync(path)).digest("hex"); const location = /[\\/]uploads[\\/]upload_/.test(path) ? "upload" : "local"; @@ -202,6 +207,21 @@ test.describe("Terminal protocol queries", () => { await expect.poll(() => terminalOutput(terminalId), { timeout: 10_000 }).toContain(text); } + // Every bracketed paste body the receiver consumed, in order and joined: what a + // shell would read as one command line. + async function pastedText(terminalId: string): Promise { + const output = await terminalOutput(terminalId); + return [...output.matchAll(/PASTED:("(?:[^"\\]|\\.)*")/g)] + .map(([, json]) => JSON.parse(json) as string) + .join(""); + } + + // Exactly one ASCII space after each quoted path and nothing else between them, + // so the paths stay separate shell words. + async function expectSeparatePastedPaths(terminalId: string): Promise { + await expect.poll(() => pastedText(terminalId)).toMatch(/^'[^']+' '[^']+' $/); + } + test("pastes images from two panes into their own terminals as uploaded host files", async ({ page, }) => { @@ -238,6 +258,23 @@ test.describe("Terminal protocol queries", () => { await expectTerminalOutput(terminal.id, fileRead(pngBytes(3), "upload")); await expectTerminalOutput(terminal.id, fileRead(Buffer.from("remote notes"), "upload")); + await expectSeparatePastedPaths(terminal.id); + } finally { + await harness.killTerminal(terminal.id); + } + }); + + test("keeps the paths of two consecutive drops to another machine apart", async ({ page }) => { + const terminal = await openFileReceiver(page, "file-drop-remote-twice"); + const first = { name: "first.png", type: "image/png", bytes: pngBytes(10) }; + const second = { name: "second.txt", type: "text/plain", bytes: Buffer.from("second drop") }; + try { + await dropFiles(page, [first]); + await expectTerminalOutput(terminal.id, fileRead(first.bytes, "upload")); + await dropFiles(page, [second]); + await expectTerminalOutput(terminal.id, fileRead(second.bytes, "upload")); + + await expectSeparatePastedPaths(terminal.id); } finally { await harness.killTerminal(terminal.id); } diff --git a/packages/app/src/terminal/file-paste/index.test.ts b/packages/app/src/terminal/file-paste/index.test.ts index 63a1e5e73d..1934b311d2 100644 --- a/packages/app/src/terminal/file-paste/index.test.ts +++ b/packages/app/src/terminal/file-paste/index.test.ts @@ -74,7 +74,8 @@ describe("terminal file paste queue", () => { firstUpload.resolve("/host/uploads/a.png"); await settle(); - expect(host.pasted).toEqual(["'/host/uploads/a.png'", "'/host/uploads/b.png'"]); + expect(host.pasted).toEqual(["'/host/uploads/a.png' ", "'/host/uploads/b.png' "]); + expect(host.pasted.join("")).toBe("'/host/uploads/a.png' '/host/uploads/b.png' "); expect(events).toEqual({ uploading: [true, false, true, false], uploaded: 2, failures: [] }); }); @@ -85,7 +86,7 @@ describe("terminal file paste queue", () => { queue.add(Promise.resolve(file("shot.png")), host); await settle(); - expect(host.pasted).toEqual(['"C:\\Users\\Jo Doe\\.paseo\\uploads\\upload_1\\shot.png"']); + expect(host.pasted).toEqual(['"C:\\Users\\Jo Doe\\.paseo\\uploads\\upload_1\\shot.png" ']); }); it("uploads under a file name that needs no shell quoting", async () => { @@ -120,7 +121,7 @@ describe("terminal file paste queue", () => { expect(events.failures).toEqual(["disk full"]); expect(events.uploaded).toBe(1); - expect(host.pasted).toEqual(["'/host/uploads/b.png'"]); + expect(host.pasted).toEqual(["'/host/uploads/b.png' "]); }); it("reports unreadable and oversized files without uploading them", async () => { @@ -166,7 +167,7 @@ describe("terminal file paste queue", () => { queue.add(Promise.resolve(file("next.png")), host); await settle(); - expect(host.pasted).toEqual(["'/host/uploads/next.png'"]); + expect(host.pasted).toEqual(["'/host/uploads/next.png' "]); expect(events.uploading).toEqual([true, false, true, false]); expect(queue.cancel()).toBe(false); }); diff --git a/packages/app/src/terminal/file-paste/index.ts b/packages/app/src/terminal/file-paste/index.ts index bc96b17c5d..f793edb643 100644 --- a/packages/app/src/terminal/file-paste/index.ts +++ b/packages/app/src/terminal/file-paste/index.ts @@ -75,7 +75,7 @@ export class TerminalFilePasteQueue { this.setUploading(true); const path = await target.upload({ ...file, fileName: toShellSafeFileName(file.fileName) }); if (isCurrent()) { - target.paste(prepareTerminalPath(path)); + target.paste(`${prepareTerminalPath(path)} `); this.events.onUploaded(); } } catch (error) {