From 01c2b7f66e468eb929dc15839476c541cef4d936 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 9 Sep 2026 15:38:32 +0200 Subject: [PATCH] fix(daemon): preserve reachability when status details time out Provider discovery can outlast a status request after authentication succeeds. Keep local connection facts separate from unavailable details, while explicit remote queries retain their error outcome. Migrate CLI and packaged Desktop fixtures to persistent managed configuration and explicit targets. Isolate generic Git observation from forge discovery and reuse disposable CLI homes on Windows. --- packages/cli/src/commands/daemon/status.ts | 113 +++++++++++------- packages/cli/tests/04-agent-ls.test.ts | 36 +++--- packages/cli/tests/05-agent-run.test.ts | 37 +++--- packages/cli/tests/06-agent-send.test.ts | 23 ++-- packages/cli/tests/07-agent-stop.test.ts | 17 +-- packages/cli/tests/08-agent-logs.test.ts | 19 +-- packages/cli/tests/09-agent-inspect.test.ts | 15 +-- packages/cli/tests/10-agent-mode.test.ts | 17 +-- packages/cli/tests/11-agent-archive.test.ts | 15 +-- packages/cli/tests/11-agent-wait.test.ts | 15 +-- packages/cli/tests/12-permit-ls.test.ts | 11 +- .../cli/tests/13-permit-allow-deny.test.ts | 23 ++-- packages/cli/tests/14-worktree.test.ts | 19 +-- packages/cli/tests/16-agent-update.test.ts | 14 +-- packages/cli/tests/17-onboard.test.ts | 22 +++- packages/cli/tests/27-agent-delete.test.ts | 19 ++- .../cli/tests/34-daemon-status-auth.test.ts | 77 ++++++++++++ ...emon-status-windows-process-lookup.test.ts | 33 +---- packages/desktop/e2e/packaged-app-smoke.js | 25 +--- .../workspace-git-service.observation.test.ts | 25 ++-- public-docs/cli.md | 2 +- public-docs/connectivity.md | 2 +- 22 files changed, 330 insertions(+), 249 deletions(-) diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts index 787e5d22d6..c44e01524e 100644 --- a/packages/cli/src/commands/daemon/status.ts +++ b/packages/cli/src/commands/daemon/status.ts @@ -1,4 +1,5 @@ import { Command } from "commander"; +import { DaemonConnectionError } from "@getpaseo/client/internal/daemon-client"; import { readDaemonInstance, readPersistedConfig, @@ -8,9 +9,9 @@ import { DaemonInstanceError, } from "@getpaseo/server"; import { connectToDaemon, buildDaemonConnectionCommandError } from "../../utils/client.js"; -import { withOutput, type CommandOptions } from "../../output/index.js"; +import { withOutput, toCommandError, type CommandOptions } from "../../output/index.js"; import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; -import { describeDaemonTarget } from "../../utils/daemon-target.js"; +import { describeDaemonTarget, type DaemonTarget } from "../../utils/daemon-target.js"; export function daemonStatusCommand(): Command { return addJsonAndDaemonHostOptions( @@ -25,46 +26,11 @@ export async function runStatusCommand(options: CommandOptions, _command: Comman target.kind === "instance" ? localStatus(target.home, instance) : { host: describeDaemonTarget(target) }; - let connectedDaemon = "not_probed"; - let note: string | undefined; - let live: Record = {}; - if (target.kind === "endpoint" || instance?.listen) { - try { - const client = await connectToDaemon({ - target, - instance: instance ?? undefined, - timeout: 1_500, - }); - try { - const status = await client.getDaemonStatus({ timeout: 1_500 }); - const current = target.kind === "instance" ? await readDaemonInstance(target.home) : null; - if (instance && (!current || !isSameDaemonInstance(instance, current))) - throw new DaemonInstanceError( - "DAEMON_REPLACED", - "Supervisor exited or was replaced during status observation.", - ); - live = { - serverId: client.getLastServerInfoMessage()?.serverId ?? null, - daemonVersion: status.version, - workerPid: status.pid, - daemonNode: status.nodePath, - providers: status.providers, - relay: status.relay, - }; - connectedDaemon = "reachable"; - } finally { - await client.close(); - } - } catch (error) { - const failure = buildDaemonConnectionCommandError({ target, error }); - if (target.kind === "endpoint") throw failure; - connectedDaemon = "unreachable"; - if (failure.code === "AUTH_REQUIRED") connectedDaemon = "auth_required"; - if (failure.code === "AUTH_FAILED") connectedDaemon = "auth_failed"; - note = failure.message; - } - } - const data: Record = { ...local, ...live, connectedDaemon, note }; + const observed = + target.kind === "endpoint" || instance?.listen + ? await probeDaemonStatus(target, instance, local) + : { connectedDaemon: "not_probed" }; + const data: Record = { ...local, ...observed }; return { type: "single" as const, data, @@ -83,6 +49,69 @@ export async function runStatusCommand(options: CommandOptions, _command: Comman }; } +async function probeDaemonStatus( + target: DaemonTarget, + instance: Awaited>, + local: Record, +) { + let connectedDaemon = "unreachable"; + let note: string | undefined; + let live: Record = {}; + let client: Awaited> | undefined; + try { + client = await connectToDaemon({ target, instance: instance ?? undefined, timeout: 1_500 }); + } catch (error) { + const failure = buildDaemonConnectionCommandError({ target, error }); + if (target.kind === "endpoint") throw failure; + connectedDaemon = "unreachable"; + if (failure.code === "AUTH_REQUIRED") connectedDaemon = "auth_required"; + if (failure.code === "AUTH_FAILED") connectedDaemon = "auth_failed"; + note = failure.message; + } + if (!client) return { connectedDaemon, note }; + try { + let requestError: unknown; + const status = await client.getDaemonStatus({ timeout: 1_500 }).catch((error: unknown) => { + requestError = error; + return null; + }); + // Recheck even when the RPC times out before combining local and live observations. + const current = target.kind === "instance" ? await readDaemonInstance(target.home) : null; + if (instance && (!current || !isSameDaemonInstance(instance, current))) + throw new DaemonInstanceError( + "DAEMON_REPLACED", + "Supervisor exited or was replaced during status observation.", + ); + const info = client.getLastServerInfoMessage(); + live = { serverId: info?.serverId, daemonVersion: info?.version }; + connectedDaemon = client.isConnected ? "reachable" : "unreachable"; + if (!status) { + const failure = toCommandError(requestError); + note = `Status details unavailable (${failure.code}): ${failure.message}`; + if ( + target.kind === "endpoint" || + !client.isConnected || + !(requestError instanceof DaemonConnectionError) || + requestError.code !== "DAEMON_REQUEST_TIMEOUT" + ) { + throw { ...failure, message: note, details: { ...local, ...live, connectedDaemon } }; + } + } else { + live = { + ...live, + daemonVersion: status.version, + workerPid: status.pid, + daemonNode: status.nodePath, + providers: status.providers, + relay: status.relay, + }; + } + } finally { + await client.close(); + } + return { ...live, connectedDaemon, note }; +} + function localStatus(home: string, instance: Awaited>) { const config = resolveConfigFromPersisted( home, diff --git a/packages/cli/tests/04-agent-ls.test.ts b/packages/cli/tests/04-agent-ls.test.ts index 363eba5694..65b3bec0bc 100644 --- a/packages/cli/tests/04-agent-ls.test.ts +++ b/packages/cli/tests/04-agent-ls.test.ts @@ -23,12 +23,13 @@ import assert from "node:assert"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; +import { getAvailablePort } from "./helpers/network.ts"; import { runLocalPaseo } from "./helpers/local-cli.ts"; console.log("=== LS Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -62,7 +63,6 @@ try { console.log("Test 3: paseo ls handles daemon not running"); const result = await runLocalPaseo(["ls"], { PASEO_HOST: `localhost:${port}`, - PASEO_HOME: paseoHome, }); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); @@ -74,8 +74,8 @@ try { assert(hasError, "error message should mention connection issue"); assert.match( output, - /--host .*PASEO_HOST/s, - "the recovery message should explain both remote connection inputs", + /Check the selected endpoint and credentials/, + "the recovery message should explain how to check the selected endpoint", ); console.log("✓ paseo ls handles daemon not running\n"); } @@ -85,7 +85,6 @@ try { console.log("Test 4: paseo ls --json handles errors"); const result = await runLocalPaseo(["ls", "--json"], { PASEO_HOST: `localhost:${port}`, - PASEO_HOME: paseoHome, }); // Should still fail (daemon not running) assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); @@ -109,7 +108,6 @@ try { console.log("Test 5: paseo ls -a flag is accepted"); const result = await runLocalPaseo(["ls", "-a"], { PASEO_HOST: `localhost:${port}`, - PASEO_HOME: paseoHome, }); // Will fail due to no daemon, but flag should be parsed without error // (no "unknown option" error) @@ -124,7 +122,6 @@ try { console.log("Test 6: paseo ls -g flag is accepted"); const result = await runLocalPaseo(["ls", "-g"], { PASEO_HOST: `localhost:${port}`, - PASEO_HOME: paseoHome, }); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -g flag"); @@ -137,7 +134,6 @@ try { console.log("Test 7: paseo ls -ag combined flags are accepted"); const result = await runLocalPaseo(["ls", "-ag"], { PASEO_HOST: `localhost:${port}`, - PASEO_HOME: paseoHome, }); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -ag flags"); @@ -150,7 +146,6 @@ try { console.log("Test 8: -q (quiet) flag is accepted"); const result = await runLocalPaseo(["-q", "ls"], { PASEO_HOST: `localhost:${port}`, - PASEO_HOME: paseoHome, }); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); @@ -163,7 +158,6 @@ try { console.log("Test 9: paseo ls --ui is rejected"); const result = await runLocalPaseo(["ls", "--ui"], { PASEO_HOST: `localhost:${port}`, - PASEO_HOME: paseoHome, }); assert.notStrictEqual(result.exitCode, 0, "should fail for removed --ui flag"); const output = result.stdout + result.stderr; @@ -185,17 +179,23 @@ try { console.log("✓ global --host targets the requested daemon\n"); } - // Test 11: the last explicit --host wins + // Test 11: conflicting explicit --host selectors are rejected { - console.log("Test 11: the last explicit --host wins"); + console.log("Test 11: conflicting explicit --host selectors are rejected"); const firstHost = `localhost:${port}`; - const lastHost = `localhost:${port + 1}`; + const lastHost = `localhost:${await getAvailablePort()}`; const result = await runLocalPaseo(["--host", firstHost, "ls", "--host", lastHost]); const output = result.stdout + result.stderr; - assert.notStrictEqual(result.exitCode, 0, "should fail when the selected daemon is absent"); - assert(output.includes(lastHost), "connection error should name the last explicit host"); - assert(!output.includes(firstHost), "the earlier host should be fully overridden"); - console.log("✓ the last explicit --host wins\n"); + assert.notStrictEqual(result.exitCode, 0, "should reject conflicting explicit selectors"); + assert( + output.includes("Conflicting duplicate --host selectors."), + "should report the selector conflict", + ); + assert( + !output.includes("Cannot connect"), + "must reject the selectors before attempting a connection", + ); + console.log("✓ conflicting explicit --host selectors are rejected\n"); } } finally { // Clean up temp directory diff --git a/packages/cli/tests/05-agent-run.test.ts b/packages/cli/tests/05-agent-run.test.ts index 0af2a6ec7c..5c56cadfbf 100644 --- a/packages/cli/tests/05-agent-run.test.ts +++ b/packages/cli/tests/05-agent-run.test.ts @@ -21,6 +21,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; @@ -30,8 +31,8 @@ $.verbose = false; console.log("=== Run Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); const schemaPath = join(paseoHome, "run-output-schema.json"); await writeFile( @@ -72,7 +73,7 @@ try { { console.log("Test 2: run requires prompt argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without prompt"); const output = result.stdout + result.stderr; // Commander should complain about missing argument @@ -88,7 +89,7 @@ try { { console.log("Test 3: run handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider claude "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --provider claude "test prompt"`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -104,7 +105,7 @@ try { { console.log("Test 4: run -d flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run -d "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run -d "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -d flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -115,7 +116,7 @@ try { { console.log("Test 5: run --name flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --name "test-agent" "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --name "test-agent" "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --name flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -126,7 +127,7 @@ try { { console.log("Test 6: run --provider flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider codex "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --provider codex "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --provider flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -137,7 +138,7 @@ try { { console.log("Test 6b: run --provider provider/model syntax is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider codex/gpt-5.4 "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --provider codex/gpt-5.4 "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept provider/model syntax"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -148,7 +149,7 @@ try { { console.log("Test 7: run --mode flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --mode bypass "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --mode bypass "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --mode flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -159,7 +160,7 @@ try { { console.log("Test 8: run --cwd flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --cwd /tmp "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --cwd /tmp "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --cwd flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -170,7 +171,7 @@ try { { console.log("Test 9: run --output-schema flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --output-schema ${schemaPath} "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --output-schema ${schemaPath} "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --output-schema flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -181,7 +182,7 @@ try { { console.log("Test 10: run --output-schema cannot be used with --background"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --background --output-schema ${schemaPath} "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --background --output-schema ${schemaPath} "test prompt"`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail with --background and --output-schema"); const output = result.stdout + result.stderr; assert( @@ -195,7 +196,7 @@ try { { console.log("Test 11: -q (quiet) flag is accepted with run"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q run -d "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -206,7 +207,7 @@ try { { console.log("Test 12: Combined flags work together"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d --name "test-fixer" --provider claude --mode bypass --cwd /tmp "Fix the tests"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q run -d --name "test-fixer" --provider claude --mode bypass --cwd /tmp "Fix the tests"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept all combined flags"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -217,7 +218,7 @@ try { { console.log("Test 12b: conflicting provider/model syntax is rejected"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --provider codex/gpt-5.4 --model gpt-5.5 "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --provider codex/gpt-5.4 --model gpt-5.5 "test prompt"`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail for conflicting model inputs"); const output = result.stdout + result.stderr; assert( @@ -240,7 +241,7 @@ try { { console.log("Test 14: run --ui is rejected"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --ui "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --ui "test prompt"`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail for removed --ui flag"); const output = result.stdout + result.stderr; assert(output.includes("unknown option"), "should report unknown option for --ui"); @@ -251,7 +252,7 @@ try { { console.log("Test 15: run --new-workspace is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --new-workspace local "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --new-workspace local "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --new-workspace"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -262,7 +263,7 @@ try { { console.log("Test 16: run --isolation is rejected"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --isolation local "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} run --isolation local "test prompt"`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail for removed --isolation flag"); const output = result.stdout + result.stderr; assert(output.includes("unknown option"), "should report unknown option for --isolation"); diff --git a/packages/cli/tests/06-agent-send.test.ts b/packages/cli/tests/06-agent-send.test.ts index 60fd9aac72..ca28e26f56 100644 --- a/packages/cli/tests/06-agent-send.test.ts +++ b/packages/cli/tests/06-agent-send.test.ts @@ -18,6 +18,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; @@ -27,8 +28,8 @@ $.verbose = false; console.log("=== Send Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); const promptFilePath = join(paseoHome, "send-prompt.txt"); await writeFile(promptFilePath, "prompt from file"); @@ -58,7 +59,7 @@ try { { console.log("Test 2: send requires id argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} send`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without id"); const output = result.stdout + result.stderr; // Commander should complain about missing argument @@ -74,7 +75,7 @@ try { { console.log("Test 3: send requires prompt argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} send abc123`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without prompt"); const output = result.stdout + result.stderr; // Commander should complain about missing argument @@ -90,7 +91,7 @@ try { { console.log("Test 4: send handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send abc123 "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} send abc123 "test prompt"`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -106,7 +107,7 @@ try { { console.log("Test 5: send --no-wait flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send --no-wait abc123 "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} send --no-wait abc123 "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --no-wait flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -117,7 +118,7 @@ try { { console.log("Test 5b: send --prompt flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send --prompt "test prompt" abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} send --prompt "test prompt" abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --prompt flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -128,7 +129,7 @@ try { { console.log("Test 5c: send --prompt-file flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send --prompt-file ${promptFilePath} abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} send --prompt-file ${promptFilePath} abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --prompt-file flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -150,7 +151,7 @@ try { { console.log("Test 7: -q (quiet) flag is accepted with send"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q send --no-wait abc123 "test prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q send --no-wait abc123 "test prompt"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -161,7 +162,7 @@ try { { console.log("Test 8: Combined flags work together"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q send --no-wait abc123 "Run the linter"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q send --no-wait abc123 "Run the linter"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept all combined flags"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -172,7 +173,7 @@ try { { console.log("Test 8b: conflicting prompt sources are rejected"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo send abc123 "positional prompt" --prompt "flag prompt"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} send abc123 "positional prompt" --prompt "flag prompt"`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail for conflicting prompt sources"); const output = result.stdout + result.stderr; assert( diff --git a/packages/cli/tests/07-agent-stop.test.ts b/packages/cli/tests/07-agent-stop.test.ts index 0b20028ec0..81b0cba66c 100644 --- a/packages/cli/tests/07-agent-stop.test.ts +++ b/packages/cli/tests/07-agent-stop.test.ts @@ -18,6 +18,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -27,8 +28,8 @@ $.verbose = false; console.log("=== Stop Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -48,7 +49,7 @@ try { { console.log("Test 2: stop requires ID, --all, or --cwd"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} stop`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without id, --all, or --cwd"); const output = result.stdout + result.stderr; const hasError = @@ -64,7 +65,7 @@ try { { console.log("Test 3: stop handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} stop abc123`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -80,7 +81,7 @@ try { { console.log("Test 4: stop --all flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop --all`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} stop --all`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --all flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -91,7 +92,7 @@ try { { console.log("Test 5: stop --cwd flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop --cwd /tmp`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} stop --cwd /tmp`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --cwd flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -102,7 +103,7 @@ try { { console.log("Test 6: stop with ID and --host flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo stop abc123 --host localhost:${port}`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} stop abc123 --host localhost:${port}`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --host flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -122,7 +123,7 @@ try { { console.log("Test 8: -q (quiet) flag is accepted with stop"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q stop abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q stop abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); diff --git a/packages/cli/tests/08-agent-logs.test.ts b/packages/cli/tests/08-agent-logs.test.ts index f857bffdff..672f4a2dd0 100644 --- a/packages/cli/tests/08-agent-logs.test.ts +++ b/packages/cli/tests/08-agent-logs.test.ts @@ -18,6 +18,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -27,8 +28,8 @@ $.verbose = false; console.log("=== Logs Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -51,7 +52,7 @@ try { { console.log("Test 2: logs requires ID argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} logs`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without id"); const output = result.stdout + result.stderr; const hasError = @@ -67,7 +68,7 @@ try { { console.log("Test 3: logs handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} logs abc123`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -84,7 +85,7 @@ try { console.log("Test 4: logs -f (follow) flag is accepted"); // Use timeout to avoid hanging on follow mode const result = - await $`timeout 1 bash -c 'PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs -f abc123' || true`.nothrow(); + await $`timeout 1 bash -c 'PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} logs -f abc123' || true`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -f flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -95,7 +96,7 @@ try { { console.log("Test 5: logs --follow flag is accepted"); const result = - await $`timeout 1 bash -c 'PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs --follow abc123' || true`.nothrow(); + await $`timeout 1 bash -c 'PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} logs --follow abc123' || true`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --follow flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -106,7 +107,7 @@ try { { console.log("Test 6: logs --tail flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs --tail 50 abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} logs --tail 50 abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --tail flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -117,7 +118,7 @@ try { { console.log("Test 7: logs with ID and --host flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo logs abc123 --host localhost:${port}`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} logs abc123 --host localhost:${port}`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --host flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -137,7 +138,7 @@ try { { console.log("Test 9: -q (quiet) flag is accepted with logs"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q logs abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q logs abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); diff --git a/packages/cli/tests/09-agent-inspect.test.ts b/packages/cli/tests/09-agent-inspect.test.ts index 0ae33343d7..9c449971bd 100644 --- a/packages/cli/tests/09-agent-inspect.test.ts +++ b/packages/cli/tests/09-agent-inspect.test.ts @@ -18,6 +18,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -27,8 +28,8 @@ $.verbose = false; console.log("=== Inspect Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -48,7 +49,7 @@ try { { console.log("Test 2: inspect requires id argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} inspect`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without id"); const output = result.stdout + result.stderr; // Commander should complain about missing argument @@ -64,7 +65,7 @@ try { { console.log("Test 3: inspect handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} inspect abc123`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -91,7 +92,7 @@ try { { console.log("Test 5: -q (quiet) flag is accepted with inspect"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q inspect abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q inspect abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -102,7 +103,7 @@ try { { console.log("Test 6: --json flag is accepted with inspect"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect abc123 --json`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} inspect abc123 --json`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --json flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -113,7 +114,7 @@ try { { console.log("Test 7: --format yaml flag is accepted with inspect"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo --format yaml inspect abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} --format yaml inspect abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --format yaml flag"); assert(!output.includes("error: option"), "should not have option parsing error"); diff --git a/packages/cli/tests/10-agent-mode.test.ts b/packages/cli/tests/10-agent-mode.test.ts index 0aa492ea4f..e44898c03e 100644 --- a/packages/cli/tests/10-agent-mode.test.ts +++ b/packages/cli/tests/10-agent-mode.test.ts @@ -17,6 +17,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -26,8 +27,8 @@ $.verbose = false; console.log("=== Agent Mode Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -47,7 +48,7 @@ try { { console.log("Test 2: agent mode requires id argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent mode`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without id"); const output = result.stdout + result.stderr; const hasError = @@ -63,7 +64,7 @@ try { { console.log("Test 3: agent mode handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode abc123 bypass`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent mode abc123 bypass`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -79,7 +80,7 @@ try { { console.log("Test 4: agent mode --list flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode --list abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent mode --list abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --list flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -90,7 +91,7 @@ try { { console.log("Test 5: agent mode with ID and --host flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode abc123 plan --host localhost:${port}`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent mode abc123 plan --host localhost:${port}`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --host flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -110,7 +111,7 @@ try { { console.log("Test 7: -q (quiet) flag is accepted with agent mode"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q agent mode abc123 bypass`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q agent mode abc123 bypass`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -121,7 +122,7 @@ try { { console.log("Test 8: agent mode requires mode argument when not using --list"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent mode abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent mode abc123`.nothrow(); // Should fail because mode is required unless --list is specified assert.notStrictEqual(result.exitCode, 0, "should fail without mode argument"); const output = result.stdout + result.stderr; diff --git a/packages/cli/tests/11-agent-archive.test.ts b/packages/cli/tests/11-agent-archive.test.ts index 7493395a2b..0685e2948b 100644 --- a/packages/cli/tests/11-agent-archive.test.ts +++ b/packages/cli/tests/11-agent-archive.test.ts @@ -17,6 +17,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -26,8 +27,8 @@ $.verbose = false; console.log("=== Agent Archive Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -46,7 +47,7 @@ try { { console.log("Test 2: agent archive requires ID argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent archive`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without id"); const output = result.stdout + result.stderr; const hasError = @@ -61,7 +62,7 @@ try { { console.log("Test 3: agent archive handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent archive abc123`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -77,7 +78,7 @@ try { { console.log("Test 4: agent archive --force flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive abc123 --force`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent archive abc123 --force`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --force flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -88,7 +89,7 @@ try { { console.log("Test 5: agent archive with ID and --host flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent archive abc123 --host localhost:${port}`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent archive abc123 --host localhost:${port}`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --host flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -108,7 +109,7 @@ try { { console.log("Test 7: -q (quiet) flag is accepted with agent archive"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q agent archive abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q agent archive abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); diff --git a/packages/cli/tests/11-agent-wait.test.ts b/packages/cli/tests/11-agent-wait.test.ts index 8984bc0c4e..198fa08910 100644 --- a/packages/cli/tests/11-agent-wait.test.ts +++ b/packages/cli/tests/11-agent-wait.test.ts @@ -19,6 +19,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -28,8 +29,8 @@ $.verbose = false; console.log("=== Wait Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -51,7 +52,7 @@ try { { console.log("Test 2: wait requires id argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} wait`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without id"); const output = result.stdout + result.stderr; // Commander should complain about missing argument @@ -67,7 +68,7 @@ try { { console.log("Test 3: wait handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} wait abc123`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -105,7 +106,7 @@ try { { console.log("Test 6: -q (quiet) flag is accepted with wait"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q wait abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q wait abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -116,7 +117,7 @@ try { { console.log("Test 7: --json flag is accepted with wait"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait abc123 --json`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} wait abc123 --json`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --json flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -127,7 +128,7 @@ try { { console.log("Test 8: --format yaml flag is accepted with wait"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo --format yaml wait abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} --format yaml wait abc123`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --format yaml flag"); assert(!output.includes("error: option"), "should not have option parsing error"); diff --git a/packages/cli/tests/12-permit-ls.test.ts b/packages/cli/tests/12-permit-ls.test.ts index c4549ae6a7..44b9a720af 100644 --- a/packages/cli/tests/12-permit-ls.test.ts +++ b/packages/cli/tests/12-permit-ls.test.ts @@ -17,6 +17,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -26,8 +27,8 @@ $.verbose = false; console.log("=== Permit LS Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -55,7 +56,7 @@ try { { console.log("Test 3: permit ls handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit ls`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit ls`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -71,7 +72,7 @@ try { { console.log("Test 4: permit ls --json handles errors"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit ls --json`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit ls --json`.nothrow(); // Should still fail (daemon not running) assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); // But output should be valid JSON if present @@ -93,7 +94,7 @@ try { { console.log("Test 5: -q (quiet) flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q permit ls`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q permit ls`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); diff --git a/packages/cli/tests/13-permit-allow-deny.test.ts b/packages/cli/tests/13-permit-allow-deny.test.ts index 51773da185..a44113726f 100644 --- a/packages/cli/tests/13-permit-allow-deny.test.ts +++ b/packages/cli/tests/13-permit-allow-deny.test.ts @@ -22,6 +22,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -31,8 +32,8 @@ $.verbose = false; console.log("=== Permit Allow/Deny Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -65,7 +66,7 @@ try { { console.log("Test 3: permit allow handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123 req456`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit allow abc123 req456`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -81,7 +82,7 @@ try { { console.log("Test 4: permit deny handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 req456`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit deny abc123 req456`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -97,7 +98,7 @@ try { { console.log("Test 5: permit allow --all flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123 --all`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit allow abc123 --all`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --all flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -108,7 +109,7 @@ try { { console.log("Test 6: permit deny --all flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 --all`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit deny abc123 --all`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --all flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -119,7 +120,7 @@ try { { console.log("Test 7: permit deny --message flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 req456 --message "Not allowed"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit deny abc123 req456 --message "Not allowed"`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --message flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -130,7 +131,7 @@ try { { console.log("Test 8: permit deny --interrupt flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123 req456 --interrupt`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit deny abc123 req456 --interrupt`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --interrupt flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -141,7 +142,7 @@ try { { console.log("Test 9: permit allow --input flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123 req456 --input '{"key":"value"}'`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit allow abc123 req456 --input '{"key":"value"}'`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --input flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -152,7 +153,7 @@ try { { console.log("Test 10: permit allow requires req_id or --all"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit allow abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit allow abc123`.nothrow(); // This might fail due to daemon not running first, or due to missing argument // The important thing is it doesn't crash with an unhandled error assert.notStrictEqual(result.exitCode, 0, "should fail without req_id or --all"); @@ -163,7 +164,7 @@ try { { console.log("Test 11: permit deny requires req_id or --all"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit deny abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} permit deny abc123`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without req_id or --all"); console.log("✓ permit deny requires req_id or --all\n"); } diff --git a/packages/cli/tests/14-worktree.test.ts b/packages/cli/tests/14-worktree.test.ts index d1b30a541d..e0c4d7f9b5 100644 --- a/packages/cli/tests/14-worktree.test.ts +++ b/packages/cli/tests/14-worktree.test.ts @@ -19,6 +19,7 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -28,8 +29,8 @@ $.verbose = false; console.log("=== Worktree Command Tests ===\n"); -// Get random port that's definitely not in use (never 6767) -const port = 10000 + Math.floor(Math.random() * 50000); +// Allocate an unused endpoint for connection-error and argument-validation checks. +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); try { @@ -56,7 +57,7 @@ try { { console.log("Test 3: worktree ls handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} worktree ls`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -72,7 +73,7 @@ try { { console.log("Test 4: worktree ls with --host flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls --host localhost:${port}`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} worktree ls --host localhost:${port}`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --host flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -93,7 +94,7 @@ try { { console.log("Test 6: worktree archive requires name argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree archive`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} worktree archive`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without name"); const output = result.stdout + result.stderr; const hasError = @@ -108,7 +109,7 @@ try { { console.log("Test 7: worktree archive handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree archive test-worktree`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} worktree archive test-worktree`.nothrow(); // Should fail because daemon not running assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; @@ -124,7 +125,7 @@ try { { console.log("Test 8: worktree archive with name and --host flag is accepted"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree archive test-worktree --host localhost:${port}`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} worktree archive test-worktree --host localhost:${port}`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --host flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -135,7 +136,7 @@ try { { console.log("Test 9: -q (quiet) flag is accepted with worktree ls"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q worktree ls`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q worktree ls`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -146,7 +147,7 @@ try { { console.log("Test 10: --json flag is accepted with worktree ls"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls --json`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} worktree ls --json`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --json flag"); assert(!output.includes("error: option"), "should not have option parsing error"); diff --git a/packages/cli/tests/16-agent-update.test.ts b/packages/cli/tests/16-agent-update.test.ts index 8e84ebf7b8..8ae54323bc 100644 --- a/packages/cli/tests/16-agent-update.test.ts +++ b/packages/cli/tests/16-agent-update.test.ts @@ -11,11 +11,11 @@ */ import assert from "node:assert"; +import { getAvailablePort } from "./helpers/network.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; -import { getAvailablePort } from "./helpers/network.ts"; $.verbose = false; @@ -43,7 +43,7 @@ try { { console.log("Test 2: agent update requires ID argument"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update --name "New Name"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent update --name "New Name"`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without id"); const output = result.stdout + result.stderr; const hasError = @@ -59,7 +59,7 @@ try { { console.log("Test 3: agent update requires update field"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent update abc123`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail without --name/--label"); const output = result.stdout + result.stderr; const hasError = @@ -74,7 +74,7 @@ try { { console.log("Test 4: agent update handles daemon not running"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --name "Renamed Agent"`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent update abc123 --name "Renamed Agent"`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running"); const output = result.stdout + result.stderr; const hasError = @@ -89,7 +89,7 @@ try { { console.log("Test 5: agent update accepts multi-label syntax"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --label surface=workspace,area=frontend --label priority=high`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent update abc123 --label surface=workspace,area=frontend --label priority=high`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept --label flag"); assert(!output.includes("error: option"), "should not have option parsing error"); @@ -109,7 +109,7 @@ try { { console.log("Test 7: agent update accepts thinking as an update field"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --thinking high`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent update abc123 --thinking high`.nothrow(); const output = result.stdout + result.stderr; assert(!output.includes("Nothing to update"), "should treat --thinking as an update field"); console.log("✓ agent update accepts thinking as an update field\n"); @@ -119,7 +119,7 @@ try { { console.log("Test 8: thinking cannot be combined with metadata updates"); const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo agent update abc123 --name renamed --thinking high`.nothrow(); + await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent update abc123 --name renamed --thinking high`.nothrow(); assert.notStrictEqual(result.exitCode, 0, "should reject a combined update"); const output = result.stdout + result.stderr; assert( diff --git a/packages/cli/tests/17-onboard.test.ts b/packages/cli/tests/17-onboard.test.ts index 3d8e641a26..a8b25148a2 100644 --- a/packages/cli/tests/17-onboard.test.ts +++ b/packages/cli/tests/17-onboard.test.ts @@ -5,6 +5,7 @@ import { readFile, mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { $ } from "zx"; +import { runLocalPaseo } from "./helpers/local-cli.ts"; import { getAvailablePort } from "./helpers/network.ts"; $.verbose = false; @@ -15,9 +16,19 @@ const paseoHome = await mkdtemp(join(tmpdir(), "paseo-onboard-home-")); const port = await getAvailablePort(); try { + const configured = await runLocalPaseo([ + "daemon", + "config", + "set", + "daemon.listen", + `127.0.0.1:${port}`, + "--home", + paseoHome, + ]); + assert.strictEqual(configured.exitCode, 0, configured.stderr); + console.log("Test 1: `paseo` runs blocking onboarding without implicit relay pairing"); - const onboard = - await $`PASEO_HOME=${paseoHome} PASEO_LISTEN=127.0.0.1:${port} PASEO_PAIRING_QR=0 npx paseo`.nothrow(); + const onboard = await $`PASEO_HOME=${paseoHome} PASEO_PAIRING_QR=0 npx paseo`.nothrow(); assert.strictEqual( onboard.exitCode, @@ -37,8 +48,8 @@ try { assert(onboard.stdout.includes("paseo --help"), "onboard output should include --help shortcut"); assert(onboard.stdout.includes("paseo ls"), "onboard output should include ls shortcut"); assert( - onboard.stdout.includes('paseo run "your prompt"'), - "onboard output should include run shortcut", + onboard.stdout.includes(`paseo run --home ${JSON.stringify(paseoHome)} "your prompt"`), + "onboard output should include a run shortcut for the selected home", ); assert(onboard.stdout.includes("paseo status"), "onboard output should include status shortcut"); assert( @@ -58,8 +69,7 @@ try { assert.strictEqual(enableRelay.exitCode, 0, `relay enable should succeed: ${enableRelay.stderr}`); assert(enableRelay.stdout.includes("#offer="), "relay enable should produce a pairing offer"); - const noRelayOnboard = - await $`PASEO_HOME=${paseoHome} PASEO_LISTEN=127.0.0.1:${port} npx paseo --no-relay`.nothrow(); + const noRelayOnboard = await $`PASEO_HOME=${paseoHome} npx paseo --no-relay`.nothrow(); assert.strictEqual( noRelayOnboard.exitCode, 0, diff --git a/packages/cli/tests/27-agent-delete.test.ts b/packages/cli/tests/27-agent-delete.test.ts index 64ab4007bf..bec523f9ca 100644 --- a/packages/cli/tests/27-agent-delete.test.ts +++ b/packages/cli/tests/27-agent-delete.test.ts @@ -11,27 +11,23 @@ */ import assert from "node:assert"; -import { $ } from "zx"; +import { runLocalPaseo } from "./helpers/local-cli.ts"; +import { getAvailablePort } from "./helpers/network.ts"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; - -$.verbose = false; +import { join } from "path"; console.log("=== Delete Command Tests ===\n"); -const cliRoot = dirname(fileURLToPath(import.meta.url)); -const repoRoot = join(cliRoot, "..", "..", ".."); -const port = 10000 + Math.floor(Math.random() * 50000); +const port = await getAvailablePort(); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-delete-test-home-")); async function runCli(args: string[]) { - return $`npm --prefix ${repoRoot} run cli -- ${args}`.nothrow(); + return runLocalPaseo(["--host", `localhost:${port}`, ...args], { PASEO_HOME: paseoHome }); } async function runDelete(args: string[]) { - return $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm --prefix ${repoRoot} run cli -- delete ${args}`.nothrow(); + return runCli(["delete", ...args]); } try { @@ -110,8 +106,7 @@ try { { console.log("Test 8: -q (quiet) flag is accepted with delete"); - const result = - await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npm --prefix ${repoRoot} run cli -- -q delete abc123`.nothrow(); + const result = await runCli(["-q", "delete", "abc123"]); const output = result.stdout + result.stderr; assert(!output.includes("unknown option"), "should accept -q flag"); assert(!output.includes("error: option"), "should not have option parsing error"); diff --git a/packages/cli/tests/34-daemon-status-auth.test.ts b/packages/cli/tests/34-daemon-status-auth.test.ts index 202c8eba12..ebbad314a2 100644 --- a/packages/cli/tests/34-daemon-status-auth.test.ts +++ b/packages/cli/tests/34-daemon-status-auth.test.ts @@ -1,6 +1,9 @@ #!/usr/bin/env npx tsx import assert from "node:assert"; +import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { runLocalPaseo } from "./helpers/local-cli.ts"; import { startTestDaemon } from "./helpers/test-daemon.ts"; @@ -70,4 +73,78 @@ try { await daemon.stop(); } +// POSIX executable probing executes --version; Windows resolves executables differently. +if (process.platform !== "win32") { + const root = await mkdtemp(join(tmpdir(), "paseo status slow provider ")); + const home = join(root, "daemon"); + const workDir = join(root, "work"); + const provider = join(root, "slow-provider"); + const marker = join(root, "availability.log"); + let slowDaemon: Awaited> | undefined; + try { + await mkdir(home); + await mkdir(workDir); + await writeFile( + provider, + `#!${process.execPath} +import('node:fs').then(({appendFileSync}) => { + appendFileSync(${JSON.stringify(marker)}, JSON.stringify(process.argv.slice(2)) + "\\n"); + setTimeout(() => console.log('provider 1.0.0'), 1800); +}); +`, + { mode: 0o700 }, + ); + await writeFile( + join(home, "config.json"), + JSON.stringify({ + version: 1, + agents: { providers: { claude: { command: { mode: "replace", argv: [provider] } } } }, + }), + ); + slowDaemon = await startTestDaemon({ + paseoHome: home, + workDir, + env: { PASEO_PASSWORD: "shared-secret" }, + }); + console.log("Test 4: local status separates authenticated reachability from slow details"); + const local = await runLocalPaseo(["daemon", "status", "--home", home, "--json"], { + PASEO_PASSWORD: "shared-secret", + }); + assert.strictEqual(local.exitCode, 0, local.stderr); + const status = JSON.parse(local.stdout); + assert.strictEqual(status.localDaemon, "running"); + assert.strictEqual(status.connectedDaemon, "reachable", JSON.stringify(status)); + assert.strictEqual(typeof status.serverId, "string"); + assert.match(status.note, /DAEMON_REQUEST_TIMEOUT/); + assert.match(status.note, /Status details unavailable/); + assert(!("workerPid" in status), "must not invent worker facts"); + assert(!("providers" in status), "must not invent provider facts"); + assert.match( + await readFile(marker, "utf8"), + /\["--version"\]/, + "the public provider executable was actually probed", + ); + console.log("✓ local authenticated connection remains reachable when details time out\n"); + + console.log("Test 5: explicit endpoint status remains an error when details time out"); + const remote = await runLocalPaseo( + ["daemon", "status", "--host", `127.0.0.1:${slowDaemon.port}`, "--json"], + { PASEO_PASSWORD: "shared-secret" }, + ); + assert.notStrictEqual(remote.exitCode, 0); + const { error } = JSON.parse(remote.stderr); + assert.strictEqual(error.code, "DAEMON_REQUEST_TIMEOUT"); + assert.match(error.message, /Status details unavailable/); + assert.strictEqual(error.details.connectedDaemon, "reachable"); + assert.strictEqual(error.details.serverId, status.serverId); + assert(!("home" in error.details), "endpoint observation must not invent local ownership"); + assert(!("workerPid" in error.details)); + assert(!("providers" in error.details)); + console.log("✓ failed remote query retains its error and authenticated connection fact\n"); + } finally { + await slowDaemon?.stop(); + await rm(root, { recursive: true, force: true }); + } +} + console.log("=== Daemon Status Auth Tests Passed ==="); diff --git a/packages/cli/tests/36-daemon-status-windows-process-lookup.test.ts b/packages/cli/tests/36-daemon-status-windows-process-lookup.test.ts index c5a0b92cde..f7872b310b 100644 --- a/packages/cli/tests/36-daemon-status-windows-process-lookup.test.ts +++ b/packages/cli/tests/36-daemon-status-windows-process-lookup.test.ts @@ -1,43 +1,12 @@ #!/usr/bin/env npx tsx import assert from "node:assert"; -import { spawn } from "node:child_process"; +import { runLocalPaseo } from "./helpers/local-cli.ts"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getAvailablePort } from "./helpers/network.ts"; -const CLI_ENTRY = join(import.meta.dirname, "..", "dist", "index.js"); - -interface CommandResult { - exitCode: number | null; - stdout: string; - stderr: string; -} - -function runLocalPaseo(args: string[], env: NodeJS.ProcessEnv): Promise { - return new Promise((resolve, reject) => { - const child = spawn(process.execPath, [CLI_ENTRY, ...args], { - env: { ...process.env, ...env }, - stdio: ["ignore", "pipe", "pipe"], - windowsHide: true, - }); - - let stdout = ""; - let stderr = ""; - child.stdout.on("data", (chunk: Buffer) => { - stdout += chunk.toString(); - }); - child.stderr.on("data", (chunk: Buffer) => { - stderr += chunk.toString(); - }); - child.on("error", reject); - child.on("close", (exitCode) => { - resolve({ exitCode, stdout, stderr }); - }); - }); -} - if (process.platform !== "win32") { console.log("Skipping Windows daemon status process lookup regression on non-Windows."); process.exit(0); diff --git a/packages/desktop/e2e/packaged-app-smoke.js b/packages/desktop/e2e/packaged-app-smoke.js index b07b75df4d..aedf3238cd 100644 --- a/packages/desktop/e2e/packaged-app-smoke.js +++ b/packages/desktop/e2e/packaged-app-smoke.js @@ -157,19 +157,15 @@ function getShellCommand(script) { } function createDefaultDaemonEnv(extraEnv) { - const env = { - ...process.env, + return { + ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("PASEO_"))), ...extraEnv, }; - - delete env.PASEO_HOME; - delete env.PASEO_LISTEN; - return env; } function createIsolatedDesktopEnv({ home, listen, userData, cdpPort }) { return { - ...process.env, + ...createDefaultDaemonEnv({ HOME: home, USERPROFILE: home }), PASEO_HOME: home, PASEO_LISTEN: listen, PASEO_ELECTRON_USER_DATA_DIR: userData, @@ -656,24 +652,15 @@ async function smokeColdCliDaemonStart({ appPath }) { const pidPath = path.join(home, "paseo.pid"); const port = await reserveLocalTcpPort(); const listen = `127.0.0.1:${port}`; - const env = createDefaultDaemonEnv(); + const env = createDefaultDaemonEnv({ HOME: home, USERPROFILE: home }); + configureIsolatedDaemonHome(home, listen); try { console.log("Packaged desktop smoke: cold-starting daemon through bundled CLI shim"); await runCliShimCommand({ appPath, env, - args: [ - "daemon", - "start", - "--home", - home, - "--listen", - listen, - "--no-relay", - "--no-mcp", - "--no-inject-mcp", - ], + args: ["daemon", "start", "--home", home], label: "Bundled CLI shim cold daemon start", }); diff --git a/packages/server/src/server/workspace-git-service.observation.test.ts b/packages/server/src/server/workspace-git-service.observation.test.ts index c55faa2bb5..4763b77a28 100644 --- a/packages/server/src/server/workspace-git-service.observation.test.ts +++ b/packages/server/src/server/workspace-git-service.observation.test.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { pathToFileURL } from "node:url"; import type pino from "pino"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import type { CheckoutSnapshotFacts, CheckoutStatusGit } from "../utils/checkout-git.js"; @@ -8,6 +9,8 @@ import { WorkspaceGitServiceImpl } from "./workspace-git-service.js"; const REPO_CWD = path.resolve("/tmp/paseo-observation-repo"); const GIT_DIR = path.join(REPO_CWD, ".git"); +// Checkout observation must not depend on installed forge CLIs or host-auth probes. +const REMOTE_URL = pathToFileURL(path.join(REPO_CWD, "remote.git")).href; const WORKTREE_A = path.resolve("/tmp/paseo-observation-worktree-a"); const WORKTREE_B = path.resolve("/tmp/paseo-observation-worktree-b"); @@ -500,7 +503,7 @@ describe("WorkspaceGitService checkout observation", () => { const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => ({ ...createCheckoutFacts(cwd), currentBranch: "feature", - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, resolvedBaseRef: "main", comparisonBaseRef: "origin/main", })); @@ -525,7 +528,7 @@ describe("WorkspaceGitService checkout observation", () => { currentBranch: "feature", baseRef: "main", hasRemote: true, - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, }), ), hasOriginRemote: vi.fn(async () => true), @@ -569,7 +572,7 @@ describe("WorkspaceGitService checkout observation", () => { const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => ({ ...createCheckoutFacts(cwd), currentBranch: "feature", - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, resolvedBaseRef: "main", comparisonBaseRef: "origin/main", })); @@ -626,7 +629,7 @@ describe("WorkspaceGitService checkout observation", () => { const releaseFetch = createDeferred(); const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => ({ ...createCheckoutFacts(cwd), - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, })); const runGitFetch = vi.fn(async (_cwd, observer) => { if (phase === "before") { @@ -681,7 +684,7 @@ describe("WorkspaceGitService checkout observation", () => { const releaseFetch = createDeferred(); const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => ({ ...createCheckoutFacts(cwd), - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, })); const getCheckoutRefDerivedState = vi.fn(); const service = createService(watcher, { @@ -731,7 +734,7 @@ describe("WorkspaceGitService checkout observation", () => { const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => ({ ...createCheckoutFacts(cwd), currentBranch: "feature", - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, resolvedBaseRef: "main", comparisonBaseRef: "origin/main", })); @@ -791,7 +794,7 @@ describe("WorkspaceGitService checkout observation", () => { const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => ({ ...createCheckoutFacts(cwd), currentBranch: "feature", - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, resolvedBaseRef: "main", comparisonBaseRef: "origin/main", })); @@ -820,7 +823,7 @@ describe("WorkspaceGitService checkout observation", () => { currentBranch: "feature", baseRef: "main", hasRemote: true, - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, }), ), hasOriginRemote: vi.fn(async () => true), @@ -864,7 +867,7 @@ describe("WorkspaceGitService checkout observation", () => { const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => ({ ...createCheckoutFacts(cwd), currentBranch: "feature", - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, resolvedBaseRef: "main", comparisonBaseRef: "origin/main", })); @@ -875,7 +878,7 @@ describe("WorkspaceGitService checkout observation", () => { currentBranch: "feature", baseRef: "main", hasRemote: true, - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, }), ), hasOriginRemote: vi.fn(async () => true), @@ -923,7 +926,7 @@ describe("WorkspaceGitService checkout observation", () => { const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => ({ ...createCheckoutFacts(cwd), currentBranch: "main", - remoteUrl: "https://example.com/repo.git", + remoteUrl: REMOTE_URL, resolvedBaseRef: "main", comparisonBaseRef: null, branchRemoteName: null, diff --git a/public-docs/cli.md b/public-docs/cli.md index 1e20cf4a7a..93b066e25b 100644 --- a/public-docs/cli.md +++ b/public-docs/cli.md @@ -293,7 +293,7 @@ paseo daemon stop --home ~/paseo-test `stop --home` waits for that local supervisor to exit. On POSIX it signals the supervisor without contacting a TCP endpoint. On Windows it uses the ready daemon's shutdown RPC; an unbound instance requires explicit `--force`. `--force` permits forced process-tree cleanup after the graceful timeout (default 15 seconds). `stop --host` only reports **shutdown requested**; remote process exit is not verified. A service manager may start another instance after the captured supervisor exits. -`status` separates local supervisor state, its published endpoint, the configured address, and RPC reachability. A stopped home is never probed at its configured address. An unbound live supervisor is **not ready**. +`status` separates local supervisor state, its published endpoint, the configured address, and RPC reachability. A stopped home is never probed at its configured address. An unbound live supervisor is **not ready**. If an authenticated local connection remains open but status details time out, the result stays `reachable` with a note explaining the unavailable details. Worker and provider fields are omitted. An explicit `--host` query still fails when its status request fails. `reload` validates the file, applies runtime-safe changes, and reports `appliedPaths`, `restartRequiredPaths`, and `overrideControlledPaths`. It never implicitly restarts. Use `--json` or `--format yaml` for structured results. An older host lacking the capability reports that it needs an update. diff --git a/public-docs/connectivity.md b/public-docs/connectivity.md index 425e605fd1..3263e61510 100644 --- a/public-docs/connectivity.md +++ b/public-docs/connectivity.md @@ -43,7 +43,7 @@ Set a different remote daemon port with `daemonPort`: paseo --host 'ssh://user@host?daemonPort=7777' ls -a ``` -Put `--host` before the command. `paseo daemon status` checks only the local daemon; use `paseo --host ssh://user@host ls` to verify a remote connection. `paseo --host ssh://user@host run --cwd /path/on/remote ...` requires a working directory that exists on the remote host. +Put `--host` before the command. `paseo daemon status` observes the default local home; use `paseo --host ssh://user@host daemon status` to query a remote daemon. `paseo --host ssh://user@host run --cwd /path/on/remote ...` requires a working directory that exists on the remote host. In Paseo Desktop, open **Settings → Add host → Remote SSH** and enter the same `ssh://` destination.