From 0bf3b8466914fc41816166a09d6c367b4f600077 Mon Sep 17 00:00:00 2001 From: Tianworld <40754565+Tianworld@users.noreply.github.com> Date: Wed, 15 Apr 2026 00:11:47 +0800 Subject: [PATCH] fix: avoid setup crash on missing provider ids (#66649) (thanks @Tianworld) * fix(wizard): avoid trim crash on missing provider ids Guard provider id comparisons in setup-mode model selection policy so setup does not crash when plugin provider metadata is missing an id. Fixes #66641 Fixes #66619 Made-with: Cursor * test: fix wizard provider-id regression coverage * fix: avoid setup crash on missing provider ids (#66649) (thanks @Tianworld) --------- Co-authored-by: Ayaan Zaidi --- CHANGELOG.md | 1 + src/wizard/setup.test.ts | 56 ++++++++++++++++++++++++++++++++++++++++ src/wizard/setup.ts | 12 ++++++--- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9609adceca2..f77ca9f3154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai - Video generation/live tests: bound provider polling for live video smoke, default to the fast non-FAL text-to-video path, and use a one-second lobster prompt so release validation no longer waits indefinitely on slow provider queues. - Memory-core/QMD `memory_get`: reject reads of arbitrary workspace markdown paths and only allow canonical memory files (`MEMORY.md`, `memory.md`, `DREAMS.md`, `dreams.md`, `memory/**`) plus exact paths of active indexed QMD workspace documents, so the QMD memory backend can no longer be used as a generic workspace-file read shim that bypasses `read` tool-policy denials. (#66026) Thanks @eleqtrizit. - Cron/agents: forward embedded-run tool policy and internal event params into the attempt layer so `--tools` allowlists, cron-owned message-tool suppression, explicit message targeting, and command-path internal events all take effect at runtime again. (#62675) Thanks @hexsprite. +- Setup/providers: guard preferred-provider lookup during setup so malformed plugin metadata with a missing provider id no longer crashes the wizard with `Cannot read properties of undefined (reading 'trim')`. (#66649) Thanks @Tianworld. ## 2026.4.14 diff --git a/src/wizard/setup.test.ts b/src/wizard/setup.test.ts index 3d14ddd8cc9..0f3899ce7c2 100644 --- a/src/wizard/setup.test.ts +++ b/src/wizard/setup.test.ts @@ -278,6 +278,62 @@ describe("runSetupWizard", () => { return dir; } + it("does not crash when preferred-provider lookup sees a provider without an id", async () => { + setupChannels.mockClear(); + readConfigFileSnapshot.mockResolvedValueOnce({ + path: "/tmp/.openclaw/openclaw.json", + exists: true, + raw: "{}", + parsed: {}, + resolved: {}, + valid: true, + config: {}, + issues: [], + warnings: [], + legacyIssues: [], + }); + resolvePreferredProviderForAuthChoice.mockResolvedValueOnce("demo-provider"); + resolvePluginProvidersRuntime.mockReturnValueOnce([ + { id: undefined } as unknown as { id: string }, + { id: "demo-provider", wizard: { setup: {} } } as unknown as { id: string }, + ]); + + const caseDir = await makeCaseDir("provider-missing-id-"); + const select = vi.fn(async ({ message }: WizardSelectParams) => { + if (message === "Select setup mode") return "quickstart"; + if (message === "Select channel (QuickStart)") return "__skip__"; + if (message === "How do you want to hatch your bot?") return "skip"; + return "skip"; + }) as unknown as WizardPrompter["select"]; + const confirm = vi.fn(async () => true) as unknown as WizardPrompter["confirm"]; + const prompter = buildWizardPrompter({ select, confirm }); + const runtime = createRuntime({ throwsOnExit: true }); + + await expect( + runSetupWizard( + { + acceptRisk: true, + flow: "quickstart", + authChoice: "ollama", + installDaemon: false, + skipProviders: false, + skipSkills: true, + skipSearch: true, + skipChannels: false, + skipUi: true, + workspace: caseDir, + }, + runtime, + prompter, + ), + ).resolves.toBeUndefined(); + expect(resolvePreferredProviderForAuthChoice).toHaveBeenCalledWith( + expect.objectContaining({ choice: "ollama" }), + ); + expect(resolvePluginProvidersRuntime).toHaveBeenCalled(); + setupChannels.mockClear(); + }); + it("exits when config is invalid", async () => { readConfigFileSnapshot.mockResolvedValueOnce({ path: "/tmp/.openclaw/openclaw.json", diff --git a/src/wizard/setup.ts b/src/wizard/setup.ts index e4698071beb..febaad42653 100644 --- a/src/wizard/setup.ts +++ b/src/wizard/setup.ts @@ -58,9 +58,15 @@ async function resolveAuthChoiceModelSelectionPolicy(params: { }); const matchedProvider = resolvedChoice?.provider ?? - (preferredProvider - ? providers.find((provider) => provider.id.trim() === preferredProvider.trim()) - : undefined); + (() => { + const preferredId = preferredProvider?.trim(); + if (!preferredId) { + return undefined; + } + return providers.find( + (provider) => typeof provider.id === "string" && provider.id.trim() === preferredId, + ); + })(); const setupPolicy = resolvedChoice?.wizard?.modelSelection ?? matchedProvider?.wizard?.setup?.modelSelection;