mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-09 18:04:55 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bc8c00ea6 | ||
|
|
9f68af0274 | ||
|
|
79f2db8d56 |
@@ -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<string> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -48,7 +48,8 @@ afterEach(() => {
|
||||
process.env.PATH = originalEnv.PATH;
|
||||
process.env.PATHEXT = originalEnv.PATHEXT;
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
// Windows can briefly retain a lock on the copied executable after its probe exits.
|
||||
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -562,6 +562,13 @@ describe("runWorktreeSetupInBackground", () => {
|
||||
});
|
||||
|
||||
function startSubdirectorySetup(command: string) {
|
||||
const startedAt = performance.now();
|
||||
const recordPhase = (phase: string) => {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ phase, elapsedMs: performance.now() - startedAt })}\n`,
|
||||
);
|
||||
};
|
||||
recordPhase("fixture-started");
|
||||
const tempDir = realpathSync.native(mkdtempSync(path.join(tmpdir(), "worktree-setup-test-")));
|
||||
cleanupPaths.push(tempDir);
|
||||
const repoDir = path.join(tempDir, "source");
|
||||
@@ -574,6 +581,7 @@ describe("runWorktreeSetupInBackground", () => {
|
||||
// Setup consumes an existing workspace. Git creation is covered by the workflow tests.
|
||||
mkdirSync(sourceWorkspaceCwd, { recursive: true });
|
||||
mkdirSync(workspaceCwd, { recursive: true });
|
||||
recordPhase("git-init-started");
|
||||
execFileSync("git", ["init", "-b", worktree.branchName], {
|
||||
cwd: worktree.worktreePath,
|
||||
stdio: "pipe",
|
||||
@@ -588,12 +596,20 @@ describe("runWorktreeSetupInBackground", () => {
|
||||
);
|
||||
const paseoHome = path.join(tempDir, ".paseo");
|
||||
|
||||
recordPhase("fixture-ready");
|
||||
const controller = new AbortController();
|
||||
const completion = runWorktreeSetupInBackground(
|
||||
{
|
||||
paseoHome,
|
||||
emitWorkspaceUpdateForWorkspaceId: async () => {},
|
||||
cacheWorkspaceSetupSnapshot: () => {},
|
||||
cacheWorkspaceSetupSnapshot: (_workspaceId, snapshot) => {
|
||||
const lastCommand = snapshot.detail.commands.at(-1);
|
||||
recordPhase(
|
||||
lastCommand
|
||||
? `setup-${snapshot.status}:command-${lastCommand.status}`
|
||||
: `setup-${snapshot.status}`,
|
||||
);
|
||||
},
|
||||
emit: () => {},
|
||||
sessionLogger: createLogger(),
|
||||
terminalManager: null,
|
||||
@@ -611,8 +627,10 @@ describe("runWorktreeSetupInBackground", () => {
|
||||
controller.signal,
|
||||
);
|
||||
const stop = async () => {
|
||||
recordPhase("cleanup-started");
|
||||
controller.abort();
|
||||
await completion;
|
||||
recordPhase("cleanup-joined");
|
||||
};
|
||||
stopSetup = stop;
|
||||
return { tempDir, workspaceCwd, worktreePath: worktree.worktreePath, completion, stop };
|
||||
|
||||
Reference in New Issue
Block a user