






















@@ -58,6 +58,145 @@ describe("plugin state keyed store", () => {
5858});
5959});
606061+it("registerIfAbsent inserts the first value and preserves live duplicates", async () => {
62+await withOpenClawTestState({ label: "plugin-state-register-if-absent-live" }, async () => {
63+vi.useFakeTimers();
64+const store = createPluginStateKeyedStore<{ version: number }>("discord", {
65+namespace: "claims",
66+maxEntries: 10,
67+});
68+69+vi.setSystemTime(1000);
70+await expect(store.registerIfAbsent("claim", { version: 1 }, { ttlMs: 1000 })).resolves.toBe(
71+true,
72+);
73+vi.setSystemTime(1200);
74+await expect(store.registerIfAbsent("claim", { version: 2 }, { ttlMs: 5000 })).resolves.toBe(
75+false,
76+);
77+78+await expect(store.lookup("claim")).resolves.toEqual({ version: 1 });
79+await expect(store.entries()).resolves.toMatchObject([
80+{ key: "claim", value: { version: 1 }, createdAt: 1000, expiresAt: 2000 },
81+]);
82+});
83+});
84+85+it("registerIfAbsent replaces expired keys", async () => {
86+await withOpenClawTestState({ label: "plugin-state-register-if-absent-expired" }, async () => {
87+vi.useFakeTimers();
88+const store = createPluginStateKeyedStore<{ version: number }>("discord", {
89+namespace: "claims-expired",
90+maxEntries: 10,
91+});
92+93+vi.setSystemTime(1000);
94+await expect(store.registerIfAbsent("claim", { version: 1 }, { ttlMs: 100 })).resolves.toBe(
95+true,
96+);
97+vi.setSystemTime(1200);
98+await expect(store.registerIfAbsent("claim", { version: 2 })).resolves.toBe(true);
99+100+await expect(store.lookup("claim")).resolves.toEqual({ version: 2 });
101+await expect(store.entries()).resolves.toMatchObject([
102+{ key: "claim", value: { version: 2 }, createdAt: 1200 },
103+]);
104+});
105+});
106+107+it("registerIfAbsent keeps plugin and namespace claims isolated", async () => {
108+await withOpenClawTestState(
109+{ label: "plugin-state-register-if-absent-isolation" },
110+async () => {
111+const discordA = createPluginStateKeyedStore<{ owner: string }>("discord", {
112+namespace: "claims-a",
113+maxEntries: 10,
114+});
115+const discordB = createPluginStateKeyedStore<{ owner: string }>("discord", {
116+namespace: "claims-b",
117+maxEntries: 10,
118+});
119+const telegramA = createPluginStateKeyedStore<{ owner: string }>("telegram", {
120+namespace: "claims-a",
121+maxEntries: 10,
122+});
123+124+await expect(discordA.registerIfAbsent("same", { owner: "discord-a" })).resolves.toBe(true);
125+await expect(discordB.registerIfAbsent("same", { owner: "discord-b" })).resolves.toBe(true);
126+await expect(telegramA.registerIfAbsent("same", { owner: "telegram-a" })).resolves.toBe(
127+true,
128+);
129+await expect(discordA.registerIfAbsent("same", { owner: "overwrite" })).resolves.toBe(
130+false,
131+);
132+133+await expect(discordA.lookup("same")).resolves.toEqual({ owner: "discord-a" });
134+await expect(discordB.lookup("same")).resolves.toEqual({ owner: "discord-b" });
135+await expect(telegramA.lookup("same")).resolves.toEqual({ owner: "telegram-a" });
136+},
137+);
138+});
139+140+it("registerIfAbsent only lets one parallel claimant win", async () => {
141+await withOpenClawTestState({ label: "plugin-state-register-if-absent-race" }, async () => {
142+const store = createPluginStateKeyedStore<{ claimant: number }>("discord", {
143+namespace: "claims-race",
144+maxEntries: 10,
145+});
146+147+const attempts = await Promise.all(
148+Array.from({ length: 25 }, async (_, claimant) =>
149+store.registerIfAbsent("claim", { claimant }),
150+),
151+);
152+153+expect(attempts.filter(Boolean)).toHaveLength(1);
154+const stored = await store.lookup("claim");
155+expect(stored).toBeDefined();
156+expect(attempts[stored?.claimant ?? -1]).toBe(true);
157+});
158+});
159+160+it("registerIfAbsent preserves eviction and plugin row cap behavior", async () => {
161+await withOpenClawTestState({ label: "plugin-state-register-if-absent-limits" }, async () => {
162+vi.useFakeTimers();
163+const evicting = createPluginStateKeyedStore<number>("discord", {
164+namespace: "claims-evict",
165+maxEntries: 2,
166+});
167+vi.setSystemTime(1000);
168+await evicting.registerIfAbsent("a", 1);
169+vi.setSystemTime(2000);
170+await evicting.registerIfAbsent("b", 2);
171+vi.setSystemTime(3000);
172+await evicting.registerIfAbsent("c", 3);
173+await expect(evicting.entries()).resolves.toMatchObject([{ key: "b" }, { key: "c" }]);
174+175+seedPluginStateEntriesForTests([
176+ ...Array.from({ length: 999 }, (_, entryIndex) => ({
177+pluginId: "limited-plugin",
178+namespace: "limit",
179+key: `k-${entryIndex}`,
180+value: { entryIndex },
181+})),
182+{
183+pluginId: "limited-plugin",
184+namespace: "sibling",
185+key: "k-0",
186+value: { sibling: true },
187+},
188+]);
189+const limited = createPluginStateKeyedStore("limited-plugin", {
190+namespace: "limit",
191+maxEntries: 1_001,
192+});
193+await expect(limited.registerIfAbsent("overflow", { overflow: true })).rejects.toMatchObject({
194+code: "PLUGIN_STATE_LIMIT_EXCEEDED",
195+});
196+await expect(limited.lookup("overflow")).resolves.toBeUndefined();
197+});
198+});
199+61200it("returns undefined for missing lookups and consumes by deleting atomically", async () => {
62201await withOpenClawTestState({ label: "plugin-state-consume" }, async () => {
63202const store = createPluginStateKeyedStore<{ ok: boolean }>("discord", {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。