From 599846650cb412a1b3f835239f289ab8ead9f4f4 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 13 Aug 2026 23:10:28 +0200 Subject: [PATCH] Stabilize live relay status coverage (#3337) * test(cli): stabilize live relay status coverage The regression fixture deleted the supervisor's active PID lock to model a reachable daemon without local process state. Under CI load, worker readiness could race that deletion and make the supervisor tear down the daemon while status and pairing were probing it.\n\nLaunch the real worker directly so the no-PID state exists naturally, then wait on the CLI-visible live relay state before asserting behavior. * test(cli): wait for complete relay probe state Status readiness alone does not guarantee the next pairing RPC or the foreign-home identity probe will finish inside the CLI's per-call timeout under CI contention. Retry each exact observable outcome within the fixture deadline and resolve the real worker entry through the server package. * test(cli): name retry mechanics once Keep the relay scenarios expressed as CLI probes and exact outcomes while one file-local helper owns the shared deadline, worker-liveness check, and retry cadence. --- packages/cli/tests/03-daemon.test.ts | 219 +++++++++++++++++++++------ 1 file changed, 174 insertions(+), 45 deletions(-) diff --git a/packages/cli/tests/03-daemon.test.ts b/packages/cli/tests/03-daemon.test.ts index b5401fb5b..dcafbb8e8 100644 --- a/packages/cli/tests/03-daemon.test.ts +++ b/packages/cli/tests/03-daemon.test.ts @@ -16,9 +16,13 @@ */ import assert from "node:assert"; -import { mkdtemp, readFile, rm, unlink, writeFile } from "fs/promises"; +import { spawn, type ChildProcess } from "node:child_process"; +import { once } from "node:events"; +import { existsSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { mkdtemp, readFile, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; -import { join } from "path"; +import { dirname, join } from "path"; import { runLocalPaseo } from "./helpers/local-cli.ts"; console.log("=== Daemon Commands ===\n"); @@ -26,11 +30,82 @@ console.log("=== Daemon Commands ===\n"); // Keep restart off default 6767 to avoid collisions with any existing daemon. const port = 10000 + Math.floor(Math.random() * 50000); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); +const require = createRequire(import.meta.url); function daemonCommand(args: string[]) { return runLocalPaseo(["daemon", ...args], { PASEO_HOME: paseoHome }); } +async function stopChildProcess(child: ChildProcess): Promise { + if (child.exitCode !== null || child.signalCode !== null) return; + + const exited = once(child, "exit"); + child.kill("SIGTERM"); + const forceKill = setTimeout(() => child.kill("SIGKILL"), 5_000); + try { + await exited; + } finally { + clearTimeout(forceKill); + } +} + +function resolveDaemonWorkerEntry(): string { + let currentDir = dirname(require.resolve("@getpaseo/server")); + + while (true) { + const packageJsonPath = join(currentDir, "package.json"); + if (existsSync(packageJsonPath)) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); + if (packageJson.name === "@getpaseo/server") { + const candidates = [ + join(currentDir, "dist", "server", "server", "daemon-worker.js"), + join(currentDir, "src", "server", "daemon-worker.ts"), + ]; + const workerEntry = candidates.find(existsSync); + if (workerEntry) return workerEntry; + throw new Error(`Daemon worker not found under ${currentDir}`); + } + } + + const parentDir = dirname(currentDir); + if (parentDir === currentDir) break; + currentDir = parentDir; + } + + throw new Error("Unable to resolve @getpaseo/server package root"); +} + +async function tailDaemonLog(): Promise { + try { + const log = await readFile(join(paseoHome, "daemon.log"), "utf-8"); + return log.split("\n").slice(-30).join("\n"); + } catch { + return ""; + } +} + +async function retryWhileWorkerRuns( + worker: ChildProcess, + probe: () => Promise, + accepts: (result: T) => boolean, + failureDetails: (result: T) => string | Promise, +): Promise { + const deadline = Date.now() + 120_000; + let result = await probe(); + + while (!accepts(result)) { + assert( + worker.exitCode === null && worker.signalCode === null, + "IPC daemon exited before the probe completed", + ); + assert(Date.now() < deadline, await failureDetails(result)); + await new Promise((resolve) => setTimeout(resolve, 100)); + result = await probe(); + } + + return result; +} + try { // Test 1: daemon --help shows subcommands { @@ -132,56 +207,110 @@ try { process.platform === "win32" ? `\\\\.\\pipe\\paseo-status-${process.pid}-${Date.now()}` : join(paseoHome, "status.sock"); - const start = await daemonCommand(["start", "--listen", listen, "--relay"]); - assert.strictEqual(start.exitCode, 0, `IPC daemon should start: ${start.stderr}`); - const configPath = join(paseoHome, "config.json"); const config = JSON.parse(await readFile(configPath, "utf-8")); - config.daemon = { ...config.daemon, listen }; + config.daemon = { + ...config.daemon, + listen, + relay: { ...config.daemon?.relay, enabled: false }, + }; await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8"); - const pidPath = join(paseoHome, "paseo.pid"); - const pidContents = await readFile(pidPath, "utf-8"); - await unlink(pidPath); - const status = await daemonCommand(["status", "--json"]); - const pairing = await daemonCommand(["pair", "--json"]); - await writeFile(pidPath, pidContents, "utf-8"); - assert.strictEqual(status.exitCode, 0, `IPC daemon status should succeed: ${status.stderr}`); - const payload = JSON.parse(status.stdout); - assert.strictEqual(payload.connectedDaemon, "reachable", "IPC daemon should be reachable"); - assert.strictEqual(payload.localDaemon, "stopped", "missing PID should report stopped locally"); - assert.notStrictEqual(payload.relay, "disabled", "status should use live relay state"); - assert.strictEqual(pairing.exitCode, 0, `IPC daemon pairing should succeed: ${pairing.stderr}`); - const pairingPayload = JSON.parse(pairing.stdout); - assert.strictEqual(pairingPayload.relayEnabled, true, "pairing should use live relay state"); - assert.match(pairingPayload.url, /#offer=/, "pairing should use the live daemon offer"); + // A supervised daemon owns and heartbeats paseo.pid. Launch the worker + // directly so this fixture naturally has a reachable daemon without a PID file. + const workerEntry = resolveDaemonWorkerEntry(); + const workerArgs = workerEntry.endsWith(".ts") + ? ["--import", "tsx", workerEntry, "--relay"] + : [workerEntry, "--relay"]; + const worker = spawn(process.execPath, workerArgs, { + cwd: join(import.meta.dirname, ".."), + env: { + ...process.env, + PASEO_HOME: paseoHome, + PASEO_LISTEN: listen, + PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0", + PASEO_DICTATION_ENABLED: "0", + PASEO_VOICE_MODE_ENABLED: "0", + CI: "true", + }, + stdio: "ignore", + }); - const foreignHome = await mkdtemp(join(tmpdir(), "paseo-test-foreign-home-")); try { - await writeFile( - join(foreignHome, "config.json"), - `${JSON.stringify({ daemon: { listen } }, null, 2)}\n`, - "utf-8", + const relayProbe = await retryWhileWorkerRuns( + worker, + async () => { + const statusResult = await daemonCommand(["status", "--json"]); + const statusPayload = + statusResult.exitCode === 0 ? JSON.parse(statusResult.stdout) : null; + const pairingResult = await daemonCommand(["pair", "--json"]); + return { statusResult, statusPayload, pairingResult }; + }, + (result) => + result.statusPayload?.connectedDaemon === "reachable" && + result.statusPayload.relay !== "disabled" && + result.pairingResult.exitCode === 0, + async (result) => + [ + "IPC daemon did not expose live relay state", + `status: ${result.statusResult.stdout || result.statusResult.stderr}`, + `pairing: ${result.pairingResult.stdout || result.pairingResult.stderr}`, + `daemon log:\n${await tailDaemonLog()}`, + ].join("\n"), ); - const foreignPairing = await runLocalPaseo( - ["daemon", "pair", "--home", foreignHome, "--json"], - { PASEO_HOME: foreignHome }, - ); - assert.notStrictEqual( - foreignPairing.exitCode, - 0, - "pairing should reject a daemon owned by another home", - ); - assert( - foreignPairing.stderr.includes("different Paseo home"), - "pairing should explain the daemon identity mismatch", - ); - assert(!foreignPairing.stdout.includes("#offer="), "pairing should not expose another offer"); - } finally { - await rm(foreignHome, { recursive: true, force: true }); - } + const { statusResult: status, statusPayload: payload, pairingResult: pairing } = relayProbe; - const cleanup = await daemonCommand(["stop", "--force"]); - assert.strictEqual(cleanup.exitCode, 0, "cleanup stop should succeed after IPC status"); + assert.strictEqual(status.exitCode, 0, `IPC daemon status should succeed: ${status.stderr}`); + assert.strictEqual(payload.connectedDaemon, "reachable", "IPC daemon should be reachable"); + assert.strictEqual( + payload.localDaemon, + "stopped", + "missing PID should report stopped locally", + ); + assert.notStrictEqual(payload.relay, "disabled", "status should use live relay state"); + assert.strictEqual( + pairing.exitCode, + 0, + `IPC daemon pairing should succeed: ${pairing.stderr}`, + ); + const pairingPayload = JSON.parse(pairing.stdout); + assert.strictEqual(pairingPayload.relayEnabled, true, "pairing should use live relay state"); + assert.match(pairingPayload.url, /#offer=/, "pairing should use the live daemon offer"); + + const foreignHome = await mkdtemp(join(tmpdir(), "paseo-test-foreign-home-")); + try { + await writeFile( + join(foreignHome, "config.json"), + `${JSON.stringify({ daemon: { listen } }, null, 2)}\n`, + "utf-8", + ); + const foreignPairing = await retryWhileWorkerRuns( + worker, + () => + runLocalPaseo(["daemon", "pair", "--home", foreignHome, "--json"], { + PASEO_HOME: foreignHome, + }), + (result) => result.stderr.includes("different Paseo home"), + (result) => `Pairing did not report the daemon identity mismatch: ${result.stderr}`, + ); + assert.notStrictEqual( + foreignPairing.exitCode, + 0, + "pairing should reject a daemon owned by another home", + ); + assert( + foreignPairing.stderr.includes("different Paseo home"), + "pairing should explain the daemon identity mismatch", + ); + assert( + !foreignPairing.stdout.includes("#offer="), + "pairing should not expose another offer", + ); + } finally { + await rm(foreignHome, { recursive: true, force: true }); + } + } finally { + await stopChildProcess(worker); + } console.log("✓ daemon status probes live relay state over local IPC\n"); }