Skip to content

Commit 6d3eff9

Browse files
authored
refactor(channels): privatize Mattermost and MSTeams test seams (#107924)
* refactor(mattermost): privatize internal test seams * refactor(msteams): privatize internal test seams * docs(mattermost): fix public API import path * refactor(deadcode): shrink export baseline
1 parent 72e50c8 commit 6d3eff9

32 files changed

Lines changed: 241 additions & 340 deletions

docs/channels/mattermost.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ When a user clicks a button:
415415

416416
### Direct API integration (external scripts)
417417

418-
External scripts and webhooks can post buttons directly via the Mattermost REST API instead of going through the agent's `message` tool. Use `buildButtonAttachments()` from the plugin when possible; if posting raw JSON, follow these rules:
418+
External scripts and webhooks can post buttons directly via the Mattermost REST API instead of going through the agent's `message` tool. Prefer OpenClaw's `message` tool. For direct integrations, import `buildButtonAttachments` from `@openclaw/mattermost/api.js`; if posting raw JSON, follow these rules:
419419

420420
**Payload structure:**
421421

extensions/mattermost/api.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Mattermost tests cover the documented public helper API.
2+
import { describe, expect, it } from "vitest";
3+
import { buildButtonAttachments } from "./api.js";
4+
5+
describe("@openclaw/mattermost api", () => {
6+
it("exports the interactive-button attachment builder", () => {
7+
expect(typeof buildButtonAttachments).toBe("function");
8+
});
9+
});

extensions/mattermost/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
// Keep this barrel helper-only so plugin-sdk facades do not pull the full
2-
// channel plugin (and its runtime state) into tests or other shared surfaces.
1+
// Public Mattermost API barrel for lightweight integration helpers.
2+
export { buildButtonAttachments } from "./src/mattermost/interactions.js";
33
export { isMattermostSenderAllowed } from "./src/mattermost/monitor-auth.js";

extensions/mattermost/src/channel.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => {
2424
});
2525

