























@@ -0,0 +1,297 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { expect, test } from "vitest";
5+import { withEnvAsync } from "../test-utils/env.js";
6+import {
7+embeddedRunMock,
8+piSdkMock,
9+rpcReq,
10+startConnectedServerWithClient,
11+writeSessionStore,
12+} from "./test-helpers.js";
13+import {
14+setupGatewaySessionsTestHarness,
15+getSessionManagerModule,
16+getGatewayConfigModule,
17+sessionStoreEntry,
18+createCheckpointFixture,
19+} from "./test/server-sessions-helpers.js";
20+21+const { createSessionStoreDir, openClient } = setupGatewaySessionsTestHarness();
22+23+test("sessions.compaction.* lists checkpoints and branches or restores from pre-compaction snapshots", async () => {
24+const { dir, storePath } = await createSessionStoreDir();
25+const fixture = await createCheckpointFixture(dir);
26+const { SessionManager } = await getSessionManagerModule();
27+await writeSessionStore({
28+entries: {
29+main: sessionStoreEntry(fixture.sessionId, {
30+sessionFile: fixture.sessionFile,
31+compactionCheckpoints: [
32+{
33+checkpointId: "checkpoint-1",
34+sessionKey: "agent:main:main",
35+sessionId: fixture.sessionId,
36+createdAt: Date.now(),
37+reason: "manual",
38+tokensBefore: 123,
39+tokensAfter: 45,
40+summary: "checkpoint summary",
41+firstKeptEntryId: fixture.preCompactionLeafId,
42+preCompaction: {
43+sessionId: fixture.preCompactionSession.getSessionId(),
44+sessionFile: fixture.preCompactionSessionFile,
45+leafId: fixture.preCompactionLeafId,
46+},
47+postCompaction: {
48+sessionId: fixture.sessionId,
49+sessionFile: fixture.sessionFile,
50+leafId: fixture.postCompactionLeafId,
51+entryId: fixture.postCompactionLeafId,
52+},
53+},
54+],
55+}),
56+},
57+});
58+59+const { ws } = await openClient();
60+61+const listedSessions = await rpcReq<{
62+sessions: Array<{
63+key: string;
64+compactionCheckpointCount?: number;
65+latestCompactionCheckpoint?: {
66+checkpointId: string;
67+reason: string;
68+tokensBefore?: number;
69+tokensAfter?: number;
70+};
71+}>;
72+}>(ws, "sessions.list", {});
73+expect(listedSessions.ok).toBe(true);
74+const main = listedSessions.payload?.sessions.find(
75+(session) => session.key === "agent:main:main",
76+);
77+expect(main?.compactionCheckpointCount).toBe(1);
78+expect(main?.latestCompactionCheckpoint?.checkpointId).toBe("checkpoint-1");
79+expect(main?.latestCompactionCheckpoint?.reason).toBe("manual");
80+81+const listedCheckpoints = await rpcReq<{
82+ok: true;
83+key: string;
84+checkpoints: Array<{ checkpointId: string; summary?: string; tokensBefore?: number }>;
85+}>(ws, "sessions.compaction.list", { key: "main" });
86+expect(listedCheckpoints.ok).toBe(true);
87+expect(listedCheckpoints.payload?.key).toBe("agent:main:main");
88+expect(listedCheckpoints.payload?.checkpoints).toHaveLength(1);
89+expect(listedCheckpoints.payload?.checkpoints[0]).toMatchObject({
90+checkpointId: "checkpoint-1",
91+summary: "checkpoint summary",
92+tokensBefore: 123,
93+});
94+95+const checkpoint = await rpcReq<{
96+ok: true;
97+key: string;
98+checkpoint: { checkpointId: string; preCompaction: { sessionFile: string } };
99+}>(ws, "sessions.compaction.get", {
100+key: "main",
101+checkpointId: "checkpoint-1",
102+});
103+expect(checkpoint.ok).toBe(true);
104+expect(checkpoint.payload?.checkpoint.checkpointId).toBe("checkpoint-1");
105+expect(checkpoint.payload?.checkpoint.preCompaction.sessionFile).toBe(
106+fixture.preCompactionSessionFile,
107+);
108+109+const branched = await rpcReq<{
110+ok: true;
111+sourceKey: string;
112+key: string;
113+entry: { sessionId: string; sessionFile?: string; parentSessionKey?: string };
114+}>(ws, "sessions.compaction.branch", {
115+key: "main",
116+checkpointId: "checkpoint-1",
117+});
118+expect(branched.ok).toBe(true);
119+expect(branched.payload?.sourceKey).toBe("agent:main:main");
120+expect(branched.payload?.entry.parentSessionKey).toBe("agent:main:main");
121+const branchedSessionFile = branched.payload?.entry.sessionFile;
122+expect(branchedSessionFile).toBeTruthy();
123+const branchedSession = SessionManager.open(branchedSessionFile!, dir);
124+expect(branchedSession.getEntries()).toHaveLength(
125+fixture.preCompactionSession.getEntries().length,
126+);
127+128+const storeAfterBranch = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
129+string,
130+{
131+parentSessionKey?: string;
132+compactionCheckpoints?: unknown[];
133+sessionId?: string;
134+}
135+>;
136+const branchedEntry = storeAfterBranch[branched.payload!.key];
137+expect(branchedEntry?.parentSessionKey).toBe("agent:main:main");
138+expect(branchedEntry?.compactionCheckpoints).toBeUndefined();
139+140+const restored = await rpcReq<{
141+ok: true;
142+key: string;
143+sessionId: string;
144+entry: { sessionId: string; sessionFile?: string; compactionCheckpoints?: unknown[] };
145+}>(ws, "sessions.compaction.restore", {
146+key: "main",
147+checkpointId: "checkpoint-1",
148+});
149+expect(restored.ok).toBe(true);
150+expect(restored.payload?.key).toBe("agent:main:main");
151+expect(restored.payload?.sessionId).not.toBe(fixture.sessionId);
152+expect(restored.payload?.entry.compactionCheckpoints).toHaveLength(1);
153+const restoredSessionFile = restored.payload?.entry.sessionFile;
154+expect(restoredSessionFile).toBeTruthy();
155+const restoredSession = SessionManager.open(restoredSessionFile!, dir);
156+expect(restoredSession.getEntries()).toHaveLength(
157+fixture.preCompactionSession.getEntries().length,
158+);
159+160+const storeAfterRestore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
161+string,
162+{ compactionCheckpoints?: unknown[]; sessionId?: string }
163+>;
164+expect(storeAfterRestore["agent:main:main"]?.sessionId).toBe(restored.payload?.sessionId);
165+expect(storeAfterRestore["agent:main:main"]?.compactionCheckpoints).toHaveLength(1);
166+167+ws.close();
168+});
169+170+test("sessions.compact without maxLines runs embedded manual compaction for checkpoint-capable flows", async () => {
171+const { dir, storePath } = await createSessionStoreDir();
172+await fs.writeFile(
173+path.join(dir, "sess-main.jsonl"),
174+`${JSON.stringify({ role: "user", content: "hello" })}\n`,
175+"utf-8",
176+);
177+await writeSessionStore({
178+entries: {
179+main: sessionStoreEntry("sess-main", {
180+thinkingLevel: "medium",
181+reasoningLevel: "stream",
182+}),
183+},
184+});
185+186+const { ws } = await openClient();
187+const compacted = await rpcReq<{
188+ok: true;
189+key: string;
190+compacted: boolean;
191+result?: { tokensAfter?: number };
192+}>(ws, "sessions.compact", {
193+key: "main",
194+});
195+196+expect(compacted.ok).toBe(true);
197+expect(compacted.payload?.key).toBe("agent:main:main");
198+expect(compacted.payload?.compacted).toBe(true);
199+expect(embeddedRunMock.compactEmbeddedPiSession).toHaveBeenCalledTimes(1);
200+expect(embeddedRunMock.compactEmbeddedPiSession).toHaveBeenCalledWith(
201+expect.objectContaining({
202+sessionId: "sess-main",
203+sessionKey: "agent:main:main",
204+sessionFile: expect.stringMatching(/sess-main\.jsonl$/),
205+config: expect.any(Object),
206+provider: expect.any(String),
207+model: expect.any(String),
208+thinkLevel: "medium",
209+reasoningLevel: "stream",
210+trigger: "manual",
211+}),
212+);
213+214+const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
215+string,
216+{ compactionCount?: number; totalTokens?: number; totalTokensFresh?: boolean }
217+>;
218+expect(store["agent:main:main"]?.compactionCount).toBe(1);
219+expect(store["agent:main:main"]?.totalTokens).toBe(80);
220+expect(store["agent:main:main"]?.totalTokensFresh).toBe(true);
221+222+ws.close();
223+});
224+225+test("sessions.patch preserves nested model ids under provider overrides", async () => {
226+const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-sessions-nested-"));
227+const storePath = path.join(dir, "sessions.json");
228+await fs.writeFile(
229+storePath,
230+JSON.stringify({
231+"agent:main:main": sessionStoreEntry("sess-main"),
232+}),
233+"utf-8",
234+);
235+236+await withEnvAsync({ OPENCLAW_CONFIG_PATH: undefined }, async () => {
237+const { clearConfigCache, clearRuntimeConfigSnapshot } = await getGatewayConfigModule();
238+clearConfigCache();
239+clearRuntimeConfigSnapshot();
240+const cfg = {
241+session: { store: storePath, mainKey: "main" },
242+agents: {
243+defaults: {
244+model: { primary: "openai/gpt-test-a" },
245+},
246+list: [{ id: "main", default: true, workspace: dir }],
247+},
248+};
249+const configPath = path.join(dir, "openclaw.json");
250+await fs.writeFile(configPath, JSON.stringify(cfg, null, 2), "utf-8");
251+252+await withEnvAsync({ OPENCLAW_CONFIG_PATH: configPath }, async () => {
253+const started = await startConnectedServerWithClient();
254+const { server, ws } = started;
255+try {
256+piSdkMock.enabled = true;
257+piSdkMock.models = [
258+{ id: "moonshotai/kimi-k2.5", name: "Kimi K2.5 (NVIDIA)", provider: "nvidia" },
259+];
260+261+const patched = await rpcReq<{
262+ok: true;
263+entry: {
264+modelOverride?: string;
265+providerOverride?: string;
266+model?: string;
267+modelProvider?: string;
268+};
269+resolved?: { model?: string; modelProvider?: string };
270+}>(ws, "sessions.patch", {
271+key: "agent:main:main",
272+model: "nvidia/moonshotai/kimi-k2.5",
273+});
274+expect(patched.ok).toBe(true);
275+expect(patched.payload?.entry.modelOverride).toBe("moonshotai/kimi-k2.5");
276+expect(patched.payload?.entry.providerOverride).toBe("nvidia");
277+expect(patched.payload?.entry.model).toBeUndefined();
278+expect(patched.payload?.entry.modelProvider).toBeUndefined();
279+expect(patched.payload?.resolved?.modelProvider).toBe("nvidia");
280+expect(patched.payload?.resolved?.model).toBe("moonshotai/kimi-k2.5");
281+282+const listed = await rpcReq<{
283+sessions: Array<{ key: string; modelProvider?: string; model?: string }>;
284+}>(ws, "sessions.list", {});
285+expect(listed.ok).toBe(true);
286+const mainSession = listed.payload?.sessions.find(
287+(session) => session.key === "agent:main:main",
288+);
289+expect(mainSession?.modelProvider).toBe("nvidia");
290+expect(mainSession?.model).toBe("moonshotai/kimi-k2.5");
291+} finally {
292+ws.close();
293+await server.close();
294+}
295+});
296+});
297+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。