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; }