|
1 | 1 | import fs from "node:fs"; |
2 | 2 | import os from "node:os"; |
3 | 3 | import path from "node:path"; |
4 | | -import { afterEach, describe, expect, it, vi } from "vitest"; |
| 4 | +import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime"; |
| 5 | +import { |
| 6 | + createPluginStateKeyedStoreForTests, |
| 7 | + resetPluginStateStoreForTests, |
| 8 | +} from "openclaw/plugin-sdk/plugin-state-test-runtime"; |
| 9 | +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 10 | +import type { PluginRuntime } from "../../runtime-api.js"; |
| 11 | +import { setMatrixRuntime } from "../../runtime.js"; |
| 12 | +import { LogService } from "../sdk/logger.js"; |
5 | 13 | import { createMatrixInboundEventDeduper } from "./inbound-dedupe.js"; |
6 | 14 |
|
7 | 15 | describe("Matrix inbound event dedupe", () => { |
8 | 16 | const tempDirs: string[] = []; |
9 | 17 |
|
| 18 | + beforeEach(() => { |
| 19 | + setMatrixRuntime({ |
| 20 | + state: { |
| 21 | + openKeyedStore: (options: OpenKeyedStoreOptions) => |
| 22 | + createPluginStateKeyedStoreForTests("matrix", options), |
| 23 | + }, |
| 24 | + } as unknown as PluginRuntime); |
| 25 | + }); |
| 26 | + |
10 | 27 | afterEach(() => { |
11 | 28 | vi.restoreAllMocks(); |
12 | 29 | vi.useRealTimers(); |
| 30 | + resetPluginStateStoreForTests(); |
13 | 31 | for (const dir of tempDirs.splice(0)) { |
14 | 32 | fs.rmSync(dir, { recursive: true, force: true }); |
15 | 33 | } |
@@ -125,22 +143,73 @@ describe("Matrix inbound event dedupe", () => { |
125 | 143 | expect(second.claimEvent({ roomId: "!room:example.org", eventId: "$backlog" })).toBe(false); |
126 | 144 | }); |
127 | 145 |
|
128 | | - it("treats stop persistence failures as best-effort cleanup", async () => { |
129 | | - const blockingPath = createStoragePath(); |
130 | | - fs.writeFileSync(blockingPath, "blocking file", "utf8"); |
131 | | - const deduper = await createMatrixInboundEventDeduper({ |
| 146 | + it("imports legacy JSON entries into plugin state", async () => { |
| 147 | + const storagePath = createStoragePath(); |
| 148 | + fs.writeFileSync( |
| 149 | + storagePath, |
| 150 | + JSON.stringify({ |
| 151 | + version: 1, |
| 152 | + entries: [{ key: "!room:example.org|$legacy", ts: 90 }], |
| 153 | + }), |
| 154 | + "utf8", |
| 155 | + ); |
| 156 | + |
| 157 | + const first = await createMatrixInboundEventDeduper({ |
132 | 158 | auth: auth as never, |
133 | | - storagePath: path.join(blockingPath, "nested", "inbound-dedupe.json"), |
| 159 | + storagePath, |
| 160 | + ttlMs: 20, |
| 161 | + nowMs: () => 100, |
134 | 162 | }); |
| 163 | + expect(first.claimEvent({ roomId: "!room:example.org", eventId: "$legacy" })).toBe(false); |
135 | 164 |
|
136 | | - expect(deduper.claimEvent({ roomId: "!room:example.org", eventId: "$persist-fail" })).toBe( |
137 | | - true, |
138 | | - ); |
139 | | - await deduper.commitEvent({ |
140 | | - roomId: "!room:example.org", |
141 | | - eventId: "$persist-fail", |
| 165 | + fs.rmSync(storagePath, { force: true }); |
| 166 | + resetPluginStateStoreForTests(); |
| 167 | + const second = await createMatrixInboundEventDeduper({ |
| 168 | + auth: auth as never, |
| 169 | + storagePath, |
| 170 | + ttlMs: 20, |
| 171 | + nowMs: () => 100, |
| 172 | + }); |
| 173 | + expect(second.claimEvent({ roomId: "!room:example.org", eventId: "$legacy" })).toBe(false); |
| 174 | + }); |
| 175 | + |
| 176 | + it("keeps committed events in memory when plugin-state persistence fails", async () => { |
| 177 | + const storagePath = createStoragePath(); |
| 178 | + const warnSpy = vi.spyOn(LogService, "warn").mockImplementation(() => {}); |
| 179 | + setMatrixRuntime({ |
| 180 | + state: { |
| 181 | + openKeyedStore: () => ({ |
| 182 | + entries: async () => [], |
| 183 | + register: async () => { |
| 184 | + throw new Error("sqlite unavailable"); |
| 185 | + }, |
| 186 | + registerIfAbsent: async () => false, |
| 187 | + lookup: async () => undefined, |
| 188 | + consume: async () => undefined, |
| 189 | + delete: async () => false, |
| 190 | + clear: async () => {}, |
| 191 | + }), |
| 192 | + }, |
| 193 | + } as unknown as PluginRuntime); |
| 194 | + const deduper = await createMatrixInboundEventDeduper({ |
| 195 | + auth: auth as never, |
| 196 | + storagePath, |
142 | 197 | }); |
143 | 198 |
|
144 | | - await expect(deduper.stop()).resolves.toBeUndefined(); |
| 199 | + expect(deduper.claimEvent({ roomId: "!room:example.org", eventId: "$best-effort" })).toBe(true); |
| 200 | + await expect( |
| 201 | + deduper.commitEvent({ |
| 202 | + roomId: "!room:example.org", |
| 203 | + eventId: "$best-effort", |
| 204 | + }), |
| 205 | + ).resolves.toBeUndefined(); |
| 206 | + expect(deduper.claimEvent({ roomId: "!room:example.org", eventId: "$best-effort" })).toBe( |
| 207 | + false, |
| 208 | + ); |
| 209 | + expect(warnSpy).toHaveBeenCalledWith( |
| 210 | + "MatrixInboundDedupe", |
| 211 | + "Failed persisting Matrix inbound dedupe entry:", |
| 212 | + expect.any(Error), |
| 213 | + ); |
145 | 214 | }); |
146 | 215 | }); |
0 commit comments