fix(mattermost): anchor slash state on globalThis (#68113)

This commit is contained in:
ben.li
2026-06-04 23:19:09 +08:00
committed by clawsweeper
parent 6b0ffa2106
commit 3cf28a1f96
2 changed files with 67 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
// Mattermost tests cover slash state plugin behavior.
import { describe, expect, it } from "vitest";
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";
@@ -48,6 +48,49 @@ const slashApi = {
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();

View File

@@ -62,8 +62,29 @@ type SlashCommandAccountState = {
triggerMap: Map<string, string>;
};
/** Map from accountId → per-account slash command state. */
const accountStates = new Map<string, SlashCommandAccountState>();
/**
* Map from accountId → per-account slash command state.
*
* Anchored to globalThis so that jiti-loaded (route registration) and
* native-ESM-loaded (monitor/activation) module instances share the
* same Map. Without this, each module loader creates its own copy of
* the module-level variable and the HTTP handler never sees the tokens
* populated by the monitor.
*/
const ACCOUNT_STATES_KEY = Symbol.for("openclaw.mattermost.slash-account-states");
function getSlashAccountStates(): Map<string, SlashCommandAccountState> {
const globalStore = globalThis as Record<PropertyKey, unknown>;
const existing = globalStore[ACCOUNT_STATES_KEY];
if (existing instanceof Map) {
return existing as Map<string, SlashCommandAccountState>;
}
const accountStates = new Map<string, SlashCommandAccountState>();
globalStore[ACCOUNT_STATES_KEY] = accountStates;
return accountStates;
}
const accountStates = getSlashAccountStates();
export function resolveSlashHandlerForToken(token: string): SlashHandlerMatch {
const matches: Array<{