From 298cae67bb98ebddd1307181bc02f6c6b2693985 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Wed, 6 May 2026 15:23:34 +0100 Subject: [PATCH] fix(mattermost): collect setup URL in wizard Fixes #76670.\n\nSummary:\n- Collect the Mattermost bot token and server URL as separate wizard patches so validation does not run before the URL is entered.\n- Preserve non-interactive Mattermost setup validation for explicit --bot-token + --http-url flows.\n- Add a regression test and changelog entry.\n\nVerification:\n- Reporter manually verified setup against a real Mattermost server.\n- pnpm test extensions/mattermost\n- pnpm tsgo:extensions\n- pnpm tsgo:extensions:test\n- pnpm exec oxfmt --check --threads=1 extensions/mattermost/src/setup-core.ts extensions/mattermost/src/setup-surface.ts extensions/mattermost/src/setup.test.ts\n- git diff --check upstream/main...HEAD --- CHANGELOG.md | 1 + extensions/mattermost/src/setup-core.ts | 42 ++++++++++++++-------- extensions/mattermost/src/setup-surface.ts | 18 +++++++++- extensions/mattermost/src/setup.test.ts | 40 +++++++++++++++++++++ 4 files changed, 86 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5384ab1fa3d..e08ca2f9efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ Docs: https://docs.openclaw.ai - Telegram/Codex: generate DM topic labels with Codex-compatible simple-completion requests so auto-created private topics can be renamed instead of staying `New Chat`. - Web fetch: bound guarded dispatcher cleanup after request timeouts so timed-out fetches return tool errors instead of leaving Gateway tool lanes active. (#78439) Thanks @obviyus. +- Mattermost/setup: prompt for and persist the server base URL after the bot token in `openclaw setup --wizard`, instead of failing validation before `--http-url` is collected. Fixes #76670. Thanks @jacobtomlinson. - Gate Slack startup user allowlist resolution [AI]. (#77898) Thanks @pgondhi987. - OpenAI/Codex: suppress stale `openai-codex` GPT-5.1/5.2/5.3 model refs that ChatGPT/Codex OAuth accounts now reject, keeping model lists, config validation, and forward-compat resolution on current 5.4/5.5 routes. Fixes #67158. Thanks @drpau. - CLI/update: keep pnpm package updates on the running custom global install root and pass pnpm's `--global-dir` so `openclaw update` does not create a second default-prefix install when `OPENCLAW_HOME` or the shell points at a custom OpenClaw directory. Fixes #78377. Thanks @amknight. diff --git a/extensions/mattermost/src/setup-core.ts b/extensions/mattermost/src/setup-core.ts index 06ed55b35a8..13f8bb845da 100644 --- a/extensions/mattermost/src/setup-core.ts +++ b/extensions/mattermost/src/setup-core.ts @@ -30,6 +30,33 @@ export function resolveMattermostAccountWithSecrets(cfg: OpenClawConfig, account }); } +export function applyMattermostSetupConfigPatch(params: { + cfg: OpenClawConfig; + accountId: string; + name?: string; + patch: Record; +}): OpenClawConfig { + const namedConfig = applyAccountNameToChannelSection({ + cfg: params.cfg, + channelKey: channel, + accountId: params.accountId, + name: params.name, + }); + const next = + params.accountId !== DEFAULT_ACCOUNT_ID + ? migrateBaseNameToDefaultAccount({ + cfg: namedConfig, + channelKey: channel, + }) + : namedConfig; + return applySetupAccountConfigPatch({ + cfg: next, + channelKey: channel, + accountId: params.accountId, + patch: params.patch, + }); +} + export const mattermostSetupAdapter: ChannelSetupAdapter = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => @@ -66,23 +93,10 @@ export const mattermostSetupAdapter: ChannelSetupAdapter = { applyAccountConfig: ({ cfg, accountId, input }) => { const token = input.botToken ?? input.token; const baseUrl = normalizeMattermostBaseUrl(input.httpUrl); - const namedConfig = applyAccountNameToChannelSection({ + return applyMattermostSetupConfigPatch({ cfg, - channelKey: channel, accountId, name: input.name, - }); - const next = - accountId !== DEFAULT_ACCOUNT_ID - ? migrateBaseNameToDefaultAccount({ - cfg: namedConfig, - channelKey: channel, - }) - : namedConfig; - return applySetupAccountConfigPatch({ - cfg: next, - channelKey: channel, - accountId, patch: input.useEnv ? {} : { diff --git a/extensions/mattermost/src/setup-surface.ts b/extensions/mattermost/src/setup-surface.ts index 4194228ca3d..b27f2c19547 100644 --- a/extensions/mattermost/src/setup-surface.ts +++ b/extensions/mattermost/src/setup-surface.ts @@ -6,7 +6,11 @@ import { formatDocsLink, type ChannelSetupWizard, } from "openclaw/plugin-sdk/setup"; -import { isMattermostConfigured, resolveMattermostAccountWithSecrets } from "./setup-core.js"; +import { + applyMattermostSetupConfigPatch, + isMattermostConfigured, + resolveMattermostAccountWithSecrets, +} from "./setup-core.js"; import { normalizeMattermostBaseUrl } from "./setup.client.runtime.js"; import { hasConfiguredSecretInput } from "./setup.secret-input.runtime.js"; @@ -81,6 +85,12 @@ export const mattermostSetupWizard: ChannelSetupWizard = { hasConfiguredValue: hasConfiguredSecretInput(resolvedAccount.config.botToken), }; }, + applySet: async ({ cfg, accountId, value }) => + applyMattermostSetupConfigPatch({ + cfg, + accountId, + patch: { botToken: value }, + }), }, ], textInputs: [ @@ -106,6 +116,12 @@ export const mattermostSetupWizard: ChannelSetupWizard = { ? undefined : "Mattermost base URL must include a valid base URL.", normalizeValue: ({ value }) => normalizeMattermostBaseUrl(value) ?? value.trim(), + applySet: async ({ cfg, accountId, value }) => + applyMattermostSetupConfigPatch({ + cfg, + accountId, + patch: { baseUrl: value }, + }), }, ], disable: (cfg: OpenClawConfig) => ({ diff --git a/extensions/mattermost/src/setup.test.ts b/extensions/mattermost/src/setup.test.ts index c013af7e9c4..f1acb93b7f1 100644 --- a/extensions/mattermost/src/setup.test.ts +++ b/extensions/mattermost/src/setup.test.ts @@ -1,4 +1,9 @@ import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api"; +import { + createSetupWizardAdapter, + createQueuedWizardPrompter, + runSetupWizardConfigure, +} from "openclaw/plugin-sdk/plugin-test-runtime"; import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig, OpenClawPluginApi } from "../runtime-api.js"; @@ -352,6 +357,41 @@ describe("mattermost setup", () => { }, }); }); + + it("prompts for bot token and server URL before validating wizard setup", async () => { + normalizeMattermostBaseUrl.mockImplementation((value: string | undefined) => + value?.startsWith("http") ? value : undefined, + ); + const queued = createQueuedWizardPrompter({ + textValues: ["bot-token", "https://chat.example.com"], + }); + const adapter = createSetupWizardAdapter({ + plugin: { + id: "mattermost", + meta: { label: "Mattermost" }, + config: { + listAccountIds: () => [DEFAULT_ACCOUNT_ID], + }, + setup: mattermostSetupAdapter, + } as never, + wizard: mattermostSetupWizard, + }); + + const result = await runSetupWizardConfigure({ + configure: adapter.configure, + cfg: { channels: { mattermost: {} } } as OpenClawConfig, + prompter: queued.prompter, + options: { secretInputMode: "plaintext" as const }, + }); + + const textMessages = queued.text.mock.calls.map( + ([params]) => (params as { message: string }).message, + ); + expect(textMessages).toEqual(["Enter Mattermost bot token", "Enter Mattermost base URL"]); + expect(result.cfg.channels?.mattermost?.botToken).toBe("bot-token"); + expect(result.cfg.channels?.mattermost?.baseUrl).toBe("https://chat.example.com"); + expect(result.accountId).toBe(DEFAULT_ACCOUNT_ID); + }); }); function registerEnvDefaults() {