Files
openclaw/extensions/mattermost/src/mattermost/slash-state.test.ts
clawsweeper[bot] 69d1d78649 fix(mattermost): anchor slash state on globalThis (#68113) (#90534)
Summary:
- The branch stores Mattermost slash-command account state in a process-wide Symbol.for/globalThis Map and adds module-reload regression coverage.
- PR surface: Source +21, Tests +43. Total +64 across 2 files.
- Reproducibility: yes. at source level: current main's route handler returns 503 when its module-local accoun ... pulate state through a separate loader path. I did not run a live Mattermost POST in this read-only review.

Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.

Validation:
- ClawSweeper review passed for head 3cf28a1f96.
- Required merge gates passed before the squash merge.

Prepared head SHA: 3cf28a1f96
Review: https://github.com/openclaw/openclaw/pull/90534#issuecomment-4627897262

Co-authored-by: ben.li <ly85206559@163.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-06-05 04:10:43 +00:00

189 lines
5.5 KiB
TypeScript

// Mattermost tests cover slash state plugin behavior.
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig, RuntimeEnv } from "../runtime-api.js";
import type { ResolvedMattermostAccount } from "./accounts.js";
import type { MattermostRegisteredCommand } from "./slash-commands.js";
import {
activateSlashCommands,
deactivateSlashCommands,
resolveSlashHandlerForCommand,
resolveSlashHandlerForToken,
} from "./slash-state.js";
function createResolvedMattermostAccount(accountId: string): ResolvedMattermostAccount {
return {
accountId,
enabled: true,
botTokenSource: "config",
baseUrlSource: "config",
streamingMode: "partial",
config: {},
};
}
function createRegisteredCommand(params?: {
id?: string;
teamId?: string;
trigger?: string;
}): MattermostRegisteredCommand {
return {
id: params?.id ?? "cmd-1",
teamId: params?.teamId ?? "team-1",
trigger: params?.trigger ?? "oc_status",
token: "token-1",
url: "https://gateway.example.com/slash",
managed: false,
};
}
const slashApi = {
cfg: {},
runtime: {
log: () => {},
error: () => {},
exit: () => {},
},
} satisfies {
cfg: OpenClawConfig;
runtime: RuntimeEnv;
};
const ACCOUNT_STATES_KEY = Symbol.for("openclaw.mattermost.slash-account-states");
describe("slash-state global singleton", () => {
afterEach(() => {
deactivateSlashCommands();
});
it("anchors accountStates on globalThis", () => {
deactivateSlashCommands();
activateSlashCommands({
account: createResolvedMattermostAccount("a1"),
commandTokens: ["tok-a"],
registeredCommands: [],
api: slashApi,
});
const globalStore = globalThis as Record<PropertyKey, unknown>;
const map = globalStore[ACCOUNT_STATES_KEY];
expect(map).toBeInstanceOf(Map);
expect((map as Map<string, unknown>).has("a1")).toBe(true);
});
it("preserves slash state across module reloads", async () => {
deactivateSlashCommands();
activateSlashCommands({
account: createResolvedMattermostAccount("a1"),
commandTokens: ["tok-reload"],
registeredCommands: [],
api: slashApi,
});
vi.resetModules();
const reloaded = await import("./slash-state.js");
const match = reloaded.resolveSlashHandlerForToken("tok-reload");
expect(match.kind).toBe("single");
if (match.kind !== "single") {
throw new Error("expected single match after module reload");
}
expect(match.accountIds).toEqual(["a1"]);
});
});
describe("slash-state token routing", () => {
it("returns single match when token belongs to one account", () => {
deactivateSlashCommands();
activateSlashCommands({
account: createResolvedMattermostAccount("a1"),
commandTokens: ["tok-a"],
registeredCommands: [],
api: slashApi,
});
const match = resolveSlashHandlerForToken("tok-a");
expect(match.kind).toBe("single");
if (match.kind !== "single") {
throw new Error("expected single match");
}
expect(match.source).toBe("token");
expect(match.accountIds).toEqual(["a1"]);
expect(typeof match.handler).toBe("function");
});
it("returns ambiguous when same token exists in multiple accounts", () => {
deactivateSlashCommands();
activateSlashCommands({
account: createResolvedMattermostAccount("a1"),
commandTokens: ["tok-shared"],
registeredCommands: [],
api: slashApi,
});
activateSlashCommands({
account: createResolvedMattermostAccount("a2"),
commandTokens: ["tok-shared"],
registeredCommands: [],
api: slashApi,
});
const match = resolveSlashHandlerForToken("tok-shared");
expect(match.kind).toBe("ambiguous");
if (match.kind !== "ambiguous") {
throw new Error("expected ambiguous match");
}
expect(match.source).toBe("token");
expect(match.accountIds.toSorted()).toEqual(["a1", "a2"]);
});
it("routes by registered team and command when token lookup misses", () => {
deactivateSlashCommands();
activateSlashCommands({
account: createResolvedMattermostAccount("a1"),
commandTokens: ["old-token"],
registeredCommands: [createRegisteredCommand()],
api: slashApi,
});
const match = resolveSlashHandlerForCommand({
teamId: "team-1",
command: "/oc_status",
});
expect(match.kind).toBe("single");
if (match.kind !== "single") {
throw new Error("expected single match");
}
expect(match.source).toBe("command");
expect(match.accountIds).toEqual(["a1"]);
expect(typeof match.handler).toBe("function");
});
it("returns ambiguous when registered team and command match multiple accounts", () => {
deactivateSlashCommands();
activateSlashCommands({
account: createResolvedMattermostAccount("a1"),
commandTokens: ["tok-a"],
registeredCommands: [createRegisteredCommand({ id: "cmd-a" })],
api: slashApi,
});
activateSlashCommands({
account: createResolvedMattermostAccount("a2"),
commandTokens: ["tok-b"],
registeredCommands: [createRegisteredCommand({ id: "cmd-b" })],
api: slashApi,
});
const match = resolveSlashHandlerForCommand({
teamId: "team-1",
command: "/oc_status",
});
expect(match.kind).toBe("ambiguous");
if (match.kind !== "ambiguous") {
throw new Error("expected ambiguous match");
}
expect(match.source).toBe("command");
expect(match.accountIds.toSorted()).toEqual(["a1", "a2"]);
});
});