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.
This commit is contained in:
Mohamed Boudra
2026-08-13 23:10:28 +02:00
committed by GitHub
parent b5610928a5
commit 599846650c
+174 -45
View File
@@ -16,9 +16,13 @@
*/ */
import assert from "node:assert"; 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 { tmpdir } from "os";
import { join } from "path"; import { dirname, join } from "path";
import { runLocalPaseo } from "./helpers/local-cli.ts"; import { runLocalPaseo } from "./helpers/local-cli.ts";
console.log("=== Daemon Commands ===\n"); 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. // Keep restart off default 6767 to avoid collisions with any existing daemon.
const port = 10000 + Math.floor(Math.random() * 50000); const port = 10000 + Math.floor(Math.random() * 50000);
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
const require = createRequire(import.meta.url);
function daemonCommand(args: string[]) { function daemonCommand(args: string[]) {
return runLocalPaseo(["daemon", ...args], { PASEO_HOME: paseoHome }); return runLocalPaseo(["daemon", ...args], { PASEO_HOME: paseoHome });
} }
async function stopChildProcess(child: ChildProcess): Promise<void> {
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<string> {
try {
const log = await readFile(join(paseoHome, "daemon.log"), "utf-8");
return log.split("\n").slice(-30).join("\n");
} catch {
return "<daemon log unavailable>";
}
}
async function retryWhileWorkerRuns<T>(
worker: ChildProcess,
probe: () => Promise<T>,
accepts: (result: T) => boolean,
failureDetails: (result: T) => string | Promise<string>,
): Promise<T> {
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 { try {
// Test 1: daemon --help shows subcommands // Test 1: daemon --help shows subcommands
{ {
@@ -132,56 +207,110 @@ try {
process.platform === "win32" process.platform === "win32"
? `\\\\.\\pipe\\paseo-status-${process.pid}-${Date.now()}` ? `\\\\.\\pipe\\paseo-status-${process.pid}-${Date.now()}`
: join(paseoHome, "status.sock"); : 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 configPath = join(paseoHome, "config.json");
const config = JSON.parse(await readFile(configPath, "utf-8")); 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"); await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
const pidPath = join(paseoHome, "paseo.pid"); // A supervised daemon owns and heartbeats paseo.pid. Launch the worker
const pidContents = await readFile(pidPath, "utf-8"); // directly so this fixture naturally has a reachable daemon without a PID file.
await unlink(pidPath); const workerEntry = resolveDaemonWorkerEntry();
const status = await daemonCommand(["status", "--json"]); const workerArgs = workerEntry.endsWith(".ts")
const pairing = await daemonCommand(["pair", "--json"]); ? ["--import", "tsx", workerEntry, "--relay"]
await writeFile(pidPath, pidContents, "utf-8"); : [workerEntry, "--relay"];
assert.strictEqual(status.exitCode, 0, `IPC daemon status should succeed: ${status.stderr}`); const worker = spawn(process.execPath, workerArgs, {
const payload = JSON.parse(status.stdout); cwd: join(import.meta.dirname, ".."),
assert.strictEqual(payload.connectedDaemon, "reachable", "IPC daemon should be reachable"); env: {
assert.strictEqual(payload.localDaemon, "stopped", "missing PID should report stopped locally"); ...process.env,
assert.notStrictEqual(payload.relay, "disabled", "status should use live relay state"); PASEO_HOME: paseoHome,
assert.strictEqual(pairing.exitCode, 0, `IPC daemon pairing should succeed: ${pairing.stderr}`); PASEO_LISTEN: listen,
const pairingPayload = JSON.parse(pairing.stdout); PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0",
assert.strictEqual(pairingPayload.relayEnabled, true, "pairing should use live relay state"); PASEO_DICTATION_ENABLED: "0",
assert.match(pairingPayload.url, /#offer=/, "pairing should use the live daemon offer"); PASEO_VOICE_MODE_ENABLED: "0",
CI: "true",
},
stdio: "ignore",
});
const foreignHome = await mkdtemp(join(tmpdir(), "paseo-test-foreign-home-"));
try { try {
await writeFile( const relayProbe = await retryWhileWorkerRuns(
join(foreignHome, "config.json"), worker,
`${JSON.stringify({ daemon: { listen } }, null, 2)}\n`, async () => {
"utf-8", 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( const { statusResult: status, statusPayload: payload, pairingResult: pairing } = relayProbe;
["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 cleanup = await daemonCommand(["stop", "--force"]); assert.strictEqual(status.exitCode, 0, `IPC daemon status should succeed: ${status.stderr}`);
assert.strictEqual(cleanup.exitCode, 0, "cleanup stop should succeed after IPC status"); 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"); console.log("✓ daemon status probes live relay state over local IPC\n");
} }