




















1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5+import { loadSessionStore, type SessionEntry } from "../config/sessions.js";
6+import type { OpenClawConfig } from "../config/types.openclaw.js";
7+8+const state = vi.hoisted(() => ({
9+cfg: undefined as OpenClawConfig | undefined,
10+workspaceDir: undefined as string | undefined,
11+agentDir: undefined as string | undefined,
12+runAgentAttemptMock: vi.fn(),
13+deliveryFreshEntries: [] as Array<SessionEntry | undefined>,
14+}));
15+16+vi.mock("../config/io.js", () => ({
17+getRuntimeConfig: () => state.cfg,
18+readConfigFileSnapshotForWrite: async () => ({ snapshot: { valid: false } }),
19+}));
20+21+vi.mock("./agent-runtime-config.js", () => ({
22+resolveAgentRuntimeConfig: async () => ({
23+loadedRaw: state.cfg,
24+sourceConfig: state.cfg,
25+cfg: state.cfg,
26+}),
27+}));
28+29+vi.mock("./agent-scope.js", async () => {
30+const actual = await vi.importActual<typeof import("./agent-scope.js")>("./agent-scope.js");
31+return {
32+ ...actual,
33+clearAutoFallbackPrimaryProbeSelection: vi.fn(),
34+entryMatchesAutoFallbackPrimaryProbe: () => false,
35+hasSessionAutoModelFallbackProvenance: () => false,
36+listAgentIds: () => ["main"],
37+markAutoFallbackPrimaryProbe: vi.fn(),
38+resolveAutoFallbackPrimaryProbe: () => undefined,
39+resolveAgentConfig: () => undefined,
40+resolveAgentDir: () => state.agentDir ?? "/tmp/openclaw-agent",
41+resolveDefaultAgentId: () => "main",
42+resolveEffectiveModelFallbacks: () => undefined,
43+resolveSessionAgentId: () => "main",
44+resolveAgentWorkspaceDir: () => state.workspaceDir ?? "/tmp/openclaw-workspace",
45+};
46+});
47+48+vi.mock("../plugins/manifest-contract-eligibility.js", () => ({
49+loadManifestMetadataSnapshot: () => ({ plugins: [] }),
50+}));
51+52+vi.mock("./model-catalog.js", () => ({
53+loadManifestModelCatalog: () => [],
54+}));
55+56+vi.mock("./harness/runtime-plugin.js", () => ({
57+ensureSelectedAgentHarnessPlugin: vi.fn(async () => undefined),
58+}));
59+60+vi.mock("./workspace.js", () => ({
61+ensureAgentWorkspace: vi.fn(async () => undefined),
62+}));
63+64+vi.mock("./auth-profiles/store.js", async () => {
65+const actual = await vi.importActual<typeof import("./auth-profiles/store.js")>(
66+"./auth-profiles/store.js",
67+);
68+return {
69+ ...actual,
70+ensureAuthProfileStore: () => ({ profiles: {} }),
71+saveAuthProfileStore: vi.fn(),
72+updateAuthProfileStoreWithLock: vi.fn(async () => ({ profiles: {} })),
73+};
74+});
75+76+vi.mock("../acp/control-plane/manager.js", () => ({
77+getAcpSessionManager: () => ({
78+resolveSession: () => null,
79+}),
80+}));
81+82+vi.mock("../skills/runtime/remote.js", () => ({
83+getRemoteSkillEligibility: () => ({ enabled: false, reason: "test" }),
84+}));
85+86+vi.mock("../skills/runtime/session-snapshot.js", () => ({
87+resolveReusableWorkspaceSkillSnapshot: () => ({
88+shouldRefresh: true,
89+snapshot: {
90+prompt: "",
91+skills: [],
92+resolvedSkills: [],
93+version: 0,
94+},
95+}),
96+}));
97+98+vi.mock("./exec-defaults.js", () => ({
99+canExecRequestNode: () => false,
100+}));
101+102+vi.mock("./model-fallback.js", () => ({
103+runWithModelFallback: async (params: {
104+provider: string;
105+model: string;
106+run: (provider: string, model: string) => Promise<unknown>;
107+}) => ({
108+result: await params.run(params.provider, params.model),
109+provider: params.provider,
110+model: params.model,
111+attempts: [],
112+}),
113+}));
114+115+vi.mock("./command/attempt-execution.runtime.js", async () => {
116+const actual = await vi.importActual<typeof import("./command/attempt-execution.runtime.js")>(
117+"./command/attempt-execution.runtime.js",
118+);
119+return {
120+ ...actual,
121+runAgentAttempt: (...args: unknown[]) => state.runAgentAttemptMock(...args),
122+};
123+});
124+125+vi.mock("./command/cli-compaction.js", () => ({
126+runCliTurnCompactionLifecycle: async (params: { sessionEntry?: SessionEntry }) =>
127+params.sessionEntry,
128+}));
129+130+vi.mock("./command/delivery.runtime.js", () => ({
131+deliverAgentCommandResult: async (params: {
132+resolveFreshSessionEntryForDelivery?: () => Promise<SessionEntry | undefined>;
133+}) => {
134+state.deliveryFreshEntries.push(await params.resolveFreshSessionEntryForDelivery?.());
135+return { deliverySucceeded: true };
136+},
137+}));
138+139+let agentCommand: typeof import("./agent-command.js").agentCommand;
140+141+beforeEach(async () => {
142+vi.clearAllMocks();
143+state.deliveryFreshEntries = [];
144+const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-rotation-e2e-"));
145+state.workspaceDir = path.join(tmpDir, "workspace");
146+state.agentDir = path.join(tmpDir, "agent");
147+await fs.mkdir(state.workspaceDir, { recursive: true });
148+await fs.mkdir(state.agentDir, { recursive: true });
149+state.cfg = {
150+session: {
151+store: path.join(tmpDir, "sessions.json"),
152+},
153+agents: {
154+defaults: {
155+models: {
156+"openai/gpt-5.5": {},
157+},
158+},
159+},
160+} as OpenClawConfig;
161+agentCommand ??= (await import("./agent-command.js")).agentCommand;
162+});
163+164+afterEach(async () => {
165+const storePath = state.cfg?.session?.store;
166+state.cfg = undefined;
167+state.workspaceDir = undefined;
168+state.agentDir = undefined;
169+if (storePath) {
170+await fs.rm(path.dirname(storePath), { recursive: true, force: true });
171+}
172+});
173+174+function makeResult(params: {
175+sessionId: string;
176+sessionFile?: string;
177+text: string;
178+compactionCount?: number;
179+}) {
180+return {
181+payloads: [{ text: params.text }],
182+meta: {
183+durationMs: 1,
184+stopReason: "end_turn",
185+executionTrace: {
186+runner: "embedded",
187+fallbackUsed: false,
188+winnerProvider: "openai",
189+winnerModel: "gpt-5.5",
190+},
191+finalAssistantVisibleText: params.text,
192+agentMeta: {
193+sessionId: params.sessionId,
194+ ...(params.sessionFile ? { sessionFile: params.sessionFile } : {}),
195+provider: "openai",
196+model: "gpt-5.5",
197+ ...(params.compactionCount ? { compactionCount: params.compactionCount } : {}),
198+},
199+},
200+};
201+}
202+203+async function readSessionMessages(sessionFile: string) {
204+const raw = await fs.readFile(sessionFile, "utf-8");
205+return raw
206+.split(/\r?\n/)
207+.filter(Boolean)
208+.map((line) => JSON.parse(line) as { type?: string; message?: { role?: string } })
209+.filter((entry) => entry.type === "message")
210+.map((entry) => entry.message);
211+}
212+213+describe("agentCommand compaction transcript rotation", () => {
214+it("keeps sessions.json on the rotated successor and resumes the next turn from it", async () => {
215+const storePath = state.cfg?.session?.store;
216+if (!storePath) {
217+throw new Error("missing test session store path");
218+}
219+const sessionsDir = await fs.realpath(path.dirname(storePath));
220+const rotatedSessionFile = path.join(sessionsDir, "rotated-session.jsonl");
221+state.runAgentAttemptMock
222+.mockResolvedValueOnce(
223+makeResult({
224+sessionId: "rotated-session",
225+sessionFile: rotatedSessionFile,
226+text: "first answer after rotation",
227+compactionCount: 1,
228+}),
229+)
230+.mockResolvedValueOnce(
231+makeResult({
232+sessionId: "rotated-session",
233+text: "second answer",
234+}),
235+);
236+237+await agentCommand({
238+message: "first prompt",
239+sessionId: "old-session",
240+cwd: state.workspaceDir,
241+});
242+243+const storeAfterRotation = loadSessionStore(storePath, { skipCache: true });
244+const entriesAfterRotation = Object.entries(storeAfterRotation);
245+expect(entriesAfterRotation).toHaveLength(1);
246+const [sessionKey, rotatedEntry] = entriesAfterRotation[0] ?? [];
247+expect(sessionKey).toBe("agent:main:explicit:old-session");
248+expect(rotatedEntry).toMatchObject({
249+sessionId: "rotated-session",
250+sessionFile: rotatedSessionFile,
251+usageFamilyKey: "agent:main:explicit:old-session",
252+usageFamilySessionIds: ["old-session", "rotated-session"],
253+compactionCount: 1,
254+});
255+await expect(readSessionMessages(rotatedSessionFile)).resolves.toEqual([
256+expect.objectContaining({ role: "assistant" }),
257+]);
258+259+await agentCommand({
260+message: "second prompt",
261+sessionId: "rotated-session",
262+cwd: state.workspaceDir,
263+});
264+265+const secondAttempt = state.runAgentAttemptMock.mock.calls[1]?.[0] as
266+| { sessionId?: string; sessionFile?: string; sessionKey?: string }
267+| undefined;
268+expect(secondAttempt).toMatchObject({
269+sessionId: "rotated-session",
270+ sessionKey,
271+sessionFile: rotatedSessionFile,
272+});
273+expect(state.deliveryFreshEntries.at(-1)).toMatchObject({
274+sessionId: "rotated-session",
275+sessionFile: rotatedSessionFile,
276+});
277+expect(loadSessionStore(storePath, { skipCache: true })[sessionKey ?? ""]).toMatchObject({
278+sessionId: "rotated-session",
279+sessionFile: rotatedSessionFile,
280+});
281+});
282+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。