From e75ee909c9762dffa70308bdd8002a85e8c21c1f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 20 Aug 2026 07:27:32 +0000 Subject: [PATCH] test(daemon): stabilize restart diagnostics --- .../app/src/desktop/daemon/desktop-daemon.ts | 18 +++--- .../25-daemon-restart-supervisor.test.ts | 60 +++++++++++++++---- .../server/src/server/lifecycle-reasons.ts | 1 + .../daemon-self-update-session-controller.ts | 3 +- .../server/src/server/websocket-server.ts | 3 +- 5 files changed, 63 insertions(+), 22 deletions(-) diff --git a/packages/app/src/desktop/daemon/desktop-daemon.ts b/packages/app/src/desktop/daemon/desktop-daemon.ts index 04cad6cc17..51e202ecc1 100644 --- a/packages/app/src/desktop/daemon/desktop-daemon.ts +++ b/packages/app/src/desktop/daemon/desktop-daemon.ts @@ -3,14 +3,16 @@ import { invokeDesktopCommand } from "@/desktop/electron/invoke"; import type { AgentSkillSelection } from "@getpaseo/protocol/messages"; export type DesktopDaemonState = "starting" | "running" | "stopped" | "errored"; -export type DesktopDaemonStopReason = - | "manual_ipc" - | "settings" - | "host_remove" - | "quit" - | "app_update" - | "version_mismatch" - | "restart"; +export const DESKTOP_DAEMON_STOP_REASONS = [ + "manual_ipc", + "settings", + "host_remove", + "quit", + "app_update", + "version_mismatch", + "restart", +] as const; +export type DesktopDaemonStopReason = (typeof DESKTOP_DAEMON_STOP_REASONS)[number]; export interface DesktopDaemonStatus { serverId: string; diff --git a/packages/cli/tests/25-daemon-restart-supervisor.test.ts b/packages/cli/tests/25-daemon-restart-supervisor.test.ts index 10e559a2db..57d0e57649 100644 --- a/packages/cli/tests/25-daemon-restart-supervisor.test.ts +++ b/packages/cli/tests/25-daemon-restart-supervisor.test.ts @@ -45,7 +45,7 @@ function readWorkerPid(supervisorPid: number): number | null { return null; } - const result = spawnSync("ps", ["ax", "-o", "pid=,ppid="], { encoding: "utf8" }); + const result = spawnSync("ps", ["ax", "-o", "pid=,ppid=,stat="], { encoding: "utf8" }); if (result.status !== 0 || result.error) { return null; } @@ -55,12 +55,15 @@ function readWorkerPid(supervisorPid: number): number | null { if (!trimmed) { continue; } - const [pidToken, ppidToken] = trimmed.split(/\s+/); + const [pidToken, ppidToken, statToken] = trimmed.split(/\s+/); const pid = Number.parseInt(pidToken ?? "", 10); const ppid = Number.parseInt(ppidToken ?? "", 10); if (!Number.isInteger(pid) || !Number.isInteger(ppid)) { continue; } + if (statToken?.includes("Z")) { + continue; + } if (ppid === supervisorPid && pid > 0) { return pid; } @@ -197,16 +200,27 @@ try { await client?.close().catch(() => undefined); } - await waitFor( - () => { - const workerPid = readWorkerPid(supervisorPid); - return ( - workerPid !== null && workerPid !== workerPidBeforeRestart && isProcessRunning(workerPid) - ); - }, - 20000, - "worker pid did not change after restart request", - ); + try { + await waitFor( + () => { + const workerPid = readWorkerPid(supervisorPid); + return ( + workerPid !== null && workerPid !== workerPidBeforeRestart && isProcessRunning(workerPid) + ); + }, + 30000, + "worker pid did not change after restart request", + ); + } catch (error) { + const capturedSupervisorLogs = await readCapturedSupervisorLogs( + paseoHome, + recentSupervisorLogs, + ); + throw new Error( + `worker pid did not change after restart request, logs:\n${capturedSupervisorLogs}`, + { cause: error }, + ); + } const workerPidAfterRestart = readWorkerPid(supervisorPid); assert(workerPidAfterRestart !== null, "worker process should exist after restart"); @@ -216,6 +230,28 @@ try { "worker pid should change after restart", ); + try { + await waitFor( + async () => { + const status = await readDaemonStatus(paseoHome); + return status.localDaemon === "running" && status.pid === supervisorPid; + }, + 30000, + "daemon did not return to running after restart", + ); + } catch (error) { + const capturedSupervisorLogs = await readCapturedSupervisorLogs( + paseoHome, + recentSupervisorLogs, + ); + throw new Error( + `daemon did not return to running after restart, logs:\n${capturedSupervisorLogs}`, + { + cause: error, + }, + ); + } + const statusAfterRestart = await readDaemonStatus(paseoHome); assert.strictEqual( statusAfterRestart.localDaemon, diff --git a/packages/server/src/server/lifecycle-reasons.ts b/packages/server/src/server/lifecycle-reasons.ts index 08c620db22..624a0a1ef8 100644 --- a/packages/server/src/server/lifecycle-reasons.ts +++ b/packages/server/src/server/lifecycle-reasons.ts @@ -1,5 +1,6 @@ export const CLIENT_SHUTDOWN_RPC_REASON = "client_shutdown_rpc"; export const DEFAULT_CLIENT_RESTART_RPC_REASON = "client_restart_rpc"; +export const DAEMON_UPDATE_RPC_REASON = "daemon_update"; export function normalizeClientRestartRpcReason(reason: string | undefined): string { return reason?.trim() || DEFAULT_CLIENT_RESTART_RPC_REASON; diff --git a/packages/server/src/server/session/daemon/daemon-self-update-session-controller.ts b/packages/server/src/server/session/daemon/daemon-self-update-session-controller.ts index 461a97277b..27746a3d52 100644 --- a/packages/server/src/server/session/daemon/daemon-self-update-session-controller.ts +++ b/packages/server/src/server/session/daemon/daemon-self-update-session-controller.ts @@ -7,6 +7,7 @@ import { type DaemonSelfUpdater, } from "./daemon-self-updater.js"; import { getErrorMessage } from "@getpaseo/protocol/error-utils"; +import { DAEMON_UPDATE_RPC_REASON } from "../../lifecycle-reasons.js"; type DaemonUpdateRequest = Extract; @@ -87,7 +88,7 @@ export class DaemonSelfUpdateSessionController { type: "restart", clientId: this.clientId, requestId: msg.requestId, - reason: "daemon_update", + reason: DAEMON_UPDATE_RPC_REASON, }); } catch (error) { if (error instanceof DaemonSelfUpdateInProgressError) { diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index f59752a23e..d137a18952 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -87,6 +87,7 @@ import { ProviderUsageService } from "../services/quota-fetcher/service.js"; import { getProcessMemoryDiagnostics, getProcessUptimeSeconds } from "./process-diagnostics.js"; import { CLIENT_SHUTDOWN_RPC_REASON, + DAEMON_UPDATE_RPC_REASON, normalizeClientRestartRpcReason, } from "./lifecycle-reasons.js"; import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities"; @@ -2919,7 +2920,7 @@ function getControlRpcLogInfo( return { requestType: message.type, requestId: message.requestId, - reason: "daemon_update", + reason: DAEMON_UPDATE_RPC_REASON, }; } return null;