























1+// Mattermost tests cover thread participation cache plugin behavior.
2+import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime";
3+import {
4+createPluginStateKeyedStoreForTests,
5+resetPluginStateStoreForTests,
6+} from "openclaw/plugin-sdk/plugin-state-test-runtime";
7+import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
8+import { afterEach, beforeEach, describe, expect, it } from "vitest";
9+import { setMattermostRuntime } from "../runtime.js";
10+import {
11+clearMattermostThreadParticipationCache,
12+hasMattermostThreadParticipationWithPersistence,
13+recordMattermostThreadParticipation,
14+} from "./thread-participation.js";
15+16+// Drain microtasks + the immediate queue so the fire-and-forget persistent write
17+// in recordMattermostThreadParticipation has settled before we assert on it.
18+const flush = (): Promise<void> => new Promise((resolve) => setImmediate(resolve));
19+20+function setRuntime(openKeyedStore: (options: OpenKeyedStoreOptions) => unknown): void {
21+setMattermostRuntime({
22+state: { openKeyedStore },
23+logging: { getChildLogger: () => ({ warn() {} }) },
24+} as unknown as PluginRuntime);
25+}
26+27+function setPersistentRuntime(): void {
28+setRuntime((options) => createPluginStateKeyedStoreForTests("mattermost", options));
29+}
30+31+describe("mattermost thread participation", () => {
32+beforeEach(() => {
33+resetPluginStateStoreForTests();
34+clearMattermostThreadParticipationCache();
35+setPersistentRuntime();
36+});
37+38+afterEach(() => {
39+clearMattermostThreadParticipationCache();
40+resetPluginStateStoreForTests();
41+});
42+43+it("remembers a thread the bot replied in", async () => {
44+recordMattermostThreadParticipation("acct", "chan", "root-1");
45+await expect(
46+hasMattermostThreadParticipationWithPersistence({
47+accountId: "acct",
48+channelId: "chan",
49+threadRootId: "root-1",
50+}),
51+).resolves.toBe(true);
52+});
53+54+it("isolates participation by account, channel, and thread", async () => {
55+recordMattermostThreadParticipation("acct", "chan", "root-1");
56+await flush();
57+for (const probe of [
58+{ accountId: "other", channelId: "chan", threadRootId: "root-1" },
59+{ accountId: "acct", channelId: "other", threadRootId: "root-1" },
60+{ accountId: "acct", channelId: "chan", threadRootId: "root-2" },
61+]) {
62+await expect(hasMattermostThreadParticipationWithPersistence(probe)).resolves.toBe(false);
63+}
64+});
65+66+it("ignores empty identifiers", async () => {
67+recordMattermostThreadParticipation("", "chan", "root-1");
68+await expect(
69+hasMattermostThreadParticipationWithPersistence({
70+accountId: "",
71+channelId: "chan",
72+threadRootId: "root-1",
73+}),
74+).resolves.toBe(false);
75+});
76+77+it("recovers participation from the persistent store after the in-memory cache is lost", async () => {
78+recordMattermostThreadParticipation("acct", "chan", "root-1");
79+await flush();
80+// Simulate a restart: in-memory cache cleared, persistent SQLite store intact.
81+clearMattermostThreadParticipationCache();
82+await expect(
83+hasMattermostThreadParticipationWithPersistence({
84+accountId: "acct",
85+channelId: "chan",
86+threadRootId: "root-1",
87+}),
88+).resolves.toBe(true);
89+});
90+91+it("degrades to in-memory only when the persistent store fails", async () => {
92+setRuntime(() => {
93+throw new Error("sqlite unavailable");
94+});
95+// record + read must not throw; the in-memory cache still answers.
96+recordMattermostThreadParticipation("acct", "chan", "root-1");
97+await expect(
98+hasMattermostThreadParticipationWithPersistence({
99+accountId: "acct",
100+channelId: "chan",
101+threadRootId: "root-1",
102+}),
103+).resolves.toBe(true);
104+await expect(
105+hasMattermostThreadParticipationWithPersistence({
106+accountId: "acct",
107+channelId: "chan",
108+threadRootId: "missing",
109+}),
110+).resolves.toBe(false);
111+});
112+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。