test(cli): cover transcript compaction reseed

This commit is contained in:
Ayaan Zaidi
2026-04-26 08:31:59 +05:30
parent dfd5940c34
commit 3eff589ac0
3 changed files with 250 additions and 0 deletions

View File

@@ -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<next_user_message>\nhi\n</next_user_message>",
}),
);
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",

View File

@@ -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("<next_user_message>\nnew ask\n</next_user_message>");
});
it("skips reseed text when the transcript has no renderable conversation", () => {
expect(
buildCliSessionHistoryPrompt({
messages: [{ role: "tool", content: "ignored" }],
prompt: "new ask",
}),
).toBeUndefined();
});
});

View File

@@ -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<Parameters<ContextEngine["compact"]>[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<string, SessionEntry> = { [sessionKey]: sessionEntry };
await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
const compactCalls: Array<Parameters<ContextEngine["compact"]>[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();
});
});