Skip to content

Commit 3cf28a1

Browse files
ly85206559clawsweeper[bot]
authored andcommitted
fix(mattermost): anchor slash state on globalThis (#68113)
1 parent 6b0ffa2 commit 3cf28a1

2 files changed

Lines changed: 67 additions & 3 deletions

File tree

extensions/mattermost/src/mattermost/slash-state.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Mattermost tests cover slash state plugin behavior.
2-
import { describe, expect, it } from "vitest";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
33
import type { OpenClawConfig, RuntimeEnv } from "../runtime-api.js";
44
import type { ResolvedMattermostAccount } from "./accounts.js";
55
import type { MattermostRegisteredCommand } from "./slash-commands.js";
@@ -48,6 +48,49 @@ const slashApi = {
4848
runtime: RuntimeEnv;
4949
};
5050

51+
const ACCOUNT_STATES_KEY = Symbol.for("openclaw.mattermost.slash-account-states");
52+
53+
describe("slash-state global singleton", () => {
54+
afterEach(() => {
55+
deactivateSlashCommands();
56+
});
57+
58+
it("anchors accountStates on globalThis", () => {
59+
deactivateSlashCommands();
60+
activateSlashCommands({
61+
account: createResolvedMattermostAccount("a1"),
62+
commandTokens: ["tok-a"],
63+
registeredCommands: [],
64+
api: slashApi,
65+
});
66+
67+
const globalStore = globalThis as Record<PropertyKey, unknown>;
68+
const map = globalStore[ACCOUNT_STATES_KEY];
69+
expect(map).toBeInstanceOf(Map);
70+
expect((map as Map<string, unknown>).has("a1")).toBe(true);
71+
});
72+
73+
it("preserves slash state across module reloads", async () => {
74+
deactivateSlashCommands();
75+
activateSlashCommands({
76+
account: createResolvedMattermostAccount("a1"),
77+
commandTokens: ["tok-reload"],
78+
registeredCommands: [],
79+
api: slashApi,
80+
});
81+
82+
vi.resetModules();
83+
const reloaded = await import("./slash-state.js");
84+
const match = reloaded.resolveSlashHandlerForToken("tok-reload");
85+
86+
expect(match.kind).toBe("single");
87+
if (match.kind !== "single") {
88+
throw new Error("expected single match after module reload");
89+
}
90+
expect(match.accountIds).toEqual(["a1"]);
91+
});
92+
});
93+
5194
describe("slash-state token routing", () => {
5295
it("returns single match when token belongs to one account", () => {
5396
deactivateSlashCommands();

extensions/mattermost/src/mattermost/slash-state.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,29 @@ type SlashCommandAccountState = {
6262
triggerMap: Map<string, string>;
6363
};
6464

65-
/** Map from accountId → per-account slash command state. */
66-
const accountStates = new Map<string, SlashCommandAccountState>();
65+
/**
66+
* Map from accountId → per-account slash command state.
67+
*
68+
* Anchored to globalThis so that jiti-loaded (route registration) and
69+
* native-ESM-loaded (monitor/activation) module instances share the
70+
* same Map. Without this, each module loader creates its own copy of
71+
* the module-level variable and the HTTP handler never sees the tokens
72+
* populated by the monitor.
73+
*/
74+
const ACCOUNT_STATES_KEY = Symbol.for("openclaw.mattermost.slash-account-states");
75+
76+
function getSlashAccountStates(): Map<string, SlashCommandAccountState> {
77+
const globalStore = globalThis as Record<PropertyKey, unknown>;
78+
const existing = globalStore[ACCOUNT_STATES_KEY];
79+
if (existing instanceof Map) {
80+
return existing as Map<string, SlashCommandAccountState>;
81+
}
82+
const accountStates = new Map<string, SlashCommandAccountState>();
83+
globalStore[ACCOUNT_STATES_KEY] = accountStates;
84+
return accountStates;
85+
}
86+
87+
const accountStates = getSlashAccountStates();
6788

6889
export function resolveSlashHandlerForToken(token: string): SlashHandlerMatch {
6990
const matches: Array<{

0 commit comments

Comments
 (0)