test(agents): move recovery transcript proof to QA (#123006)

This commit is contained in:
Peter Steinberger
2026-08-12 22:04:15 -07:00
committed by GitHub
parent 3bc665f6ae
commit 6ad5d1104c
3 changed files with 91 additions and 258 deletions
@@ -17,6 +17,7 @@ scenario:
- The runtime injects the visible-answer continuation instruction after the empty turn.
- The authenticated read result survives the empty turn and canonical retry unchanged.
- The final visible reply contains the exact recovery marker.
- The host-private recovery prompt never appears in the durable transcript.
docsRefs:
- docs/help/testing.md
codeRefs:
@@ -96,6 +97,8 @@ flow:
args:
- ref: env
- ref: sessionKey
- probeText:
ref: config.retryNeedle
- if:
expr: "Boolean(env.mock)"
then:
@@ -120,9 +123,13 @@ flow:
expr: "[scenarioRequests[1], scenarioRequests[2]].every((request) => String(request.toolOutput ?? '').includes(config.evidenceNonce) && String(request.allInputText ?? '').includes(config.evidenceNonce))"
message: authenticated read nonce did not survive into both post-tool requests
- assert:
expr: "transcript.userMessageCount === 2 && transcript.assistantToolCallCounts.read === 1 && transcript.completedToolCallCounts.read === 1 && transcript.successfulToolCallCounts.read === 1 && transcript.finalText.includes(config.expectedReply)"
expr: "transcript.userMessageCount === 1 && transcript.assistantToolCallCounts.read === 1 && transcript.completedToolCallCounts.read === 1 && transcript.successfulToolCallCounts.read === 1 && transcript.finalText.includes(config.expectedReply)"
message:
expr: "`recovery transcript lost requester, read-result, or final evidence: ${JSON.stringify(transcript)}`"
- assert:
expr: "transcript.probeTextEndLine === undefined"
message:
expr: "`host-private recovery prompt leaked into the durable transcript: ${JSON.stringify(transcript)}`"
- assert:
expr: "scenarioOutbound.length === 1 && scenarioOutbound[0]?.text.trim() === config.expectedReply && !scenarioOutbound[0].text.trim().startsWith('⚠️')"
message:
@@ -0,0 +1,83 @@
import fs from "node:fs/promises";
import path from "node:path";
import { readSessionTranscriptRawDelta } from "openclaw/plugin-sdk/session-transcript-runtime";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../../../test/helpers/temp-dir.js";
import {
appendTranscriptMessage,
upsertSessionEntryCore,
} from "../../../config/sessions/session-accessor.js";
import { buildPersistedUserTurnMessage } from "../../../sessions/user-turn-transcript.js";
import { captureEnv, setTestEnvValue } from "../../../test-utils/env.js";
import { convertToLlm } from "../../sessions/messages.js";
import { SessionManager } from "../../sessions/session-manager.js";
import { materializeProviderContext } from "./images.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const MP4 = Buffer.from("0000001c6674797069736f6d0000000069736f6d0000000000000000", "hex");
describe("native video transcript replay", () => {
it("replays native video after reopening the canonical transcript", async () => {
const stateDir = tempDirs.make("openclaw-video-transcript-replay-");
const inboundDir = path.join(stateDir, "media", "inbound");
await fs.mkdir(inboundDir, { recursive: true });
await fs.writeFile(path.join(inboundDir, "history.mp4"), MP4);
const env = captureEnv(["OPENCLAW_STATE_DIR"]);
setTestEnvValue("OPENCLAW_STATE_DIR", stateDir);
const target = {
agentId: "main",
sessionId: "video-replay",
sessionKey: "agent:main:video-replay",
storePath: path.join(stateDir, "sessions.json"),
};
const persisted = buildPersistedUserTurnMessage({
text: "inspect historical video",
media: [
{
kind: "video",
contentType: "video/mp4",
sizeBytes: MP4.length,
url: "media://inbound/history.mp4",
hydrationSuppressed: true,
},
],
});
const serialized = JSON.stringify(persisted);
expect(serialized).toContain("media://inbound/history.mp4");
expect(serialized).not.toContain(MP4.toString("base64"));
expect(serialized).not.toContain(stateDir);
try {
await upsertSessionEntryCore(target, {
sessionId: target.sessionId,
updatedAt: 1,
});
await appendTranscriptMessage(target, {
cwd: stateDir,
eventId: "historical-user",
message: persisted,
now: 1,
});
const reopened = SessionManager.open(target, stateDir).buildSessionContext();
const provider = await materializeProviderContext({
context: { systemPrompt: "system", messages: convertToLlm(reopened.messages), tools: [] },
workspaceDir: stateDir,
});
expect(provider.messages[0]?.content).toEqual([
{ type: "text", text: "inspect historical video" },
{ type: "video", data: MP4.toString("base64"), mimeType: "video/mp4" },
]);
const raw = await readSessionTranscriptRawDelta({
...target,
maxBytes: 100_000,
maxEvents: 100,
});
expect(JSON.stringify(raw)).toContain("media://inbound/history.mp4");
expect(JSON.stringify(raw)).not.toContain(MP4.toString("base64"));
} finally {
env.restore();
}
});
});
@@ -1,257 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
import { readSessionTranscriptRawDelta } from "openclaw/plugin-sdk/session-transcript-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../../../test/helpers/temp-dir.js";
import {
appendTranscriptMessage,
upsertSessionEntryCore,
} from "../../../config/sessions/session-accessor.js";
import { buildPersistedUserTurnMessage } from "../../../sessions/user-turn-transcript.js";
import { captureEnv, setTestEnvValue } from "../../../test-utils/env.js";
import { createOpenClawAgentHarness } from "../../harness/builtin-openclaw.js";
import { guardSessionManager } from "../../session-tool-result-guard-wrapper.js";
import { convertToLlm } from "../../sessions/messages.js";
import { SessionManager } from "../../sessions/session-manager.js";
import { flushSessionManagerTranscript } from "./attempt-transcript-helpers.js";
import { materializeProviderContext } from "./images.js";
const runEmbeddedAttempt = vi.hoisted(() => vi.fn());
vi.mock("./attempt.js", () => ({ runEmbeddedAttempt }));
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const MP4 = Buffer.from("0000001c6674797069736f6d0000000069736f6d0000000000000000", "hex");
function buildAssistantMessage(text: string) {
return {
role: "assistant" as const,
content: [{ type: "text" as const, text }],
api: "openai-responses" as const,
provider: "openai",
model: "test-model",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
},
stopReason: "stop" as const,
timestamp: Date.now(),
};
}
describe("embedded attempt transcript persistence", () => {
it("omits the host-private settled-turn recovery prompt from the raw transcript", async () => {
const dir = tempDirs.make("openclaw-settled-turn-finalization-");
const target = {
agentId: "main",
sessionId: "settled-turn-finalization",
sessionKey: "agent:main:settled-turn-finalization",
storePath: path.join(dir, "sessions.json"),
};
const recoveryPrompt =
"The previous assistant turn completed its tool calls but did not produce a user-visible answer. Continue from the current transcript and produce the final user-visible answer now. Do not repeat completed tool calls or restart from scratch.";
const finalAssistant = buildAssistantMessage("Recovered final answer.");
await upsertSessionEntryCore(target, {
sessionId: target.sessionId,
updatedAt: 1,
});
await appendTranscriptMessage(target, {
cwd: dir,
eventId: "original-user",
message: { role: "user", content: "Original operator request." },
now: 1,
});
runEmbeddedAttempt.mockImplementationOnce(async (attempt) => {
const finalization = attempt as {
prompt: string;
suppressNextUserMessagePersistence?: boolean;
};
const sessionManager = guardSessionManager(SessionManager.open(target, dir), {
skipBeforeMessageWriteHooks: true,
suppressNextUserMessagePersistence: finalization.suppressNextUserMessagePersistence,
});
sessionManager.appendMessage({
role: "user",
content: [{ type: "text", text: finalization.prompt }],
timestamp: Date.now(),
});
sessionManager.appendMessage(finalAssistant);
flushSessionManagerTranscript(sessionManager);
return {
terminal: { kind: "ok" },
sessionIdUsed: target.sessionId,
messagesSnapshot: [finalAssistant],
assistantTexts: ["Recovered final answer."],
toolMetas: [],
lastAssistant: finalAssistant,
currentAttemptAssistant: finalAssistant,
currentAttemptCompletedAssistant: finalAssistant,
didSendViaMessagingTool: false,
didDeliverSourceReplyViaMessageTool: false,
didSendDeterministicApprovalPrompt: false,
messagingToolSentTexts: [],
messagingToolSentMediaUrls: [],
messagingToolSentTargets: [],
messagingToolSourceReplyPayloads: [],
hasToolMediaBlockReply: false,
cloudCodeAssistFormatError: false,
replayMetadata: { hadPotentialSideEffects: false, replaySafe: true },
currentAttemptReplayMetadata: { hadPotentialSideEffects: false, replaySafe: true },
itemLifecycle: { startedCount: 0, completedCount: 0, activeCount: 0 },
} as never;
});
await createOpenClawAgentHarness().finalizeSettledTurn?.({
attempt: { prompt: recoveryPrompt } as never,
settledAttempt: {} as never,
});
const raw = await readSessionTranscriptRawDelta({
...target,
maxBytes: 100_000,
maxEvents: 100,
});
const serialized = JSON.stringify(raw);
expect(serialized).toContain("Original operator request.");
expect(serialized).toContain("Recovered final answer.");
expect(serialized).not.toContain(recoveryPrompt);
});
it("replays native video after reopening the canonical transcript", async () => {
const stateDir = tempDirs.make("openclaw-video-transcript-replay-");
const inboundDir = path.join(stateDir, "media", "inbound");
await fs.mkdir(inboundDir, { recursive: true });
await fs.writeFile(path.join(inboundDir, "history.mp4"), MP4);
const env = captureEnv(["OPENCLAW_STATE_DIR"]);
setTestEnvValue("OPENCLAW_STATE_DIR", stateDir);
const target = {
agentId: "main",
sessionId: "video-replay",
sessionKey: "agent:main:video-replay",
storePath: path.join(stateDir, "sessions.json"),
};
const persisted = buildPersistedUserTurnMessage({
text: "inspect historical video",
media: [
{
kind: "video",
contentType: "video/mp4",
sizeBytes: MP4.length,
url: "media://inbound/history.mp4",
hydrationSuppressed: true,
},
],
});
const serialized = JSON.stringify(persisted);
expect(serialized).toContain("media://inbound/history.mp4");
expect(serialized).not.toContain(MP4.toString("base64"));
expect(serialized).not.toContain(stateDir);
try {
await upsertSessionEntryCore(target, {
sessionId: target.sessionId,
updatedAt: 1,
});
await appendTranscriptMessage(target, {
cwd: stateDir,
eventId: "historical-user",
message: persisted,
now: 1,
});
const reopened = SessionManager.open(target, stateDir).buildSessionContext();
const provider = await materializeProviderContext({
context: { systemPrompt: "system", messages: convertToLlm(reopened.messages), tools: [] },
workspaceDir: stateDir,
});
expect(provider.messages[0]?.content).toEqual([
{ type: "text", text: "inspect historical video" },
{ type: "video", data: MP4.toString("base64"), mimeType: "video/mp4" },
]);
const raw = await readSessionTranscriptRawDelta({
...target,
maxBytes: 100_000,
maxEvents: 100,
});
expect(JSON.stringify(raw)).toContain("media://inbound/history.mp4");
expect(JSON.stringify(raw)).not.toContain(MP4.toString("base64"));
} finally {
env.restore();
}
});
it("resumes a raw cursor after append-only attempt settlement", async () => {
const dir = tempDirs.make("openclaw-attempt-transcript-");
const storePath = path.join(dir, "sessions.json");
const target = {
agentId: "main",
sessionId: "embedded-generation",
sessionKey: "agent:main:embedded-generation",
storePath,
};
await upsertSessionEntryCore(target, {
sessionId: target.sessionId,
updatedAt: 1,
});
await appendTranscriptMessage(target, {
cwd: dir,
eventId: "first-user",
message: { role: "user", content: "first turn" },
now: 1,
});
const bootstrap = await readSessionTranscriptRawDelta({
...target,
maxBytes: 100_000,
maxEvents: 100,
});
expect(bootstrap.kind).toBe("page");
if (bootstrap.kind !== "page") {
throw new Error(`expected bootstrap page, got ${bootstrap.kind}`);
}
const sessionManager = SessionManager.open(target, dir);
sessionManager.appendMessage({
role: "user",
content: "second turn",
timestamp: Date.now(),
});
sessionManager.appendMessage(buildAssistantMessage("second answer"));
// Production settlement invokes this barrier immediately before afterTurn.
flushSessionManagerTranscript(sessionManager);
const resumed = await readSessionTranscriptRawDelta({
...target,
cursor: bootstrap.cursor,
maxBytes: 100_000,
maxEvents: 100,
});
expect(resumed.kind).toBe("page");
if (resumed.kind !== "page") {
throw new Error(`expected append page, got ${resumed.kind}`);
}
expect(
resumed.events
.map((row) => row.event)
.filter((event): event is { message: { content: unknown }; type: "message" } =>
Boolean(
event && typeof event === "object" && "type" in event && event.type === "message",
),
)
.map((event) => event.message.content),
).toEqual(["second turn", [{ type: "text", text: "second answer" }]]);
});
});