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
This commit is contained in:
Jacob Tomlinson
2026-05-06 15:23:34 +01:00
committed by GitHub
parent fd5352bc18
commit 298cae67bb
4 changed files with 86 additions and 15 deletions

View File

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

View File

@@ -30,6 +30,33 @@ export function resolveMattermostAccountWithSecrets(cfg: OpenClawConfig, account
});
}
export function applyMattermostSetupConfigPatch(params: {
cfg: OpenClawConfig;
accountId: string;
name?: string;
patch: Record<string, unknown>;
}): 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
? {}
: {

View File

@@ -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) => ({

View File

@@ -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() {