From 3eff589ac085adc346f40f044c52ecfd97209236 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sun, 26 Apr 2026 08:31:59 +0530 Subject: [PATCH] test(cli): cover transcript compaction reseed --- src/agents/cli-runner.reliability.test.ts | 54 ++++++ src/agents/cli-runner/session-history.test.ts | 26 +++ src/agents/command/cli-compaction.test.ts | 170 ++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 src/agents/command/cli-compaction.test.ts diff --git a/src/agents/cli-runner.reliability.test.ts b/src/agents/cli-runner.reliability.test.ts index 56d3fd725d2..ec71f60e14a 100644 --- a/src/agents/cli-runner.reliability.test.ts +++ b/src/agents/cli-runner.reliability.test.ts @@ -74,6 +74,7 @@ function buildPreparedContext(params?: { sessionKey?: string; cliSessionId?: string; runId?: string; + openClawHistoryPrompt?: string; }): PreparedCliRunContext { const backend = { command: "codex", @@ -115,6 +116,9 @@ function buildPreparedContext(params?: { systemPrompt: "You are a helpful assistant.", systemPromptReport: {} as PreparedCliRunContext["systemPromptReport"], bootstrapPromptWarningLines: [], + ...(params?.openClawHistoryPrompt + ? { openClawHistoryPrompt: params.openClawHistoryPrompt } + : {}), authEpochVersion: 2, }; } @@ -324,6 +328,56 @@ describe("runCliAgent reliability", () => { }); }); + it("seeds fresh CLI sessions from the OpenClaw transcript", async () => { + supervisorSpawnMock.mockResolvedValueOnce( + createManagedRun({ + reason: "exit", + exitCode: 0, + exitSignal: null, + durationMs: 50, + stdout: "hello from cli", + stderr: "", + timedOut: false, + noOutputTimedOut: false, + }), + ); + + const result = await runPreparedCliAgent( + buildPreparedContext({ + openClawHistoryPrompt: + "Continue this conversation using the OpenClaw transcript below.\n\nUser: earlier ask\n\nAssistant: earlier answer\n\n\nhi\n", + }), + ); + + expect(result.meta.finalPromptText).toContain("User: earlier ask"); + expect(result.meta.finalPromptText).toContain("Assistant: earlier answer"); + }); + + it("keeps resumed CLI sessions on native resume history", async () => { + supervisorSpawnMock.mockResolvedValueOnce( + createManagedRun({ + reason: "exit", + exitCode: 0, + exitSignal: null, + durationMs: 50, + stdout: "hello from cli", + stderr: "", + timedOut: false, + noOutputTimedOut: false, + }), + ); + + const result = await runPreparedCliAgent( + buildPreparedContext({ + cliSessionId: "cli-session", + openClawHistoryPrompt: "User: earlier ask", + }), + ); + + expect(result.meta.finalPromptText).not.toContain("User: earlier ask"); + expect(result.meta.finalPromptText).toContain("hi"); + }); + it("reports CLI reply backends as streaming until the managed run finishes", async () => { const operation = createReplyOperation({ sessionKey: "agent:main:main", diff --git a/src/agents/cli-runner/session-history.test.ts b/src/agents/cli-runner/session-history.test.ts index b26949730c4..32bbff89d19 100644 --- a/src/agents/cli-runner/session-history.test.ts +++ b/src/agents/cli-runner/session-history.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent"; import { afterEach, describe, expect, it, vi } from "vitest"; import { + buildCliSessionHistoryPrompt, loadCliSessionHistoryMessages, MAX_CLI_SESSION_HISTORY_FILE_BYTES, MAX_CLI_SESSION_HISTORY_MESSAGES, @@ -218,3 +219,28 @@ describe("loadCliSessionHistoryMessages", () => { } }); }); + +describe("buildCliSessionHistoryPrompt", () => { + it("renders OpenClaw transcript history around the next user message", () => { + const prompt = buildCliSessionHistoryPrompt({ + messages: [ + { role: "user", content: "old ask" }, + { role: "assistant", content: [{ type: "text", text: "old answer" }] }, + ], + prompt: "new ask", + }); + + expect(prompt).toContain("User: old ask"); + expect(prompt).toContain("Assistant: old answer"); + expect(prompt).toContain("\nnew ask\n"); + }); + + it("skips reseed text when the transcript has no renderable conversation", () => { + expect( + buildCliSessionHistoryPrompt({ + messages: [{ role: "tool", content: "ignored" }], + prompt: "new ask", + }), + ).toBeUndefined(); + }); +}); diff --git a/src/agents/command/cli-compaction.test.ts b/src/agents/command/cli-compaction.test.ts new file mode 100644 index 00000000000..c83c5f64f1b --- /dev/null +++ b/src/agents/command/cli-compaction.test.ts @@ -0,0 +1,170 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { SessionEntry } from "../../config/sessions/types.js"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { ContextEngine } from "../../context-engine/types.js"; +import { + resetCliCompactionTestDeps, + runCliTurnCompactionLifecycle, + setCliCompactionTestDeps, +} from "./cli-compaction.js"; + +function buildContextEngine(params: { + compactCalls: Array[0]>; +}): ContextEngine { + return { + info: { + id: "legacy", + name: "Legacy Context Engine", + }, + async ingest() { + return { ingested: false }; + }, + async assemble(assembleParams) { + return { messages: assembleParams.messages, estimatedTokens: 0 }; + }, + async compact(compactParams) { + params.compactCalls.push(compactParams); + return { + ok: true, + compacted: true, + result: { + summary: "compacted", + tokensBefore: compactParams.currentTokenCount ?? 0, + tokensAfter: 100, + }, + }; + }, + }; +} + +async function writeSessionFile(params: { sessionFile: string; sessionId: string }) { + await fs.mkdir(path.dirname(params.sessionFile), { recursive: true }); + await fs.writeFile( + params.sessionFile, + [ + JSON.stringify({ + type: "session", + version: CURRENT_SESSION_VERSION, + id: params.sessionId, + timestamp: new Date(0).toISOString(), + cwd: path.dirname(params.sessionFile), + }), + JSON.stringify({ + type: "message", + message: { role: "user", content: "old ask", timestamp: 1 }, + }), + JSON.stringify({ + type: "message", + message: { + role: "assistant", + content: [{ type: "text", text: "old answer" }], + timestamp: 2, + }, + }), + "", + ].join("\n"), + "utf-8", + ); +} + +describe("runCliTurnCompactionLifecycle", () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-compaction-")); + }); + + afterEach(async () => { + resetCliCompactionTestDeps(); + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + it("compacts over-budget CLI transcripts and clears external CLI resume state", async () => { + const sessionKey = "agent:main:cli"; + const sessionId = "session-cli"; + const sessionFile = path.join(tmpDir, "session.jsonl"); + const storePath = path.join(tmpDir, "sessions.json"); + await writeSessionFile({ sessionFile, sessionId }); + + const sessionEntry: SessionEntry = { + sessionId, + updatedAt: Date.now(), + sessionFile, + contextTokens: 1_000, + totalTokens: 950, + totalTokensFresh: true, + cliSessionBindings: { + "claude-cli": { sessionId: "claude-session" }, + }, + cliSessionIds: { + "claude-cli": "claude-session", + }, + claudeCliSessionId: "claude-session", + }; + const sessionStore: Record = { [sessionKey]: sessionEntry }; + await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); + + const compactCalls: Array[0]> = []; + const maintenance = vi.fn(async () => ({ changed: false, bytesFreed: 0, rewrittenEntries: 0 })); + setCliCompactionTestDeps({ + resolveContextEngine: async () => buildContextEngine({ compactCalls }), + createPreparedEmbeddedPiSettingsManager: async () => ({ + getCompactionReserveTokens: () => 200, + getCompactionKeepRecentTokens: () => 0, + applyOverrides: () => {}, + }), + shouldPreemptivelyCompactBeforePrompt: () => ({ + route: "fits", + shouldCompact: false, + estimatedPromptTokens: 600, + promptBudgetBeforeReserve: 800, + overflowTokens: 0, + toolResultReducibleChars: 0, + effectiveReserveTokens: 200, + }), + resolveLiveToolResultMaxChars: () => 20_000, + runContextEngineMaintenance: maintenance, + }); + + const updatedEntry = await runCliTurnCompactionLifecycle({ + cfg: {} as OpenClawConfig, + sessionId, + sessionKey, + sessionEntry, + sessionStore, + storePath, + sessionAgentId: "main", + workspaceDir: tmpDir, + agentDir: tmpDir, + provider: "claude-cli", + model: "opus", + }); + + expect(compactCalls).toHaveLength(1); + expect(compactCalls[0]).toMatchObject({ + sessionId, + sessionKey, + sessionFile, + tokenBudget: 1_000, + currentTokenCount: 950, + force: true, + compactionTarget: "budget", + }); + expect(maintenance).toHaveBeenCalledWith( + expect.objectContaining({ + reason: "compaction", + sessionId, + sessionKey, + sessionFile, + }), + ); + expect(updatedEntry?.compactionCount).toBe(1); + expect(updatedEntry?.cliSessionBindings?.["claude-cli"]).toBeUndefined(); + expect(updatedEntry?.cliSessionIds?.["claude-cli"]).toBeUndefined(); + expect(updatedEntry?.claudeCliSessionId).toBeUndefined(); + }); +});