2626
import { mattermostPlugin } from "./channel.js";
27-
import { resetMattermostReactionBotUserCacheForTests } from "./mattermost/reactions.js";
2827
import {
2928
createMattermostReactionFetchMock,
3029
createMattermostTestConfig,
@@ -575,12 +574,10 @@ describe("mattermostPlugin", () => {
575574
});
576575

577576
describe("messageActions", () => {
578-
beforeEach(() => {
579-
resetMattermostReactionBotUserCacheForTests();
580-
});
577+
let reactionActionSequence = 0;
581578

582579
const runReactAction = async (params: Record<string, unknown>, fetchMode: "add" | "remove") => {
583-
const cfg = createMattermostTestConfig();
580+
const cfg = createMattermostTestConfig(`message-action-${++reactionActionSequence}`);
584581
const fetchImpl = createMattermostReactionFetchMock({
585582
mode: fetchMode,
586583
postId: "POST1",

extensions/mattermost/src/mattermost/interactions.test.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { setMattermostRuntime } from "../runtime.js";
66
import { resolveMattermostAccount } from "./accounts.js";
77
import type { MattermostClient, MattermostPost } from "./client.js";
88
import {
9-
buildButtonAttachments,
9+
buildButtonProps,
1010
computeInteractionCallbackUrl,
1111
createMattermostInteractionHandler,
1212
resolveInteractionCallbackPath,
@@ -15,9 +15,39 @@ import {
1515
setInteractionSecret,
1616
} from "./interactions.js";
1717

18-
type ButtonAttachments = ReturnType<typeof buildButtonAttachments>;
19-
type ButtonAttachment = ButtonAttachments[number];
20-
type ButtonAction = NonNullable<ButtonAttachment["actions"]>[number];
18+
type ButtonAction = {
19+
id: string;
20+
type: "button";
21+
name: string;
22+
style?: "default" | "primary" | "danger";
23+
integration: { url: string; context: Record<string, unknown> };
24+
};
25+
type ButtonAttachment = { text: string; actions?: ButtonAction[] };
26+
type ButtonAttachments = ButtonAttachment[];
27+
type ButtonPropsInput = {
28+
callbackUrl: string;
29+
accountId?: string;
30+
buttons: Array<{
31+
id: string;
32+
name: string;
33+
style?: "default" | "primary" | "danger";
34+
context?: Record<string, unknown>;
35+
}>;
36+
text?: string;
37+
};
38+
39+
function buildButtonAttachmentsForTest(params: ButtonPropsInput): ButtonAttachments {
40+
const signedChannelId = params.buttons[0]?.context?.__openclaw_channel_id;
41+
const props = buildButtonProps({
42+
...params,
43+
channelId: typeof signedChannelId === "string" ? signedChannelId : "test-channel",
44+
});
45+
const attachments = props?.attachments;
46+
if (!Array.isArray(attachments)) {
47+
throw new Error("Expected button attachments");
48+
}
49+
return attachments as ButtonAttachments;
50+
}
2151

2252
function requireFirstAttachment(attachments: ButtonAttachments): ButtonAttachment {
2353
const [attachment] = attachments;
@@ -45,7 +75,7 @@ function requireAction(attachments: ButtonAttachments, index = 0): ButtonAction
4575

4676
function generateInteractionToken(context: Record<string, unknown>, accountId?: string): string {
4777
const actionId = typeof context.action_id === "string" ? context.action_id : "test";
48-
const attachments = buildButtonAttachments({
78+
const attachments = buildButtonAttachmentsForTest({
4979
callbackUrl: "https://gateway.example.com/mattermost/interactions/test",
5080
accountId,
5181
buttons: [{ id: actionId, name: "Test", context }],
@@ -323,15 +353,15 @@ describe("resolveInteractionCallbackPath", () => {
323353
});
324354
});
325355

326-
// ── buildButtonAttachments ───────────────────────────────────────────
356+
// ── buildButtonProps attachments ────────────────────────────────────
327357

328-
describe("buildButtonAttachments", () => {
358+
describe("buildButtonProps attachments", () => {
329359
beforeEach(() => {
330360
setInteractionSecret("test-bot-token");
331361
});
332362

333363
it("returns an array with one attachment containing all buttons", () => {
334-
const result = buildButtonAttachments({
364+
const result = buildButtonAttachmentsForTest({
335365
callbackUrl: "http://localhost:18789/mattermost/interactions/default",
336366
buttons: [
337367
{ id: "btn1", name: "Click Me" },
@@ -344,7 +374,7 @@ describe("buildButtonAttachments", () => {
344374
});
345375

346376
it("sets type to 'button' on every action", () => {
347-
const result = buildButtonAttachments({
377+
const result = buildButtonAttachmentsForTest({
348378
callbackUrl: "http://localhost:18789/cb",
349379
buttons: [{ id: "a", name: "A" }],
350380
});
@@ -353,7 +383,7 @@ describe("buildButtonAttachments", () => {
353383
});
354384

355385
it("includes HMAC _token in integration context", () => {
356-
const result = buildButtonAttachments({
386+
const result = buildButtonAttachmentsForTest({
357387
callbackUrl: "http://localhost:18789/cb",
358388
buttons: [{ id: "test", name: "Test" }],
359389
});
@@ -363,7 +393,7 @@ describe("buildButtonAttachments", () => {
363393
});
364394

365395
it("includes sanitized action_id in integration context", () => {
366-
const result = buildButtonAttachments({
396+
const result = buildButtonAttachmentsForTest({
367397
callbackUrl: "http://localhost:18789/cb",
368398
buttons: [{ id: "my_action", name: "Do It" }],
369399
});
@@ -375,7 +405,7 @@ describe("buildButtonAttachments", () => {
375405
});
376406

377407
it("merges custom context into integration context", () => {
378-
const result = buildButtonAttachments({
408+
const result = buildButtonAttachmentsForTest({
379409
callbackUrl: "http://localhost:18789/cb",
380410
buttons: [{ id: "btn", name: "Go", context: { tweet_id: "123", batch: true } }],
381411
});
@@ -389,7 +419,7 @@ describe("buildButtonAttachments", () => {
389419

390420
it("passes callback URL to each button integration", () => {
391421
const url = "http://localhost:18789/mattermost/interactions/default";
392-
const result = buildButtonAttachments({
422+
const result = buildButtonAttachmentsForTest({
393423
callbackUrl: url,
394424
buttons: [
395425
{ id: "a", name: "A" },
@@ -403,7 +433,7 @@ describe("buildButtonAttachments", () => {
403433
});
404434

405435
it("preserves button style", () => {
406-
const result = buildButtonAttachments({
436+
const result = buildButtonAttachmentsForTest({
407437
callbackUrl: "http://localhost/cb",
408438
buttons: [
409439
{ id: "ok", name: "OK", style: "primary" },
@@ -416,7 +446,7 @@ describe("buildButtonAttachments", () => {
416446
});
417447

418448
it("uses provided text for the attachment", () => {
419-
const result = buildButtonAttachments({
449+
const result = buildButtonAttachmentsForTest({
420450
callbackUrl: "http://localhost/cb",
421451
buttons: [{ id: "x", name: "X" }],
422452
text: "Choose an action:",
@@ -426,7 +456,7 @@ describe("buildButtonAttachments", () => {
426456
});
427457

428458
it("defaults to empty string text when not provided", () => {
429-
const result = buildButtonAttachments({
459+
const result = buildButtonAttachmentsForTest({
430460
callbackUrl: "http://localhost/cb",
431461
buttons: [{ id: "x", name: "X" }],
432462
});
@@ -435,7 +465,7 @@ describe("buildButtonAttachments", () => {
435465
});
436466

437467
it("generates verifiable tokens", () => {
438-
const result = buildButtonAttachments({
468+
const result = buildButtonAttachmentsForTest({
439469
callbackUrl: "http://localhost/cb",
440470
buttons: [{ id: "verify_me", name: "V", context: { extra: "data" } }],
441471
});
@@ -447,7 +477,7 @@ describe("buildButtonAttachments", () => {
447477
});
448478

449479
it("generates tokens that verify even when Mattermost reorders context keys", () => {
450-
const result = buildButtonAttachments({
480+
const result = buildButtonAttachmentsForTest({
451481
callbackUrl: "http://localhost/cb",
452482
buttons: [{ id: "do_action", name: "Do", context: { tweet_id: "42", category: "ai" } }],
453483
});

extensions/mattermost/src/mattermost/reactions.test-helpers.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ export function requestUrl(url: string | URL | Request): string {
1313
return url.url;
1414
}
1515

16-
export function createMattermostTestConfig(): OpenClawConfig {
16+
let testConfigSequence = 0;
17+
18+
export function createMattermostTestConfig(
19+
cacheKey = String(++testConfigSequence),
20+
): OpenClawConfig {
1721
return {
1822
channels: {
1923
mattermost: {
2024
enabled: true,
21-
botToken: "test-token",
22-
baseUrl: "https://chat.example.com",
25+
botToken: `test-token-${cacheKey}`,
26+
baseUrl: `https://${cacheKey}.chat.example.com`,
2327
},
2428
},
2529
};

extensions/mattermost/src/mattermost/reactions.test.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
// Mattermost tests cover reactions plugin behavior.
22
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3-
import {
4-
addMattermostReaction,
5-
removeMattermostReaction,
6-
resetMattermostReactionBotUserCacheForTests,
7-
} from "./reactions.js";
3+
import { addMattermostReaction, removeMattermostReaction } from "./reactions.js";
84
import {
95
createMattermostReactionFetchMock,
106
createMattermostTestConfig,
117
requestUrl,
128
} from "./reactions.test-helpers.js";
139

1410
describe("mattermost reactions", () => {
11+
let cacheKeySequence = 0;
12+
let cacheKey = "";
13+
1514
beforeEach(() => {
16-
resetMattermostReactionBotUserCacheForTests();
15+
cacheKey = String(++cacheKeySequence);
1716
});
1817

1918
afterEach(() => {
@@ -22,7 +21,7 @@ describe("mattermost reactions", () => {
2221

2322
async function addReactionWithFetch(fetchMock: typeof fetch) {
2423
return addMattermostReaction({
25-
cfg: createMattermostTestConfig(),
24+
cfg: createMattermostTestConfig(cacheKey),
2625
postId: "POST1",
2726
emojiName: "thumbsup",
2827
fetchImpl: fetchMock,
@@ -31,7 +30,7 @@ describe("mattermost reactions", () => {
3130

3231
async function removeReactionWithFetch(fetchMock: typeof fetch) {
3332
return removeMattermostReaction({
34-
cfg: createMattermostTestConfig(),
33+
cfg: createMattermostTestConfig(cacheKey),
3534
postId: "POST1",
3635
emojiName: "thumbsup",
3736
fetchImpl: fetchMock,
@@ -88,7 +87,7 @@ describe("mattermost reactions", () => {
8887
emojiName: "thumbsup",
8988
});
9089

91-
const cfg = createMattermostTestConfig();
90+
const cfg = createMattermostTestConfig(cacheKey);
9291
const addResult = await addMattermostReaction({
9392
cfg,
9493
postId: "POST1",
@@ -111,7 +110,7 @@ describe("mattermost reactions", () => {
111110
});
112111

113112
it("does not reuse cached bot user ids while the process clock is invalid", async () => {
114-
const cfg = createMattermostTestConfig();
113+
const cfg = createMattermostTestConfig(cacheKey);
115114
const firstFetch = createMattermostReactionFetchMock({
116115
mode: "add",
117116
postId: "POST1",
@@ -170,7 +169,7 @@ describe("mattermost reactions", () => {
170169

171170
it("does not cache bot user ids when cache expiry would exceed the Date range", async () => {
172171
vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_000);
173-
const cfg = createMattermostTestConfig();
172+
const cfg = createMattermostTestConfig(cacheKey);
174173
const fetchMock = createMattermostReactionFetchMock({
175174
mode: "both",
176175
postId: "POST1",

extensions/mattermost/src/mattermost/reactions.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,6 @@ export async function removeMattermostReaction(params: {
7878
});
7979
}
8080

81-
export function resetMattermostReactionBotUserCacheForTests(): void {
82-
botUserIdCache.clear();
83-
}
84-
8581
async function runMattermostReaction(
8682
params: ReactionParams,
8783
options: {

extensions/mattermost/src/mattermost/runtime-api.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
3131
export {
3232
DEFAULT_GROUP_HISTORY_LIMIT,
3333
createChannelHistoryWindow,
34-
buildInboundHistoryFromMap,
35-
buildPendingHistoryContextFromMap,
36-
recordPendingHistoryEntryIfEnabled,
3734
} from "openclaw/plugin-sdk/reply-history";
3835
export { registerPluginHttpRoute } from "openclaw/plugin-sdk/webhook-targets";
3936
export {

extensions/mattermost/src/mattermost/send.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
44

55
let sendMessageMattermost: typeof import("./send.js").sendMessageMattermost;
66
let parseMattermostTarget: typeof import("./target-resolution.js").parseMattermostTarget;
7-
let resetMattermostOpaqueTargetCacheForTests: typeof import("./target-resolution.js").resetMattermostOpaqueTargetCacheForTests;
87

98
type SendMessageMattermostOptions = NonNullable<
109
Parameters<typeof import("./send.js").sendMessageMattermost>[2]
@@ -216,9 +215,7 @@ describe("sendMessageMattermost", () => {
216215
mockState.fetchMattermostChannelByName.mockResolvedValue({ id: "town-square" });
217216
mockState.uploadMattermostFile.mockResolvedValue({ id: "file-1" });
218217
({ sendMessageMattermost } = await import("./send.js"));
219-
({ parseMattermostTarget, resetMattermostOpaqueTargetCacheForTests } =
220-
await import("./target-resolution.js"));
221-
resetMattermostOpaqueTargetCacheForTests();
218+
({ parseMattermostTarget } = await import("./target-resolution.js"));
222219
});
223220

224221
it("uses provided cfg and skips runtime loadConfig", async () => {

0 commit comments

Comments
 (0)