fix: thread cfg through mattermost fallback sends

This commit is contained in:
Muhammed Mukhthar CM 2026-03-17 05:40:13 +00:00 committed by mukhtharcm
parent 21260d03e9
commit 7ca468e365
5 changed files with 202 additions and 2 deletions

View File

@ -549,6 +549,7 @@ Docs: https://docs.openclaw.ai
- Agents/edit tool: accept common path/text alias spellings, show current file contents on exact-match failures, and avoid false edit failures after successful writes. (#52516) thanks @mbelinky.
- Agents/compaction: reconcile `sessions.json.compactionCount` after a late embedded auto-compaction success so persisted session counts catch up once the handler reports completion. (#45493) Thanks @jackal092927.
- Mattermost/replies: keep pairing replies, slash-command fallback replies, and model-picker messages on the resolved config path so `exec:` SecretRef bot tokens work across all outbound reply branches. (#48347) thanks @mathiasnagler.
## 2026.3.13

View File

@ -1086,7 +1086,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
idLine: `Your Mattermost user id: ${senderId}`,
code,
}),
{ accountId: account.accountId },
{ cfg, accountId: account.accountId },
);
opts.statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {

View File

@ -45,6 +45,7 @@ describe("deliverMattermostReplyPayload", () => {
"channel:town-square",
"caption",
expect.objectContaining({
cfg,
accountId: "default",
mediaUrl,
replyToId: "root-post",
@ -63,6 +64,7 @@ describe("deliverMattermostReplyPayload", () => {
it("forwards replyToId for text-only chunked replies", async () => {
const sendMessage = vi.fn(async () => undefined);
const cfg = {} satisfies OpenClawConfig;
const core = {
channel: {
text: {
@ -75,7 +77,7 @@ describe("deliverMattermostReplyPayload", () => {
await deliverMattermostReplyPayload({
core,
cfg: {} satisfies OpenClawConfig,
cfg,
payload: { text: "hello" },
to: "channel:town-square",
accountId: "default",
@ -91,6 +93,7 @@ describe("deliverMattermostReplyPayload", () => {
"channel:town-square",
"hello",
expect.objectContaining({
cfg,
accountId: "default",
replyToId: "root-post",
}),

View File

@ -0,0 +1,193 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { PassThrough } from "node:stream";
import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/mattermost";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ResolvedMattermostAccount } from "./accounts.js";
const mockState = vi.hoisted(() => ({
readRequestBodyWithLimit: vi.fn(async () => "token=valid-token"),
parseSlashCommandPayload: vi.fn(() => ({
token: "valid-token",
command: "/oc_models",
text: "models",
channel_id: "chan-1",
user_id: "user-1",
user_name: "alice",
team_id: "team-1",
})),
resolveCommandText: vi.fn((_trigger: string, text: string) => text),
buildModelsProviderData: vi.fn(async () => ({ providers: [] })),
resolveMattermostModelPickerEntry: vi.fn(() => ({ kind: "summary" })),
authorizeMattermostCommandInvocation: vi.fn(() => ({
ok: true,
commandAuthorized: true,
channelInfo: { id: "chan-1", type: "O", name: "town-square", display_name: "Town Square" },
kind: "channel",
chatType: "channel",
channelName: "town-square",
channelDisplay: "Town Square",
roomLabel: "#town-square",
})),
createMattermostClient: vi.fn(() => ({})),
fetchMattermostChannel: vi.fn(async () => ({
id: "chan-1",
type: "O",
name: "town-square",
display_name: "Town Square",
})),
sendMessageMattermost: vi.fn(async () => ({ messageId: "post-1", channelId: "chan-1" })),
normalizeMattermostAllowList: vi.fn((value: unknown) => value),
}));
vi.mock("openclaw/plugin-sdk/mattermost", () => ({
buildModelsProviderData: mockState.buildModelsProviderData,
createReplyPrefixOptions: vi.fn(() => ({})),
createTypingCallbacks: vi.fn(() => ({ onReplyStart: vi.fn() })),
isRequestBodyLimitError: vi.fn(() => false),
logTypingFailure: vi.fn(),
readRequestBodyWithLimit: mockState.readRequestBodyWithLimit,
}));
vi.mock("../runtime.js", () => ({
getMattermostRuntime: () => ({
channel: {
commands: {
shouldHandleTextCommands: () => true,
},
text: {
hasControlCommand: () => false,
},
pairing: {
readAllowFromStore: vi.fn(async () => []),
},
routing: {
resolveAgentRoute: vi.fn(() => ({
agentId: "agent-1",
sessionKey: "mattermost:session:1",
accountId: "default",
})),
},
},
}),
}));
vi.mock("./client.js", () => ({
createMattermostClient: mockState.createMattermostClient,
fetchMattermostChannel: mockState.fetchMattermostChannel,
normalizeMattermostBaseUrl: vi.fn((value: string | undefined) => value?.trim() ?? ""),
sendMattermostTyping: vi.fn(),
}));
vi.mock("./model-picker.js", () => ({
renderMattermostModelSummaryView: vi.fn(),
renderMattermostModelsPickerView: vi.fn(),
renderMattermostProviderPickerView: vi.fn(),
resolveMattermostModelPickerCurrentModel: vi.fn(),
resolveMattermostModelPickerEntry: mockState.resolveMattermostModelPickerEntry,
}));
vi.mock("./monitor-auth.js", () => ({
authorizeMattermostCommandInvocation: mockState.authorizeMattermostCommandInvocation,
normalizeMattermostAllowList: mockState.normalizeMattermostAllowList,
}));
vi.mock("./reply-delivery.js", () => ({
deliverMattermostReplyPayload: vi.fn(),
}));
vi.mock("./send.js", () => ({
sendMessageMattermost: mockState.sendMessageMattermost,
}));
vi.mock("./slash-commands.js", () => ({
parseSlashCommandPayload: mockState.parseSlashCommandPayload,
resolveCommandText: mockState.resolveCommandText,
}));
import { createSlashCommandHttpHandler } from "./slash-http.js";
function createRequest(body = "token=valid-token"): IncomingMessage {
const req = new PassThrough();
const incoming = req as unknown as IncomingMessage;
incoming.method = "POST";
incoming.headers = {
"content-type": "application/x-www-form-urlencoded",
};
process.nextTick(() => {
req.end(body);
});
return incoming;
}
function createResponse(): {
res: ServerResponse;
getBody: () => string;
} {
let body = "";
const res = {
statusCode: 200,
setHeader() {},
end(chunk?: string | Buffer) {
body = chunk ? String(chunk) : "";
},
} as unknown as ServerResponse;
return {
res,
getBody: () => body,
};
}
const accountFixture: ResolvedMattermostAccount = {
accountId: "default",
enabled: true,
botToken: "bot-token",
baseUrl: "https://chat.example.com",
botTokenSource: "config",
baseUrlSource: "config",
config: {},
};
describe("slash-http cfg threading", () => {
beforeEach(() => {
mockState.readRequestBodyWithLimit.mockClear();
mockState.parseSlashCommandPayload.mockClear();
mockState.resolveCommandText.mockClear();
mockState.buildModelsProviderData.mockClear();
mockState.resolveMattermostModelPickerEntry.mockClear();
mockState.authorizeMattermostCommandInvocation.mockClear();
mockState.createMattermostClient.mockClear();
mockState.fetchMattermostChannel.mockClear();
mockState.sendMessageMattermost.mockClear();
mockState.normalizeMattermostAllowList.mockClear();
});
it("passes cfg through the no-models slash reply send path", async () => {
const cfg = {
channels: {
mattermost: {
botToken: "exec:secret-ref",
},
},
} as OpenClawConfig;
const handler = createSlashCommandHttpHandler({
account: accountFixture,
cfg,
runtime: {} as RuntimeEnv,
commandTokens: new Set(["valid-token"]),
});
const response = createResponse();
await handler(createRequest(), response.res);
expect(response.res.statusCode).toBe(200);
expect(response.getBody()).toContain("Processing");
expect(mockState.sendMessageMattermost).toHaveBeenCalledWith(
"channel:chan-1",
"No models available.",
expect.objectContaining({
cfg,
accountId: "default",
}),
);
});
});

View File

@ -316,6 +316,7 @@ export function createSlashCommandHttpHandler(params: SlashHttpHandlerParams) {
try {
const to = `channel:${channelId}`;
await sendMessageMattermost(to, "Sorry, something went wrong processing that command.", {
cfg,
accountId: account.accountId,
});
} catch {
@ -387,6 +388,7 @@ async function handleSlashCommandAsync(params: {
const data = await buildModelsProviderData(cfg, route.agentId);
if (data.providers.length === 0) {
await sendMessageMattermost(to, "No models available.", {
cfg,
accountId: account.accountId,
});
return;
@ -418,6 +420,7 @@ async function handleSlashCommandAsync(params: {
});
await sendMessageMattermost(to, view.text, {
cfg,
accountId: account.accountId,
buttons: view.buttons,
});