






















11import fs from "node:fs/promises";
22import path from "node:path";
3-import { describe, expect, it } from "vitest";
3+import { afterEach, describe, expect, it } from "vitest";
4+import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
45import { withTempDir } from "../test-helpers/temp-dir.js";
5-import { createFileAcpEventLedger, createInMemoryAcpEventLedger } from "./event-ledger.js";
6+import {
7+createFileAcpEventLedger,
8+createInMemoryAcpEventLedger,
9+createSqliteAcpEventLedger,
10+migrateFileAcpEventLedgerToSqlite,
11+} from "./event-ledger.js";
612713describe("ACP event ledger", () => {
14+afterEach(() => {
15+closeOpenClawStateDatabaseForTest();
16+});
17+818it("records complete in-memory session updates in sequence", async () => {
919const ledger = createInMemoryAcpEventLedger({ now: () => 123 });
1020await ledger.startSession({
@@ -142,6 +152,123 @@ describe("ACP event ledger", () => {
142152});
143153});
144154155+it("persists SQLite-backed replay state across ledger instances", async () => {
156+await withTempDir({ prefix: "openclaw-acp-ledger-" }, async (dir) => {
157+const databasePath = path.join(dir, "openclaw.sqlite");
158+const first = createSqliteAcpEventLedger({ path: databasePath, now: () => 1000 });
159+await first.startSession({
160+sessionId: "session-1",
161+sessionKey: "agent:main:work",
162+cwd: "/work",
163+complete: true,
164+});
165+await first.recordUpdate({
166+sessionId: "session-1",
167+sessionKey: "agent:main:work",
168+runId: "run-1",
169+update: {
170+sessionUpdate: "agent_thought_chunk",
171+content: { type: "text", text: "Thinking" },
172+},
173+});
174+175+closeOpenClawStateDatabaseForTest();
176+const second = createSqliteAcpEventLedger({ path: databasePath });
177+const replay = await second.readReplay({
178+sessionId: "session-1",
179+sessionKey: "agent:main:work",
180+});
181+182+expect(replay.complete).toBe(true);
183+expect(replay.events).toHaveLength(1);
184+expect(replay.events[0]?.update).toEqual({
185+sessionUpdate: "agent_thought_chunk",
186+content: { type: "text", text: "Thinking" },
187+});
188+});
189+});
190+191+it("imports legacy file-backed replay state into SQLite", async () => {
192+await withTempDir({ prefix: "openclaw-acp-ledger-" }, async (dir) => {
193+const filePath = path.join(dir, "acp", "event-ledger.json");
194+const databasePath = path.join(dir, "openclaw.sqlite");
195+const legacy = createFileAcpEventLedger({ filePath, now: () => 1000 });
196+await legacy.startSession({
197+sessionId: "session-1",
198+sessionKey: "agent:main:work",
199+cwd: "/work",
200+complete: true,
201+});
202+await legacy.recordUpdate({
203+sessionId: "session-1",
204+sessionKey: "agent:main:work",
205+runId: "run-1",
206+update: {
207+sessionUpdate: "agent_message_chunk",
208+content: { type: "text", text: "Answer" },
209+},
210+});
211+212+const migrated = await migrateFileAcpEventLedgerToSqlite({
213+ filePath,
214+path: databasePath,
215+archiveSource: true,
216+});
217+const sqlite = createSqliteAcpEventLedger({ path: databasePath });
218+const replay = await sqlite.readReplay({
219+sessionId: "session-1",
220+sessionKey: "agent:main:work",
221+});
222+223+expect(migrated).toEqual({
224+importedSessions: 1,
225+importedEvents: 1,
226+archived: true,
227+});
228+expect(replay.complete).toBe(true);
229+expect(replay.events[0]?.update).toEqual({
230+sessionUpdate: "agent_message_chunk",
231+content: { type: "text", text: "Answer" },
232+});
233+await expect(fs.stat(`${filePath}.migrated`)).resolves.toBeTruthy();
234+});
235+});
236+237+it("marks SQLite-backed replay incomplete when event retention truncates history", async () => {
238+await withTempDir({ prefix: "openclaw-acp-ledger-" }, async (dir) => {
239+const ledger = createSqliteAcpEventLedger({
240+path: path.join(dir, "openclaw.sqlite"),
241+maxEventsPerSession: 1,
242+});
243+await ledger.startSession({
244+sessionId: "session-1",
245+sessionKey: "agent:main:work",
246+cwd: "/work",
247+complete: true,
248+});
249+await ledger.recordUpdate({
250+sessionId: "session-1",
251+sessionKey: "agent:main:work",
252+update: {
253+sessionUpdate: "agent_message_chunk",
254+content: { type: "text", text: "First" },
255+},
256+});
257+await ledger.recordUpdate({
258+sessionId: "session-1",
259+sessionKey: "agent:main:work",
260+update: {
261+sessionUpdate: "agent_message_chunk",
262+content: { type: "text", text: "Second" },
263+},
264+});
265+266+await expect(
267+ledger.readReplay({ sessionId: "session-1", sessionKey: "agent:main:work" }),
268+).resolves.toEqual({ complete: false, events: [] });
269+});
270+});
271+145272it("can replay a complete session by Gateway session key", async () => {
146273const ledger = createInMemoryAcpEventLedger({ now: () => 1000 });
147274await ledger.startSession({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。