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 <hi@obviy.us>
This commit is contained in:
Tianworld
2026-04-15 00:11:47 +08:00
committed by GitHub
parent 60ea8e9a1c
commit 0bf3b84669
3 changed files with 66 additions and 3 deletions

View File

@@ -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

View File

@@ -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<unknown>) => {
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",

View File

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