mirror of https://github.com/openclaw/openclaw.git
fix(mattermost): thread resolved cfg through reply delivery send calls (#48347)
Merged via squash.
Prepared head SHA: 7ca468e365
Co-authored-by: mathiasnagler <9951231+mathiasnagler@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Reviewed-by: @mukhtharcm
This commit is contained in:
parent
208ff68298
commit
39fbfd9b28
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -87,9 +89,14 @@ describe("deliverMattermostReplyPayload", () => {
|
|||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessage).toHaveBeenCalledWith("channel:town-square", "hello", {
|
||||
accountId: "default",
|
||||
replyToId: "root-post",
|
||||
});
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
"channel:town-square",
|
||||
"hello",
|
||||
expect.objectContaining({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
replyToId: "root-post",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ type SendMattermostMessage = (
|
|||
to: string,
|
||||
text: string,
|
||||
opts: {
|
||||
cfg?: OpenClawConfig;
|
||||
accountId?: string;
|
||||
mediaUrl?: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
|
|
@ -49,12 +50,14 @@ export async function deliverMattermostReplyPayload(params: {
|
|||
params.core.channel.text.chunkMarkdownTextWithMode(value, params.textLimit, chunkMode),
|
||||
sendText: async (chunk) => {
|
||||
await params.sendMessage(params.to, chunk, {
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
replyToId: params.replyToId,
|
||||
});
|
||||
},
|
||||
sendMedia: async ({ mediaUrl, caption }) => {
|
||||
await params.sendMessage(params.to, caption ?? "", {
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
mediaUrl,
|
||||
mediaLocalRoots,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue