



















@@ -0,0 +1,196 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import type { DatabaseSync } from "node:sqlite";
5+import {
6+resolveSessionTranscriptsDirForAgent,
7+type OpenClawConfig,
8+type ResolvedMemorySearchConfig,
9+} from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
10+import type {
11+MemorySource,
12+MemorySyncProgressUpdate,
13+} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
14+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15+import { MemoryManagerSyncOps } from "./manager-sync-ops.js";
16+17+type MemoryIndexEntry = {
18+path: string;
19+absPath: string;
20+mtimeMs: number;
21+size: number;
22+hash: string;
23+content?: string;
24+};
25+26+type SyncParams = {
27+reason?: string;
28+force?: boolean;
29+sessionFiles?: string[];
30+progress?: (update: MemorySyncProgressUpdate) => void;
31+};
32+33+type SourceStateRow = { path: string; hash: string; mtime: number; size: number };
34+35+class SessionStartupCatchupHarness extends MemoryManagerSyncOps {
36+protected readonly cfg = {} as OpenClawConfig;
37+protected readonly agentId = "main";
38+protected readonly workspaceDir = "/tmp/openclaw-test-workspace";
39+protected readonly settings = {
40+sync: {
41+sessions: {
42+deltaBytes: 100_000,
43+deltaMessages: 50,
44+postCompactionForce: true,
45+},
46+},
47+} as ResolvedMemorySearchConfig;
48+protected readonly batch = {
49+enabled: false,
50+wait: false,
51+concurrency: 1,
52+pollIntervalMs: 0,
53+timeoutMs: 0,
54+};
55+protected readonly vector = { enabled: false, available: false };
56+protected readonly cache = { enabled: false };
57+protected db: DatabaseSync;
58+59+readonly syncCalls: SyncParams[] = [];
60+61+constructor(sourceRows: SourceStateRow[]) {
62+super();
63+this.sources.add("sessions");
64+this.db = {
65+prepare: () => ({
66+all: () => sourceRows,
67+get: () => undefined,
68+run: () => undefined,
69+}),
70+} as unknown as DatabaseSync;
71+}
72+73+async catchUp(): Promise<string[]> {
74+return await this.runSessionStartupCatchup();
75+}
76+77+async markStartupDirtyFiles(): Promise<string[]> {
78+return await this.markSessionStartupCatchupDirtyFiles();
79+}
80+81+getDirtySessionFiles(): string[] {
82+return Array.from(this.sessionsDirtyFiles);
83+}
84+85+isSessionsDirty(): boolean {
86+return this.sessionsDirty;
87+}
88+89+protected computeProviderKey(): string {
90+return "test";
91+}
92+93+protected async sync(params?: SyncParams): Promise<void> {
94+this.syncCalls.push(params ?? {});
95+}
96+97+protected async withTimeout<T>(
98+promise: Promise<T>,
99+_timeoutMs: number,
100+_message: string,
101+): Promise<T> {
102+return await promise;
103+}
104+105+protected getIndexConcurrency(): number {
106+return 1;
107+}
108+109+protected pruneEmbeddingCacheIfNeeded(): void {}
110+111+protected async indexFile(
112+_entry: MemoryIndexEntry,
113+_options: { source: MemorySource; content?: string },
114+): Promise<void> {}
115+}
116+117+describe("session startup catch-up", () => {
118+let stateDir = "";
119+120+beforeEach(async () => {
121+stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-startup-"));
122+vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
123+});
124+125+afterEach(async () => {
126+vi.unstubAllEnvs();
127+await fs.rm(stateDir, { recursive: true, force: true });
128+});
129+130+async function writeSessionFile(
131+name: string,
132+): Promise<{ filePath: string; size: number; mtimeMs: number }> {
133+const sessionsDir = resolveSessionTranscriptsDirForAgent("main");
134+await fs.mkdir(sessionsDir, { recursive: true });
135+const filePath = path.join(sessionsDir, name);
136+await fs.writeFile(
137+filePath,
138+JSON.stringify({ type: "message", message: { role: "user", content: "startup catchup" } }) +
139+"\n",
140+"utf-8",
141+);
142+const stat = await fs.stat(filePath);
143+return { filePath, size: stat.size, mtimeMs: stat.mtimeMs };
144+}
145+146+it("marks stale indexed session files dirty and schedules catch-up sync", async () => {
147+const session = await writeSessionFile("thread.jsonl");
148+const harness = new SessionStartupCatchupHarness([
149+{
150+path: "sessions/main/thread.jsonl",
151+hash: "old-hash",
152+mtime: session.mtimeMs - 1000,
153+size: session.size,
154+},
155+]);
156+157+await expect(harness.catchUp()).resolves.toEqual([session.filePath]);
158+expect(harness.getDirtySessionFiles()).toEqual([session.filePath]);
159+expect(harness.isSessionsDirty()).toBe(true);
160+expect(harness.syncCalls).toEqual([{ reason: "session-startup-catchup" }]);
161+});
162+163+it("can mark startup catch-up files without scheduling background sync", async () => {
164+const session = await writeSessionFile("thread.jsonl");
165+const harness = new SessionStartupCatchupHarness([
166+{
167+path: "sessions/main/thread.jsonl",
168+hash: "old-hash",
169+mtime: session.mtimeMs - 1000,
170+size: session.size,
171+},
172+]);
173+174+await expect(harness.markStartupDirtyFiles()).resolves.toEqual([session.filePath]);
175+expect(harness.getDirtySessionFiles()).toEqual([session.filePath]);
176+expect(harness.isSessionsDirty()).toBe(true);
177+expect(harness.syncCalls).toEqual([]);
178+});
179+180+it("leaves unchanged indexed session files clean", async () => {
181+const session = await writeSessionFile("thread.jsonl");
182+const harness = new SessionStartupCatchupHarness([
183+{
184+path: "sessions/main/thread.jsonl",
185+hash: "current-hash",
186+mtime: session.mtimeMs,
187+size: session.size,
188+},
189+]);
190+191+await expect(harness.catchUp()).resolves.toEqual([]);
192+expect(harness.getDirtySessionFiles()).toEqual([]);
193+expect(harness.isSessionsDirty()).toBe(false);
194+expect(harness.syncCalls).toEqual([]);
195+});
196+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。