diff --git a/CHANGELOG.md b/CHANGELOG.md index 383426803e2..0e8812c32e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Onboarding/headless Linux daemon probe hardening: treat `systemctl --user is-enabled` probe failures as non-fatal during daemon install flow so onboarding no longer crashes on SSH/headless VPS environments before showing install guidance. (#37297) Thanks @acarbajal-web. - Memory/QMD mcporter Windows spawn hardening: when `mcporter.cmd` launch fails with `spawn EINVAL`, retry via bare `mcporter` shell resolution so QMD recall can continue instead of falling back to builtin memory search. (#27402) Thanks @i0ivi0i. - Tools/web_search Brave language-code validation: align `search_lang` handling with Brave-supported codes (including `zh-hans`, `zh-hant`, `en-gb`, and `pt-br`), map common alias inputs (`zh`, `ja`) to valid Brave values, and reject unsupported codes before upstream requests to prevent 422 failures. (#37260) Thanks @heyanming. - Models/openai-completions streaming compatibility: force `compat.supportsUsageInStreaming=false` for non-native OpenAI-compatible endpoints during model normalization, preventing usage-only stream chunks from triggering `choices[0]` parser crashes in provider streams. (#8714) Thanks @nonanon1. diff --git a/src/commands/configure.daemon.test.ts b/src/commands/configure.daemon.test.ts index 28c60273657..a5254a00cf9 100644 --- a/src/commands/configure.daemon.test.ts +++ b/src/commands/configure.daemon.test.ts @@ -5,6 +5,7 @@ const loadConfig = vi.hoisted(() => vi.fn()); const resolveGatewayInstallToken = vi.hoisted(() => vi.fn()); const buildGatewayInstallPlan = vi.hoisted(() => vi.fn()); const note = vi.hoisted(() => vi.fn()); +const serviceIsLoaded = vi.hoisted(() => vi.fn(async () => false)); const serviceInstall = vi.hoisted(() => vi.fn(async () => {})); const ensureSystemdUserLingerInteractive = vi.hoisted(() => vi.fn(async () => {})); @@ -41,7 +42,7 @@ vi.mock("./daemon-runtime.js", () => ({ vi.mock("../daemon/service.js", () => ({ resolveGatewayService: vi.fn(() => ({ - isLoaded: vi.fn(async () => false), + isLoaded: serviceIsLoaded, install: serviceInstall, })), })); @@ -59,6 +60,8 @@ const { maybeInstallDaemon } = await import("./configure.daemon.js"); describe("maybeInstallDaemon", () => { beforeEach(() => { vi.clearAllMocks(); + serviceIsLoaded.mockResolvedValue(false); + serviceInstall.mockResolvedValue(undefined); loadConfig.mockReturnValue({}); resolveGatewayInstallToken.mockResolvedValue({ token: undefined, @@ -107,4 +110,19 @@ describe("maybeInstallDaemon", () => { expect(buildGatewayInstallPlan).not.toHaveBeenCalled(); expect(serviceInstall).not.toHaveBeenCalled(); }); + + it("continues daemon install flow when service status probe throws", async () => { + serviceIsLoaded.mockRejectedValueOnce( + new Error("systemctl is-enabled unavailable: Failed to connect to bus"), + ); + + await expect( + maybeInstallDaemon({ + runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() }, + port: 18789, + }), + ).resolves.toBeUndefined(); + + expect(serviceInstall).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/commands/configure.daemon.ts b/src/commands/configure.daemon.ts index f282cfc850e..2be58f19a64 100644 --- a/src/commands/configure.daemon.ts +++ b/src/commands/configure.daemon.ts @@ -20,7 +20,12 @@ export async function maybeInstallDaemon(params: { daemonRuntime?: GatewayDaemonRuntime; }) { const service = resolveGatewayService(); - const loaded = await service.isLoaded({ env: process.env }); + let loaded = false; + try { + loaded = await service.isLoaded({ env: process.env }); + } catch { + loaded = false; + } let shouldCheckLinger = false; let shouldInstall = true; let daemonRuntime = params.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;