openclaw / src /cron /isolated-agent /current-session-completion.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
5cb63c1 verified
Raw History Blame Contribute Delete
17 kB
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { setReplyPayloadMetadata, type ReplyPayload } from "../../auto-reply/reply-payload.js";
import {
loadTranscriptEvents,
replaceSessionEntry,
} from "../../config/sessions/session-accessor.js";
import {
readTranscriptEventId,
readTranscriptEventMessage,
} from "../../config/sessions/session-accessor.sqlite-read.js";
import {
MANAGED_OUTGOING_IMAGE_ARTIFACT_ID_PREFIX,
resolveManagedOutgoingMediaArtifactDownload,
} from "../../gateway/managed-image-attachments.js";
import { listManagedImageRecordEntries } from "../../gateway/managed-image-record-store.js";
import {
beginSessionWorkAdmission,
getActiveSessionLifecycleMutationCount,
} from "../../sessions/session-lifecycle-admission.js";
import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
import { readAssistantDisplayContent } from "../../shared/assistant-display-content.js";
import { openOpenClawStateDatabase } from "../../state/openclaw-state-db.js";
import {
withOpenClawTestState,
type OpenClawTestState,
} from "../../test-utils/openclaw-test-state.js";
import { resolveCronDeliveryPlan } from "../delivery-plan.js";
import { makeCronJob } from "../delivery.test-helpers.js";
import { createCliDeps } from "../isolated-agent.delivery.test-helpers.js";
import { commitCurrentSessionCronCompletion } from "./current-session-completion.js";
import type { DispatchCronDeliveryParams } from "./delivery-dispatch-types.js";
import { dispatchCronDelivery } from "./delivery-dispatch.js";
const PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=",
"base64",
);
async function createCompletionFixture(state: OpenClawTestState) {
const sessionKey = "agent:main:webchat:direct:report";
const sessionId = "report-session";
const scope = {
agentId: "main",
sessionKey,
sessionId,
storePath: path.join(state.stateDir, "agents", "main", "sessions", "sessions.json"),
};
const generation = { sessionId, lifecycleRevision: "report-generation" };
await replaceSessionEntry(scope, { ...generation, updatedAt: 1 });
await fs.mkdir(state.workspaceDir, { recursive: true });
const imagePath = path.join(state.workspaceDir, "report.png");
await fs.writeFile(imagePath, PNG);
const cfg = {
agents: { entries: { main: { workspace: state.workspaceDir } } },
session: { store: scope.storePath },
};
const payload: ReplyPayload = { text: "Example report", mediaUrl: imagePath };
const job = makeCronJob({ id: "report-job", sessionTarget: "current", sessionKey });
const params: DispatchCronDeliveryParams = {
cfg,
cfgWithAgentDefaults: cfg,
deps: createCliDeps(),
job,
agentId: "main",
agentSessionKey: "agent:main:cron:report-job",
sourceSessionKey: sessionKey,
sourceSessionGeneration: generation,
runSessionKey: "agent:main:cron:report-job:run:report-run",
sessionId: "report-run",
lifecycleRevision: "run-generation",
sessionUpdatedAt: 1000,
runStartedAt: 1000,
runEndedAt: 2000,
timeoutMs: 30000,
resolvedDelivery: { ok: false, mode: "implicit", error: new Error("No external channel") },
deliveryPlan: resolveCronDeliveryPlan(job),
deliveryRequested: true,
undeliveredRunStatus: "ok",
spawnOnlyHandoff: false,
sourceDeliveryOutcome: {
visibleDeliveries: [],
verifiedMessageToolDelivery: false,
satisfiesSourceDelivery: false,
unverifiedMessageToolDelivery: false,
},
deliveryBestEffort: false,
deliveryPayloadHasStructuredContent: true,
deliveryPayloads: [payload],
isAborted: () => false,
abortReason: () => "aborted",
withRunSession: (result) => ({ ...result, sessionId: "report-run" }),
};
const records = () => listManagedImageRecordEntries({ stateDir: state.stateDir, sessionKey });
const downloads: Array<ReturnType<typeof resolveManagedOutgoingMediaArtifactDownload>> = [];
let updates = 0;
const unsubscribe = onSessionTranscriptUpdate((update) => {
if (update.target.sessionId !== sessionId) {
return;
}
updates += 1;
for (const { record } of records()) {
downloads.push(
resolveManagedOutgoingMediaArtifactDownload({
sessionKey,
agentId: "main",
stateDir: state.stateDir,
artifactId: `${MANAGED_OUTGOING_IMAGE_ARTIFACT_ID_PREFIX}${record.attachmentId}`,
}),
);
}
});
return {
payload,
params,
scope,
records,
downloads,
updates: () => updates,
unsubscribe,
commit: () => commitCurrentSessionCronCompletion(params),
messages: async () =>
(await loadTranscriptEvents(scope)).filter(
(event) => readTranscriptEventMessage(event)?.role === "assistant",
),
};
}
describe("current-session completion delivery", () => {
it.each(["leading-token", "silent-media-caption"] as const)(
"normalizes %s output before committing the conversation",
async (mode) => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
const fixture = await createCompletionFixture(state);
try {
const hasMedia = mode === "silent-media-caption";
const text = hasMedia ? "Report complete.\nNO_REPLY" : "NO_REPLY\nReport complete.";
fixture.params.deliveryPayloads = [
{ text, ...(hasMedia ? { mediaUrl: fixture.payload.mediaUrl } : {}) },
];
fixture.params.deliveryPayloadHasStructuredContent = hasMedia;
fixture.params.synthesizedText = text;
fixture.params.outputText = text;
fixture.params.summary = text;
const delivery = await dispatchCronDelivery(fixture.params);
expect(delivery.delivered).toBe(true);
const messages = await fixture.messages();
expect(messages).toHaveLength(1);
const message = readTranscriptEventMessage(messages[0]);
expect(message?.content).toEqual([
{ type: "text", text: hasMedia ? "report.png" : "Report complete." },
]);
if (hasMedia) {
expect(readAssistantDisplayContent(message)).toEqual([
expect.objectContaining({ type: "image" }),
]);
expect(fixture.records()).toHaveLength(1);
}
} finally {
fixture.unsubscribe();
}
});
},
);
it.each([{ to: "recipient" }, { accountId: "work" }, { threadId: 0 }])(
"preserves the committed report and reports unresolved explicit intent %j",
async (coordinates) => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
const fixture = await createCompletionFixture(state);
try {
fixture.params.job.delivery = { mode: "announce", ...coordinates };
fixture.params.deliveryPlan = resolveCronDeliveryPlan(fixture.params.job);
fixture.params.deliveryPayloads = [{ text: "Final report" }];
const completion = await fixture.commit();
const messages = await fixture.messages();
expect(messages).toHaveLength(1);
expect(readTranscriptEventMessage(messages[0])?.content).toEqual([
{ type: "text", text: "Final report" },
]);
expect(completion).toEqual({
ok: true,
requiresExternalDelivery: false,
deliveryError: "No external channel",
});
} finally {
fixture.unsubscribe();
}
});
},
);
});
describe("current-session completion media", () => {
it.each(["ordinary", "promotion-failure"] as const)(
"publishes downloadable media and replays the original message after %s",
async (mode) => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
const fixture = await createCompletionFixture(state);
const database = openOpenClawStateDatabase({ env: state.env });
try {
if (mode === "promotion-failure") {
database.db.exec(`CREATE TEMP TRIGGER fail_report_promotion
BEFORE UPDATE OF message_id ON managed_outgoing_image_records
BEGIN SELECT RAISE(ABORT, 'report promotion failed'); END`);
await expect(fixture.commit()).rejects.toThrow("report promotion failed");
expect(fixture.updates()).toBe(0);
database.db.exec("DROP TRIGGER fail_report_promotion");
} else {
await expect(fixture.commit()).resolves.toMatchObject({ ok: true });
}
const original = await fixture.messages();
const originalIds = fixture.records().map(({ record }) => record.attachmentId);
expect(original).toHaveLength(1);
await expect(fixture.commit()).resolves.toMatchObject({ ok: true });
expect(await fixture.messages()).toEqual(original);
expect(fixture.records().map(({ record }) => record.attachmentId)).toEqual(originalIds);
expect(fixture.updates()).toBeGreaterThan(0);
for (const { record } of fixture.records()) {
expect(record).toMatchObject({
messageId: readTranscriptEventId(original[0]),
retentionClass: "history",
});
await expect(
fs.readFile(
path.join(
record.original.mediaRoot,
record.original.mediaSubdir,
record.original.mediaId,
),
),
).resolves.toEqual(PNG);
}
expect(fixture.downloads.length).toBeGreaterThan(0);
for (const download of await Promise.all(fixture.downloads)) {
expect(download).toMatchObject({ type: "image" });
}
} finally {
database.db.exec("DROP TRIGGER IF EXISTS fail_report_promotion");
fixture.unsubscribe();
}
});
},
);
it("keeps structured report text in both model and display history", async () => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
const fixture = await createCompletionFixture(state);
try {
fixture.payload.presentation = {
blocks: [
{
type: "table",
caption: "Revenue",
headers: ["Quarter", "Revenue"],
rows: [["Q1", 10]],
},
],
};
await fixture.commit();
const message = readTranscriptEventMessage((await fixture.messages())[0]);
expect(message?.content).toEqual([
{
type: "text",
text: "Example report\nRevenue (table)\n- Quarter: Q1; Revenue: 10\nreport.png",
},
]);
expect(readAssistantDisplayContent(message)).toEqual([
{ type: "text", text: "Example report\nRevenue (table)\n- Quarter: Q1; Revenue: 10" },
expect.objectContaining({ type: "image" }),
]);
} finally {
fixture.unsubscribe();
}
});
});
it.each(["text-only", "missing-image", "media-only"] as const)(
"preserves the completed result for %s output",
async (mode) => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
const fixture = await createCompletionFixture(state);
try {
if (mode === "text-only") {
fixture.params.deliveryPayloads = [{ text: "Final report" }];
} else if (mode === "missing-image") {
await fs.unlink(path.join(state.workspaceDir, "report.png"));
} else {
fixture.payload.text = undefined;
}
await expect(fixture.commit()).resolves.toMatchObject({ ok: true });
const message = readTranscriptEventMessage((await fixture.messages())[0]);
if (mode === "media-only") {
expect(readAssistantDisplayContent(message)).toEqual([
expect.objectContaining({ type: "image" }),
]);
expect(fixture.records()).toHaveLength(1);
} else {
expect(message?.content).toEqual([
{
type: "text",
text: mode === "text-only" ? "Final report" : "Example report\nreport.png",
},
]);
if (mode === "missing-image") {
expect(readAssistantDisplayContent(message)).toEqual([
{ type: "text", text: "Example report" },
expect.objectContaining({
type: "attachment_error",
attachment: expect.objectContaining({
label: "report.png",
code: "delivery-failed",
}),
}),
]);
} else {
expect(message).not.toHaveProperty("openclawDisplayContent");
}
expect(fixture.records()).toEqual([]);
}
} finally {
fixture.unsubscribe();
}
});
},
);
it("preserves interleaved report text, media, and spoken-payload failure metadata", async () => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
const fixture = await createCompletionFixture(state);
try {
const secondImage = path.join(state.workspaceDir, "second.png");
await fs.writeFile(secondImage, PNG);
fixture.params.deliveryPayloads.push(
setReplyPayloadMetadata(
{
spokenText: "Second report",
mediaUrl: secondImage,
},
{
assistantMediaFailures: [
{ code: "file-not-found", kind: "image", label: "missing.png" },
],
},
),
);
await fixture.commit();
const message = readTranscriptEventMessage((await fixture.messages())[0]);
expect(readAssistantDisplayContent(message)).toEqual([
{ type: "text", text: "Example report" },
expect.objectContaining({ type: "image", alt: "report.png" }),
{ type: "text", text: "Second report" },
expect.objectContaining({ type: "image", alt: "second.png" }),
expect.objectContaining({
type: "attachment_error",
attachment: expect.objectContaining({ label: "missing.png" }),
}),
]);
} finally {
fixture.unsubscribe();
}
});
});
it.each([true, false])(
"obeys workspaceOnly=%s for images beside the session store",
async (workspaceOnly) => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
const fixture = await createCompletionFixture(state);
try {
const privateImage = path.join(path.dirname(fixture.scope.storePath), "private.png");
await fs.mkdir(path.dirname(privateImage), { recursive: true });
await fs.writeFile(privateImage, PNG);
fixture.params.cfgWithAgentDefaults.tools = { profile: "full", fs: { workspaceOnly } };
fixture.params.deliveryPayloads = [{ text: "Report", mediaUrl: privateImage }];
await expect(fixture.commit()).resolves.toMatchObject({ ok: true });
const message = readTranscriptEventMessage((await fixture.messages())[0]);
const content = readAssistantDisplayContent(message);
expect(content).toEqual([
{ type: "text", text: "Report" },
expect.objectContaining({ type: workspaceOnly ? "attachment_error" : "image" }),
]);
expect(fixture.records()).toHaveLength(workspaceOnly ? 0 : 1);
} finally {
fixture.unsubscribe();
}
});
},
);
it("does not prepare media when the source generation changes while waiting", async () => {
await withOpenClawTestState({ layout: "state-only" }, async (state) => {
const fixture = await createCompletionFixture(state);
const admission = await beginSessionWorkAdmission({
scope: fixture.scope.storePath,
identities: [fixture.scope.sessionKey, fixture.scope.sessionId],
assertAllowed: () => {},
});
const commit = fixture.commit();
try {
await vi.waitFor(() => expect(getActiveSessionLifecycleMutationCount()).toBe(1));
expect(fixture.records()).toEqual([]);
await replaceSessionEntry(fixture.scope, {
sessionId: "replacement-session",
lifecycleRevision: "replacement-generation",
updatedAt: 3000,
});
admission.release();
await expect(commit).resolves.toMatchObject({ ok: false });
expect(fixture.records()).toEqual([]);
expect(await fixture.messages()).toEqual([]);
expect(fixture.updates()).toBe(0);
} finally {
admission.release();
await commit;
fixture.unsubscribe();
}
});
});
});