From 84f5818e09f9f43b66c52657ee2ba190c5a4785b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 1 May 2026 14:27:55 +0700 Subject: [PATCH] test(server): run worker terminal coverage on windows --- .github/workflows/ci.yml | 1 + .../terminal/worker-terminal-manager.test.ts | 174 ++++++++++-------- 2 files changed, 101 insertions(+), 74 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdf91bcb8..2b31047b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,7 @@ jobs: src/utils/spawn.test.ts src/utils/run-git-command.test.ts src/utils/checkout-git-rev-parse.test.ts + src/terminal/worker-terminal-manager.test.ts src/server/agent/provider-registry.test.ts src/server/agent/provider-launch-config.test.ts src/server/agent/provider-snapshot-manager.test.ts diff --git a/packages/server/src/terminal/worker-terminal-manager.test.ts b/packages/server/src/terminal/worker-terminal-manager.test.ts index 2466e31d6..d1c6abc46 100644 --- a/packages/server/src/terminal/worker-terminal-manager.test.ts +++ b/packages/server/src/terminal/worker-terminal-manager.test.ts @@ -12,6 +12,13 @@ import type { TerminalWorkerToParentMessage, } from "./terminal-worker-protocol.js"; +function nodeTerminalCommand(script: string): { command: string; args: string[] } { + return { + command: process.execPath, + args: ["-e", script], + }; +} + async function waitForCondition( predicate: () => boolean, timeoutMs: number, @@ -27,20 +34,6 @@ async function waitForCondition( throw new Error(`Timed out after ${timeoutMs}ms waiting for condition`); } -async function withShell(shell: string, run: () => Promise): Promise { - const originalShell = process.env.SHELL; - process.env.SHELL = shell; - try { - return await run(); - } finally { - if (originalShell === undefined) { - delete process.env.SHELL; - } else { - process.env.SHELL = originalShell; - } - } -} - function getVisibleText(session: TerminalSession): string { return session .getState() @@ -107,36 +100,44 @@ afterEach(() => { }); it("creates a terminal through the worker and streams output", async () => { - await withShell("/bin/sh", async () => { - manager = createWorkerTerminalManager(); - const session = await manager.createTerminal({ cwd: "/tmp", env: { PS1: "$ " } }); - const messages: string[] = []; - let snapshots = 0; - const unsubscribe = session.subscribe((message) => { - if (message.type === "output") { - messages.push(message.data); - } - if (message.type === "snapshot") { - snapshots += 1; - } - }); - await new Promise((resolve) => setTimeout(resolve, 100)); - const snapshotsBeforeOutput = snapshots; - - session.send({ type: "input", data: "printf worker-output\\r" }); - - await waitForCondition( - () => - messages.join("").includes("worker-output") || - getVisibleText(session).includes("worker-output"), - 10000, - ); - await new Promise((resolve) => setTimeout(resolve, 100)); - unsubscribe(); - - expect(messages.join("") + getVisibleText(session)).toContain("worker-output"); - expect(snapshots).toBe(snapshotsBeforeOutput); + const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-output-")); + temporaryDirs.push(cwd); + manager = createWorkerTerminalManager(); + const session = await manager.createTerminal({ + cwd, + ...nodeTerminalCommand(` + process.stdin.on("data", (chunk) => { + process.stdout.write("worker-output:" + chunk.toString()); + }); + setInterval(() => {}, 1000); + `), }); + const messages: string[] = []; + let snapshots = 0; + const unsubscribe = session.subscribe((message) => { + if (message.type === "output") { + messages.push(message.data); + } + if (message.type === "snapshot") { + snapshots += 1; + } + }); + await new Promise((resolve) => setTimeout(resolve, 100)); + const snapshotsBeforeOutput = snapshots; + + session.send({ type: "input", data: "hello\n" }); + + await waitForCondition( + () => + messages.join("").includes("worker-output:hello") || + getVisibleText(session).includes("worker-output:hello"), + 10000, + ); + await new Promise((resolve) => setTimeout(resolve, 100)); + unsubscribe(); + + expect(messages.join("") + getVisibleText(session)).toContain("worker-output:hello"); + expect(snapshots).toBe(snapshotsBeforeOutput); }); it("does not surface fire-and-forget send timeouts as unhandled rejections", async () => { @@ -171,41 +172,66 @@ it("does not surface fire-and-forget send timeouts as unhandled rejections", asy }); it("keeps registered cwd env inheritance behind the worker manager interface", async () => { - await withShell("/bin/sh", async () => { - manager = createWorkerTerminalManager(); - const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-env-")); - temporaryDirs.push(cwd); - const markerPath = join(cwd, "env.txt"); + manager = createWorkerTerminalManager(); + const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-env-")); + temporaryDirs.push(cwd); + const markerPath = join(cwd, "env.txt"); - manager.registerCwdEnv({ - cwd, - env: { PASEO_WORKER_TERMINAL_TEST: "worker-env" }, - }); - const session = await manager.createTerminal({ cwd, env: { PS1: "$ " } }); - session.send({ - type: "input", - data: `printf '%s' "$PASEO_WORKER_TERMINAL_TEST" > ${JSON.stringify(markerPath)}\r`, - }); - - await waitForCondition(() => existsSync(markerPath), 10000); - - expect(readFileSync(markerPath, "utf8")).toBe("worker-env"); + manager.registerCwdEnv({ + cwd, + env: { PASEO_WORKER_TERMINAL_TEST: "worker-env" }, }); + await manager.createTerminal({ + cwd, + ...nodeTerminalCommand(` + require("node:fs").writeFileSync( + ${JSON.stringify(markerPath)}, + process.env.PASEO_WORKER_TERMINAL_TEST ?? "", + ); + setInterval(() => {}, 1000); + `), + }); + + await waitForCondition(() => existsSync(markerPath), 10000); + + expect(readFileSync(markerPath, "utf8")).toBe("worker-env"); +}); + +it("starts the default shell through the worker and accepts quoted commands", async () => { + manager = createWorkerTerminalManager(); + const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-shell-")); + temporaryDirs.push(cwd); + const markerPath = join(cwd, "shell quoted marker.txt"); + const session = await manager.createTerminal({ cwd }); + const command = [ + "node", + "-e", + `"require('node:fs').writeFileSync('shell quoted marker.txt','shell-ok')"`, + ].join(" "); + + session.send({ type: "input", data: `${command}\r` }); + + await waitForCondition(() => existsSync(markerPath), 10000); + + expect(readFileSync(markerPath, "utf8")).toBe("shell-ok"); }); it("removes worker terminals after killAndWait", async () => { - await withShell("/bin/sh", async () => { - manager = createWorkerTerminalManager(); - const session = await manager.createTerminal({ cwd: "/tmp", env: { PS1: "$ " } }); - - await manager.killTerminalAndWait(session.id, { - gracefulTimeoutMs: 1000, - forceTimeoutMs: 500, - }); - - await waitForCondition(() => manager?.getTerminal(session.id) === undefined, 5000); - - expect(manager.getTerminal(session.id)).toBeUndefined(); - expect(manager.listDirectories()).not.toContain("/tmp"); + const cwd = mkdtempSync(join(tmpdir(), "worker-terminal-manager-kill-")); + temporaryDirs.push(cwd); + manager = createWorkerTerminalManager(); + const session = await manager.createTerminal({ + cwd, + ...nodeTerminalCommand("setInterval(() => {}, 1000);"), }); + + await manager.killTerminalAndWait(session.id, { + gracefulTimeoutMs: 1000, + forceTimeoutMs: 500, + }); + + await waitForCondition(() => manager?.getTerminal(session.id) === undefined, 5000); + + expect(manager.getTerminal(session.id)).toBeUndefined(); + expect(manager.listDirectories()).not.toContain(cwd); });