mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
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 head3cf28a1f96. - Required merge gates passed before the squash merge. Prepared head SHA:3cf28a1f96Review: 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>
189 lines
5.5 KiB
TypeScript
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"]);
|
|
});
|
|
});
|