From 1521ceb381f8c96d4427c73893c71105568c26a6 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 4 May 2026 01:40:47 +0700 Subject: [PATCH] Surface desktop daemon gate failures instead of silent no-op The async settings gate added in f0d96f8e called shouldStartDaemon() inside Promise.resolve(...).then(...) with no catch. If loadDesktopSettings() rejects (corrupted JSON, FS error), the daemon never starts, no error renders on the splash, and the retry button repeats the same silent path. Add a recordError method to DaemonStartService and an onGateError callback to startDaemonIfGateAllows / startHostRuntimeBootstrap. _layout.tsx wires it through so a gate rejection populates lastError, the splash shows the failure, and retry has signal. --- packages/app/src/app/_layout.tsx | 5 +++- .../src/app/host-runtime-bootstrap.test.ts | 21 +++++++++++++++ .../app/src/app/host-runtime-bootstrap.ts | 26 +++++++++++++------ .../src/runtime/daemon-start-service.test.ts | 15 +++++++++++ .../app/src/runtime/daemon-start-service.ts | 4 +++ 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 00342cbdb..d82627d0a 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -329,6 +329,7 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) { store, daemonStartService, shouldStartDaemon: shouldStartBuiltInDaemon, + onGateError: (message) => daemonStartService.recordError(message), }); }, []); @@ -355,9 +356,11 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) { }, [anyOnlineHostServerId, daemonStartError, daemonStartIsRunning, hasGivenUpWaitingForHost]); const retry = useCallback(() => { + const daemonStartService = getDaemonStartService({ store: getHostRuntimeStore() }); startDaemonIfGateAllows({ - daemonStartService: getDaemonStartService({ store: getHostRuntimeStore() }), + daemonStartService, shouldStartDaemon: shouldStartBuiltInDaemon, + onGateError: (message) => daemonStartService.recordError(message), }); }, []); diff --git a/packages/app/src/app/host-runtime-bootstrap.test.ts b/packages/app/src/app/host-runtime-bootstrap.test.ts index d12dd1939..e3968ef37 100644 --- a/packages/app/src/app/host-runtime-bootstrap.test.ts +++ b/packages/app/src/app/host-runtime-bootstrap.test.ts @@ -70,6 +70,27 @@ describe("startHostRuntimeBootstrap", () => { expect(daemonStartService.start).not.toHaveBeenCalled(); }); + it("surfaces gate rejection to onGateError without starting the daemon", async () => { + const store = createFakeStore(); + const daemonStartService = createFakeDaemonStartService(); + const onGateError = vi.fn(); + + startHostRuntimeBootstrap({ + store, + daemonStartService, + shouldStartDaemon: async () => { + throw new Error("settings file unreadable"); + }, + onGateError, + }); + await vi.waitFor(() => { + expect(onGateError).toHaveBeenCalledTimes(1); + }); + + expect(daemonStartService.start).not.toHaveBeenCalled(); + expect(onGateError).toHaveBeenCalledWith(expect.stringContaining("settings file unreadable")); + }); + it("does not await the daemon-start promise", () => { const store = createFakeStore(); let resolveStart: ((value: { ok: true }) => void) | undefined; diff --git a/packages/app/src/app/host-runtime-bootstrap.ts b/packages/app/src/app/host-runtime-bootstrap.ts index f8de3593f..b75cd01fe 100644 --- a/packages/app/src/app/host-runtime-bootstrap.ts +++ b/packages/app/src/app/host-runtime-bootstrap.ts @@ -17,6 +17,7 @@ export interface StartHostRuntimeBootstrapInput { store: HostRuntimeBootstrapStore; daemonStartService: HostRuntimeBootstrapDaemonStartService; shouldStartDaemon: HostRuntimeBootstrapStartGate; + onGateError?: (message: string) => void; } export function startHostRuntimeBootstrap(input: StartHostRuntimeBootstrapInput): void { @@ -24,26 +25,35 @@ export function startHostRuntimeBootstrap(input: StartHostRuntimeBootstrapInput) startDaemonIfGateAllows({ daemonStartService: input.daemonStartService, shouldStartDaemon: input.shouldStartDaemon, + onGateError: input.onGateError, }); } export function startDaemonIfGateAllows(input: { daemonStartService: HostRuntimeBootstrapDaemonStartService; shouldStartDaemon: HostRuntimeBootstrapStartGate; + onGateError?: (message: string) => void; }): void { - if (typeof input.shouldStartDaemon === "boolean") { - if (input.shouldStartDaemon) { + const gate = input.shouldStartDaemon; + if (typeof gate === "boolean") { + if (gate) { void input.daemonStartService.start(); } return; } - void Promise.resolve(input.shouldStartDaemon()).then((shouldStartDaemon) => { - if (shouldStartDaemon) { - void input.daemonStartService.start(); - } - return; - }); + void Promise.resolve() + .then(() => gate()) + .then((shouldStartDaemon) => { + if (shouldStartDaemon) { + void input.daemonStartService.start(); + } + return null; + }) + .catch((error) => { + const message = error instanceof Error ? error.message : String(error); + input.onGateError?.(`Failed to evaluate desktop daemon settings: ${message}`); + }); } export const WELCOME_ROUTE: Href = "/welcome"; diff --git a/packages/app/src/runtime/daemon-start-service.test.ts b/packages/app/src/runtime/daemon-start-service.test.ts index fc642b639..b01ccd841 100644 --- a/packages/app/src/runtime/daemon-start-service.test.ts +++ b/packages/app/src/runtime/daemon-start-service.test.ts @@ -188,6 +188,21 @@ describe("DaemonStartService", () => { expect(service.getLastError()).toBeNull(); }); + it("recordError surfaces an external error and notifies subscribers", () => { + const fake = createFakeStore(); + const service = new DaemonStartService({ + store: fake.store, + startDesktopDaemon: async () => makeStatus(), + }); + const notifications = vi.fn(); + service.subscribe(notifications); + + service.recordError("settings file unreadable"); + + expect(service.getLastError()).toBe("settings file unreadable"); + expect(notifications).toHaveBeenCalledTimes(1); + }); + it("stops notifying after a subscriber unsubscribes", async () => { const fake = createFakeStore(); let notifications = 0; diff --git a/packages/app/src/runtime/daemon-start-service.ts b/packages/app/src/runtime/daemon-start-service.ts index b11fbb0b4..e9437931f 100644 --- a/packages/app/src/runtime/daemon-start-service.ts +++ b/packages/app/src/runtime/daemon-start-service.ts @@ -53,6 +53,10 @@ export class DaemonStartService { return this.lastError; } + recordError(message: string): void { + this.setLastError(message); + } + isRunning(): boolean { return this.inFlightCount > 0; }