From b732f21a8688e922d26590e4ec073d5c6d155d35 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 20:24:31 +0100 Subject: [PATCH] fix: clarify voice-call setup diagnostics --- CHANGELOG.md | 4 +++ docs/plugins/voice-call.md | 6 ++++ extensions/voice-call/index.test.ts | 45 +++++++++++++++++++++++++++++ extensions/voice-call/index.ts | 6 ++++ 4 files changed, 61 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce5c66f9f54..486511107aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,10 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/Voice Call: treat missing provider credentials as setup-incomplete + during Gateway startup and log the missing keys as a warning instead of a + runtime startup error, while keeping explicit command/tool errors when used. Thanks + @steipete. - Tooling/check:changed: pass parent heavy-check lock markers to lint lanes so `pnpm check:changed` no longer waits on its own `lint:extensions` child. Thanks @steipete. diff --git a/docs/plugins/voice-call.md b/docs/plugins/voice-call.md index e0099108a01..70b17fe16da 100644 --- a/docs/plugins/voice-call.md +++ b/docs/plugins/voice-call.md @@ -53,6 +53,12 @@ Restart the Gateway afterwards. Set config under `plugins.entries.voice-call.config`: +If `enabled` is true but the selected provider is missing credentials, Gateway +startup logs a setup-incomplete warning with the missing keys and skips starting +the runtime. Run `openclaw voicecall setup` to see the same readiness details. +Commands, RPC calls, and agent tools still return the exact missing provider +configuration when used. + ```json5 { plugins: { diff --git a/extensions/voice-call/index.test.ts b/extensions/voice-call/index.test.ts index 23bd45cb347..fc80b581364 100644 --- a/extensions/voice-call/index.test.ts +++ b/extensions/voice-call/index.test.ts @@ -149,6 +149,7 @@ describe("voice-call plugin", () => { afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllEnvs(); delete (globalThis as Record)[Symbol.for("openclaw.voice-call.runtime")]; delete (globalThis as Record)[ Symbol.for("openclaw.voice-call.runtimePromise") @@ -197,6 +198,50 @@ describe("voice-call plugin", () => { expect(respond).toHaveBeenCalledWith(true, { callId: "call-2", initiated: true }); }); + it("does not log a startup error when provider setup is incomplete", async () => { + vi.stubEnv("TWILIO_ACCOUNT_SID", ""); + vi.stubEnv("TWILIO_AUTH_TOKEN", ""); + vi.stubEnv("TWILIO_FROM_NUMBER", ""); + const { service } = setup({ provider: "twilio" }); + + await service?.start(createServiceContext()); + + expect(createVoiceCallRuntime).not.toHaveBeenCalled(); + expect( + noopLogger.error.mock.calls.some(([message]) => + String(message).includes("Failed to start runtime"), + ), + ).toBe(false); + expect(noopLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("Runtime not started; setup incomplete"), + ); + expect(noopLogger.warn).toHaveBeenCalledWith(expect.stringContaining("TWILIO_ACCOUNT_SID")); + }); + + it("still reports missing provider setup when a command needs the runtime", async () => { + vi.stubEnv("TWILIO_ACCOUNT_SID", ""); + vi.stubEnv("TWILIO_AUTH_TOKEN", ""); + vi.stubEnv("TWILIO_FROM_NUMBER", ""); + const { methods } = setup({ provider: "twilio" }); + const handler = methods.get("voicecall.initiate") as + | ((ctx: { + params: Record; + respond: ReturnType; + }) => Promise) + | undefined; + const respond = vi.fn(); + + await handler?.({ params: { message: "Hi", to: "+15550001234" }, respond }); + + expect(createVoiceCallRuntime).not.toHaveBeenCalled(); + expect(respond).toHaveBeenCalledWith( + false, + expect.objectContaining({ + error: expect.stringContaining("TWILIO_ACCOUNT_SID"), + }), + ); + }); + it("initiates a call via voicecall.initiate", async () => { const { methods } = setup({ provider: "mock" }); const handler = methods.get("voicecall.initiate") as diff --git a/extensions/voice-call/index.ts b/extensions/voice-call/index.ts index b416fedbc82..120310a8af9 100644 --- a/extensions/voice-call/index.ts +++ b/extensions/voice-call/index.ts @@ -611,6 +611,12 @@ export default definePluginEntry({ if (!config.enabled) { return; } + if (!validation.valid) { + api.logger.warn( + `[voice-call] Runtime not started; setup incomplete: ${validation.errors.join("; ")}`, + ); + return; + } try { await ensureRuntime(); } catch (err) {