diff --git a/packages/cli/src/commands/daemon/local-daemon.ts b/packages/cli/src/commands/daemon/local-daemon.ts index a9d4b9cb8..fc0a2410b 100644 --- a/packages/cli/src/commands/daemon/local-daemon.ts +++ b/packages/cli/src/commands/daemon/local-daemon.ts @@ -287,13 +287,13 @@ function signalProcessGroupSafely(pid: number, signal: NodeJS.Signals): boolean async function waitForPidExit(pid: number, timeoutMs: number): Promise { const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (!isProcessRunning(pid)) { - return true; - } + async function poll(): Promise { + if (!isProcessRunning(pid)) return true; + if (Date.now() >= deadline) return !isProcessRunning(pid); await sleep(PID_POLL_INTERVAL_MS); + return poll(); } - return !isProcessRunning(pid); + return poll(); } type LifecycleShutdownAttempt = { requested: true } | { requested: false; reason: string }; diff --git a/packages/cli/src/commands/daemon/runtime-toolchain.ts b/packages/cli/src/commands/daemon/runtime-toolchain.ts index 1c679d76b..37b182bb5 100644 --- a/packages/cli/src/commands/daemon/runtime-toolchain.ts +++ b/packages/cli/src/commands/daemon/runtime-toolchain.ts @@ -76,25 +76,27 @@ async function resolveNodePathFromPidWindows(pid: number): Promise { + if (index >= probes.length) { + return { + nodePath: null, + error: errors.join("; ") || "could not resolve executable path from PID", + }; + } + const probe = probes[index] as (typeof probes)[number]; const result = await runProcessProbe(probe.command, probe.args); if (result.resolved) { const resolved = probe.parseValue ? probe.parseValue(result.resolved) : result.resolved; - if (resolved) { - return { nodePath: resolved }; - } + if (resolved) return { nodePath: resolved }; errors.push(`${probe.label} returned no executable path`); - continue; - } - if (result.error) { + } else if (result.error) { errors.push(`${probe.label}: ${result.error}`); } + return tryProbe(index + 1); } - return { - nodePath: null, - error: errors.join("; ") || "could not resolve executable path from PID", - }; + return tryProbe(0); } export async function resolveNodePathFromPid(pid: number): Promise { diff --git a/packages/cli/src/commands/loop/logs.ts b/packages/cli/src/commands/loop/logs.ts index 4c307e997..150257e27 100644 --- a/packages/cli/src/commands/loop/logs.ts +++ b/packages/cli/src/commands/loop/logs.ts @@ -44,7 +44,7 @@ export async function runLoopLogsCommand( ): Promise { const host = getDaemonHost({ host: options.host as string | undefined }); const pollInterval = parsePollInterval(options.pollInterval ?? "1000"); - let client; + let client: LoopDaemonClient; try { client = (await connectToDaemon({ host: options.host as string | undefined, @@ -56,23 +56,24 @@ export async function runLoopLogsCommand( process.exit(1); } - let cursor = 0; - try { - for (;;) { - const payload = await client.loopLogs(id, cursor); - if (payload.error || !payload.loop) { - throw new Error(payload.error ?? `Loop not found: ${id}`); - } - cursor = payload.nextCursor; - for (const entry of payload.entries) { - console.log(renderLogEntry(entry)); - } - if (payload.loop.status !== "running") { - await client.close(); - return; - } - await new Promise((resolve) => setTimeout(resolve, pollInterval)); + async function streamLogs(cursor: number): Promise { + const payload = await client.loopLogs(id, cursor); + if (payload.error || !payload.loop) { + throw new Error(payload.error ?? `Loop not found: ${id}`); } + for (const entry of payload.entries) { + console.log(renderLogEntry(entry)); + } + if (payload.loop.status !== "running") { + await client.close(); + return; + } + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + return streamLogs(payload.nextCursor); + } + + try { + await streamLogs(0); } catch (error) { await client.close().catch(() => {}); const message = error instanceof Error ? error.message : String(error); diff --git a/packages/cli/src/commands/onboard.ts b/packages/cli/src/commands/onboard.ts index 9a458f70f..7fee4b03c 100644 --- a/packages/cli/src/commands/onboard.ts +++ b/packages/cli/src/commands/onboard.ts @@ -199,60 +199,85 @@ function renderProgressLine(progress: DownloadProgress): string { return `Downloading speech model${modelSuffix}: ${progress.pct}%`; } +type ProbeResult = { kind: "ready"; listen: string; host: string | null } | { kind: "pending" }; + +async function probeDaemonReady(home: string): Promise { + const state = resolveLocalDaemonState({ home }); + const host = resolveTcpHostFromListen(state.listen); + + if (state.running && host) { + const client = await tryConnectToDaemon({ host, timeout: 1200 }); + if (client) { + try { + await client.fetchAgents(); + return { kind: "ready", listen: state.listen, host }; + } catch { + // Daemon process is alive but not API-ready yet. + } finally { + await client.close().catch(() => {}); + } + } + } else if (state.running && !host) { + return { kind: "ready", listen: state.listen, host: null }; + } + + return { kind: "pending" }; +} + +interface ProgressState { + lastStatus: string; + lastPrintedAt: number; +} + +function announceProgress( + home: string, + state: ProgressState, + onStatus: ((message: string) => void) | undefined, +): ProgressState { + const progress = parseDownloadProgress(tailDaemonLog(home, 120) ?? ""); + const progressLine = progress ? renderProgressLine(progress) : null; + const statusMessage = progressLine ?? "Waiting for daemon to become ready..."; + + if (statusMessage !== state.lastStatus) { + onStatus?.(statusMessage); + return { lastStatus: statusMessage, lastPrintedAt: Date.now() }; + } + if (!onStatus && Date.now() - state.lastPrintedAt >= 3000) { + console.log(statusMessage); + return { lastStatus: state.lastStatus, lastPrintedAt: Date.now() }; + } + return state; +} + async function waitForDaemonReady(args: { home: string; timeoutMs: number; onStatus?: (message: string) => void; }): Promise<{ listen: string; host: string | null }> { const deadline = Date.now() + args.timeoutMs; - let lastStatus = ""; - let lastPrintedAt = 0; - while (Date.now() < deadline) { - const state = resolveLocalDaemonState({ home: args.home }); - const host = resolveTcpHostFromListen(state.listen); - - if (state.running && host) { - const client = await tryConnectToDaemon({ host, timeout: 1200 }); - if (client) { - try { - await client.fetchAgents(); - return { listen: state.listen, host }; - } catch { - // Daemon process is alive but not API-ready yet. - } finally { - await client.close().catch(() => {}); - } - } - } else if (state.running && !host) { - return { listen: state.listen, host: null }; + async function poll(state: ProgressState): Promise<{ listen: string; host: string | null }> { + const probe = await probeDaemonReady(args.home); + if (probe.kind === "ready") { + return { listen: probe.listen, host: probe.host }; } - - const progress = parseDownloadProgress(tailDaemonLog(args.home, 120) ?? ""); - const progressLine = progress ? renderProgressLine(progress) : null; - const statusMessage = progressLine ?? "Waiting for daemon to become ready..."; - - if (statusMessage !== lastStatus) { - args.onStatus?.(statusMessage); - lastStatus = statusMessage; - lastPrintedAt = Date.now(); - } else if (!args.onStatus && Date.now() - lastPrintedAt >= 3000) { - console.log(statusMessage); - lastPrintedAt = Date.now(); + const nextState = announceProgress(args.home, state, args.onStatus); + if (Date.now() >= deadline) { + const recentLogs = tailDaemonLog(args.home, 60); + throw new Error( + [ + `Timed out after ${Math.ceil(args.timeoutMs / 1000)}s waiting for daemon readiness.`, + recentLogs ? `Recent daemon logs:\n${recentLogs}` : null, + ] + .filter(Boolean) + .join("\n\n"), + ); } - await sleep(200); + return poll(nextState); } - const recentLogs = tailDaemonLog(args.home, 60); - throw new Error( - [ - `Timed out after ${Math.ceil(args.timeoutMs / 1000)}s waiting for daemon readiness.`, - recentLogs ? `Recent daemon logs:\n${recentLogs}` : null, - ] - .filter(Boolean) - .join("\n\n"), - ); + return poll({ lastStatus: "", lastPrintedAt: 0 }); } function printNextSteps(pairingUrl: string | null, paseoHome: string, richUi: boolean): void { diff --git a/packages/cli/src/utils/client.ts b/packages/cli/src/utils/client.ts index e7f1ba3e7..c46c006cd 100644 --- a/packages/cli/src/utils/client.ts +++ b/packages/cli/src/utils/client.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync } from "node:fs"; import { loadConfig, resolvePaseoHome, DaemonClient } from "@getpaseo/server"; import path from "node:path"; -import WebSocket from "ws"; +import { WebSocket } from "ws"; import { getOrCreateCliClientId } from "./client-id.js"; import { resolveCliVersion } from "../version.js"; @@ -195,45 +195,54 @@ function createNodeWebSocketFactory() { * Create and connect a daemon client * Returns the connected client or throws if connection fails */ +async function tryConnectHost( + host: string, + clientId: string, + timeout: number, + nodeWebSocketFactory: ReturnType, +): Promise<{ client: DaemonClient } | { error: unknown }> { + const target = resolveDaemonTarget(host); + const client = new DaemonClient({ + url: target.url, + clientId, + clientType: "cli", + appVersion: resolveCliVersion(), + connectTimeoutMs: timeout, + webSocketFactory: (url: string, config?: { headers?: Record }) => + nodeWebSocketFactory(url, { + headers: config?.headers, + ...(target.type === "ipc" ? { socketPath: target.socketPath } : {}), + }), + reconnect: { enabled: false }, + } as unknown as ConstructorParameters[0]); + + try { + await client.connect(); + return { client }; + } catch (error) { + await client.close().catch(() => {}); + return { error }; + } +} + export async function connectToDaemon(options?: ConnectOptions): Promise { const timeout = options?.timeout ?? DEFAULT_TIMEOUT; const clientId = await getOrCreateCliClientId(); const hosts = resolveDaemonHostCandidates(options); const nodeWebSocketFactory = createNodeWebSocketFactory(); - let lastError: unknown = null; - for (const host of hosts) { - const target = resolveDaemonTarget(host); - const client = new DaemonClient({ - url: target.url, - clientId, - clientType: "cli", - appVersion: resolveCliVersion(), - connectTimeoutMs: timeout, - webSocketFactory: (url: string, config?: { headers?: Record }) => - nodeWebSocketFactory(url, { - headers: config?.headers, - ...(target.type === "ipc" ? { socketPath: target.socketPath } : {}), - }), - reconnect: { enabled: false }, - } as unknown as ConstructorParameters[0]); - - const connectPromise = client.connect(); - - try { - await connectPromise; - return client; - } catch (err) { - lastError = err; - await client.close().catch(() => {}); + async function tryNext(index: number, lastError: unknown): Promise { + if (index >= hosts.length) { + if (lastError instanceof Error) throw lastError; + throw new Error(`Unable to connect to Paseo daemon via ${hosts.join(", ")}`); } + const host = hosts[index] as string; + const result = await tryConnectHost(host, clientId, timeout, nodeWebSocketFactory); + if ("client" in result) return result.client; + return tryNext(index + 1, result.error); } - if (lastError instanceof Error) { - throw lastError; - } - - throw new Error(`Unable to connect to Paseo daemon via ${hosts.join(", ")}`); + return tryNext(0, null); } /** diff --git a/packages/cli/tests/15-provider.test.ts b/packages/cli/tests/15-provider.test.ts index cd985c9c3..2510f5670 100644 --- a/packages/cli/tests/15-provider.test.ts +++ b/packages/cli/tests/15-provider.test.ts @@ -71,7 +71,7 @@ const ctx = await createE2ETestContext({ timeout: 120000 }); async function runProviderModelsJson(provider: string): Promise { const transientNeedles = ["transport closed", "timed out", "timeout", "socket", "econn"]; - for (let attempt = 1; attempt <= 3; attempt++) { + async function attemptRun(attempt: number): Promise { const result = await ctx.paseo(["provider", "models", provider, "--json"]); if (result.exitCode === 0) { return JSON.parse(result.stdout.trim()) as ProviderModel[]; @@ -86,9 +86,10 @@ async function runProviderModelsJson(provider: string): Promise } await new Promise((resolve) => setTimeout(resolve, 250 * attempt)); + return attemptRun(attempt + 1); } - assert.fail(`provider models ${provider} exhausted retries`); + return attemptRun(1); } function assertClaudeModels(data: ProviderModel[]): void { diff --git a/packages/cli/tests/22-daemon-stop-supervisor.test.ts b/packages/cli/tests/22-daemon-stop-supervisor.test.ts index 5157a3acd..71c129c77 100644 --- a/packages/cli/tests/22-daemon-stop-supervisor.test.ts +++ b/packages/cli/tests/22-daemon-stop-supervisor.test.ts @@ -100,14 +100,14 @@ async function waitFor( ): Promise { const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await check()) { - return; - } + async function poll(): Promise { + if (await check()) return; + if (Date.now() >= deadline) throw new Error(message); await sleep(pollIntervalMs); + return poll(); } - throw new Error(message); + return poll(); } console.log("=== Daemon Stop (supervisor regression) ===\n"); diff --git a/packages/cli/tests/23-daemon-sigint-supervisor.test.ts b/packages/cli/tests/23-daemon-sigint-supervisor.test.ts index 1b76bc6b1..070bdb4ae 100644 --- a/packages/cli/tests/23-daemon-sigint-supervisor.test.ts +++ b/packages/cli/tests/23-daemon-sigint-supervisor.test.ts @@ -93,14 +93,14 @@ async function waitFor( ): Promise { const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await check()) { - return; - } + async function poll(): Promise { + if (await check()) return; + if (Date.now() >= deadline) throw new Error(message); await sleep(pollIntervalMs); + return poll(); } - throw new Error(message); + return poll(); } interface ExitResult { diff --git a/packages/cli/tests/24-daemon-stop-ownership.test.ts b/packages/cli/tests/24-daemon-stop-ownership.test.ts index 966f804ff..c4d666d55 100644 --- a/packages/cli/tests/24-daemon-stop-ownership.test.ts +++ b/packages/cli/tests/24-daemon-stop-ownership.test.ts @@ -39,13 +39,15 @@ function isProcessRunning(pid: number): boolean { async function waitForRunning(pid: number, timeoutMs: number): Promise { const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (isProcessRunning(pid)) { - return; + async function poll(): Promise { + if (isProcessRunning(pid)) return; + if (Date.now() >= deadline) { + throw new Error(`Process ${pid} did not become running in time`); } await sleep(50); + return poll(); } - throw new Error(`Process ${pid} did not become running in time`); + return poll(); } console.log("=== Daemon Stop Ownership Regression ===\n"); diff --git a/packages/cli/tests/25-daemon-restart-supervisor.test.ts b/packages/cli/tests/25-daemon-restart-supervisor.test.ts index 905146999..a999d4784 100644 --- a/packages/cli/tests/25-daemon-restart-supervisor.test.ts +++ b/packages/cli/tests/25-daemon-restart-supervisor.test.ts @@ -101,14 +101,14 @@ async function waitFor( ): Promise { const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await check()) { - return; - } + async function poll(): Promise { + if (await check()) return; + if (Date.now() >= deadline) throw new Error(message); await sleep(pollIntervalMs); + return poll(); } - throw new Error(message); + return poll(); } console.log("=== Daemon Restart (supervisor regression) ===\n"); diff --git a/packages/cli/tests/26-daemon-restart-unsupervised.test.ts b/packages/cli/tests/26-daemon-restart-unsupervised.test.ts index d29e8c16d..9cc2e847c 100644 --- a/packages/cli/tests/26-daemon-restart-unsupervised.test.ts +++ b/packages/cli/tests/26-daemon-restart-unsupervised.test.ts @@ -44,14 +44,14 @@ async function waitFor( ): Promise { const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await check()) { - return; - } + async function poll(): Promise { + if (await check()) return; + if (Date.now() >= deadline) throw new Error(message); await sleep(pollIntervalMs); + return poll(); } - throw new Error(message); + return poll(); } interface ExitResult { @@ -75,16 +75,18 @@ function waitForProcessExit(processRef: ChildProcess, timeoutMs: number): Promis async function canConnectToDaemon(host: string, timeoutMs: number): Promise { const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { + async function poll(): Promise { const client = await tryConnectToDaemon({ host, timeout: 500 }).catch(() => null); if (client) { await client.close().catch(() => undefined); return true; } + if (Date.now() >= deadline) return false; await sleep(pollIntervalMs); + return poll(); } - return false; + return poll(); } async function readPidLockPid(paseoHome: string): Promise { diff --git a/packages/cli/tests/31-loop-schedule.test.ts b/packages/cli/tests/31-loop-schedule.test.ts index 5d973a959..dc0381d22 100644 --- a/packages/cli/tests/31-loop-schedule.test.ts +++ b/packages/cli/tests/31-loop-schedule.test.ts @@ -141,18 +141,20 @@ try { listed.stdout, ); - let status = "running"; - for (let attempt = 0; attempt < 40; attempt += 1) { + async function pollStatus(attempt: number): Promise { + if (attempt >= 40) return "running"; const inspect = await ctx.paseo(["loop", "inspect", runJson.id, "--json"]); assert.strictEqual(inspect.exitCode, 0, inspect.stderr); const inspectJson = JSON.parse(inspect.stdout); - status = inspectJson.status; - if (status !== "running") { - assert.strictEqual(status, "succeeded", inspect.stdout); - break; + const current = inspectJson.status; + if (current !== "running") { + assert.strictEqual(current, "succeeded", inspect.stdout); + return current; } await sleep(250); + return pollStatus(attempt + 1); } + const status = await pollStatus(0); assert.strictEqual(status, "succeeded"); const logs = await ctx.paseo(["loop", "logs", runJson.id], { timeout: 15000 }); diff --git a/packages/cli/tests/e2e/permissions.test.ts b/packages/cli/tests/e2e/permissions.test.ts index ebfbec693..340266b68 100644 --- a/packages/cli/tests/e2e/permissions.test.ts +++ b/packages/cli/tests/e2e/permissions.test.ts @@ -130,19 +130,23 @@ async function test_wait_for_permission_request(agentId: string): Promise // Poll for permission requests with timeout const maxWait = 60000; // 60 seconds max const pollInterval = 1000; // 1 second - const startTime = Date.now(); + const deadline = Date.now() + maxWait; - while (Date.now() - startTime < maxWait) { + async function pollPermission(): Promise { const result = await ctx.paseo(["permit", "ls", "--json"]); const ourPermission = findMatchingPermission(result, agentId); if (ourPermission) { console.log("Permission request detected:", ourPermission); console.log("PASS: Agent requested permission"); - return; + return true; } + if (Date.now() >= deadline) return false; await sleep(pollInterval); + return pollPermission(); } + if (await pollPermission()) return; + // If we get here, check agent status - it might have already completed const statusResult = await ctx.paseo(["inspect", agentId]); console.log("Agent status:", statusResult.stdout); diff --git a/packages/cli/tests/helpers/test-daemon.ts b/packages/cli/tests/helpers/test-daemon.ts index 8be8fd679..57fafb0e3 100644 --- a/packages/cli/tests/helpers/test-daemon.ts +++ b/packages/cli/tests/helpers/test-daemon.ts @@ -167,33 +167,39 @@ export async function createTempDirs(): Promise<{ paseoHome: string; workDir: st * Wait for daemon to be ready by running `paseo agent ls` * This connects via WebSocket and ensures the daemon is responsive */ +async function probeDaemonReady(port: number): Promise { + try { + const { exitCode } = await runPaseoCli( + { + port, + wsUrl: `ws://${TEST_DAEMON_HOST}:${port}`, + paseoHome: "", + workDir: "", + process: null, + isReady: false, + stop: async () => {}, + }, + ["agent", "ls"], + ); + return exitCode === 0; + } catch { + return false; + } +} + async function waitForDaemonReady(port: number, timeout = 30000): Promise { - const start = Date.now(); + const deadline = Date.now() + timeout; - while (Date.now() - start < timeout) { - try { - const { exitCode } = await runPaseoCli( - { - port, - wsUrl: `ws://${TEST_DAEMON_HOST}:${port}`, - paseoHome: "", - workDir: "", - process: null, - isReady: false, - stop: async () => {}, - }, - ["agent", "ls"], - ); - - if (exitCode === 0) { - return; // Daemon is ready - } - } catch { - // Connection failed, keep trying + async function poll(): Promise { + if (await probeDaemonReady(port)) return; + if (Date.now() >= deadline) { + throw new Error(`Daemon failed to become ready on port ${port} within ${timeout}ms`); } await sleep(100); + return poll(); } - throw new Error(`Daemon failed to become ready on port ${port} within ${timeout}ms`); + + return poll(); } function sleep(ms: number): Promise { diff --git a/packages/cli/tests/run-all.ts b/packages/cli/tests/run-all.ts index dcd7f0f08..9a8c0e97d 100644 --- a/packages/cli/tests/run-all.ts +++ b/packages/cli/tests/run-all.ts @@ -118,7 +118,9 @@ await runCommand("Building relay", "npm run build --workspace=@getpaseo/relay"); await runCommand("Building server", "npm run build --workspace=@getpaseo/server"); await runCommand("Building CLI", "npm run build --workspace=@getpaseo/cli"); -for (const testFile of testFiles) { +type TestOutcome = { status: "passed" } | { status: "failed"; failure: Failure }; + +async function runSingleTest(testFile: string): Promise { const testPath = join(__dirname, testFile); const testName = testFile.replace(/\.test\.ts$/, ""); @@ -131,26 +133,38 @@ for (const testFile of testFiles) { await $`PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnvDefaults.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnvDefaults.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnvDefaults.PASEO_VOICE_MODE_ENABLED} npx tsx ${testPath}`.nothrow(); if (result.exitCode === 0) { console.log(`\nāœ… ${testName} PASSED`); - passed++; - } else { - console.log(`\nāŒ ${testName} FAILED (exit code: ${result.exitCode})`); - if (result.stderr) { - console.log("stderr:", result.stderr); - } - failed++; - failures.push({ test: testName, error: result.stderr || `Exit code: ${result.exitCode}` }); - break; + return { status: "passed" }; } + console.log(`\nāŒ ${testName} FAILED (exit code: ${result.exitCode})`); + if (result.stderr) { + console.log("stderr:", result.stderr); + } + return { + status: "failed", + failure: { test: testName, error: result.stderr || `Exit code: ${result.exitCode}` }, + }; } catch (e) { const error = e instanceof Error ? e.message : String(e); console.log(`\nāŒ ${testName} FAILED`); console.log("Error:", error); - failed++; - failures.push({ test: testName, error }); - break; + return { status: "failed", failure: { test: testName, error } }; } } +async function runRemainingTests(index: number): Promise { + if (index >= testFiles.length) return; + const testFile = testFiles[index] as string; + const outcome = await runSingleTest(testFile); + if (outcome.status === "passed") { + passed++; + return runRemainingTests(index + 1); + } + failed++; + failures.push(outcome.failure); +} + +await runRemainingTests(0); + // Summary console.log("\n" + "=".repeat(50)); console.log("šŸ“Š Test Results"); diff --git a/packages/cli/tests/setup.ts b/packages/cli/tests/setup.ts index 78267d6a9..94f872300 100644 --- a/packages/cli/tests/setup.ts +++ b/packages/cli/tests/setup.ts @@ -83,18 +83,26 @@ export async function createTempDirs(): Promise<{ paseoHome: string; workDir: st * Wait for daemon to be ready by testing WebSocket connection * Uses `paseo agent ls` which connects via WebSocket */ +async function probeDaemon(port: number): Promise { + try { + const result = await $`PASEO_HOST=localhost:${port} paseo agent ls`.nothrow(); + return result.exitCode === 0; + } catch { + return false; + } +} + export async function waitForDaemon(port: number, timeout = 30000): Promise { - const start = Date.now(); - while (Date.now() - start < timeout) { - try { - const result = await $`PASEO_HOST=localhost:${port} paseo agent ls`.nothrow(); - if (result.exitCode === 0) return; - } catch { - // Connection failed, keep trying + const deadline = Date.now() + timeout; + async function poll(): Promise { + if (await probeDaemon(port)) return; + if (Date.now() >= deadline) { + throw new Error(`Daemon failed to start on port ${port} within ${timeout}ms`); } await sleep(100); + return poll(); } - throw new Error(`Daemon failed to start on port ${port} within ${timeout}ms`); + return poll(); } /** diff --git a/packages/cli/tests/tmp/opencode-simple.ts b/packages/cli/tests/tmp/opencode-simple.ts index dd2aecb95..c61e6069b 100644 --- a/packages/cli/tests/tmp/opencode-simple.ts +++ b/packages/cli/tests/tmp/opencode-simple.ts @@ -1,4 +1,4 @@ -import { createE2ETestContext } from "/Users/moboudra/.paseo/worktrees/1luy0po7/beefy-parrot/packages/cli/tests/helpers/test-daemon.ts"; +import { createE2ETestContext } from "../helpers/test-daemon.js"; async function main() { const ctx = await createE2ETestContext({ timeout: 180000 });