Files
openclaw/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.e2e.test.ts
2026-02-17 10:57:31 +09:00

282 lines
8.8 KiB
TypeScript

import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { describe, expect, it } from "vitest";
import {
sanitizeGoogleTurnOrdering,
sanitizeSessionMessagesImages,
} from "./pi-embedded-helpers.js";
function makeToolCallResultPairInput(): AgentMessage[] {
return [
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_123|fc_456",
name: "read",
arguments: { path: "package.json" },
},
],
},
{
role: "toolResult",
toolCallId: "call_123|fc_456",
toolName: "read",
content: [{ type: "text", text: "ok" }],
isError: false,
},
] as AgentMessage[];
}
function expectToolCallAndResultIds(out: AgentMessage[], expectedId: string) {
const assistant = out[0] as unknown as { role?: string; content?: unknown };
expect(assistant.role).toBe("assistant");
expect(Array.isArray(assistant.content)).toBe(true);
const toolCall = (assistant.content as Array<{ type?: string; id?: string }>).find(
(block) => block.type === "toolCall",
);
expect(toolCall?.id).toBe(expectedId);
const toolResult = out[1] as unknown as {
role?: string;
toolCallId?: string;
};
expect(toolResult.role).toBe("toolResult");
expect(toolResult.toolCallId).toBe(expectedId);
}
function expectSingleAssistantContentEntry(
out: AgentMessage[],
expectEntry: (entry: { type?: string; text?: string }) => void,
) {
expect(out).toHaveLength(1);
const content = (out[0] as { content?: unknown }).content;
expect(Array.isArray(content)).toBe(true);
expect(content).toHaveLength(1);
expectEntry((content as Array<{ type?: string; text?: string }>)[0] ?? {});
}
describe("sanitizeSessionMessagesImages", () => {
it("keeps tool call + tool result IDs unchanged by default", async () => {
const input = makeToolCallResultPairInput();
const out = await sanitizeSessionMessagesImages(input, "test");
expectToolCallAndResultIds(out, "call_123|fc_456");
});
it("sanitizes tool call + tool result IDs in strict mode (alphanumeric only)", async () => {
const input = makeToolCallResultPairInput();
const out = await sanitizeSessionMessagesImages(input, "test", {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
});
// Strict mode strips all non-alphanumeric characters
expectToolCallAndResultIds(out, "call123fc456");
});
it("does not synthesize tool call input when missing", async () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read" }],
},
] as unknown as AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test");
const assistant = out[0] as { content?: Array<Record<string, unknown>> };
const toolCall = assistant.content?.find((b) => b.type === "toolCall");
expect(toolCall).toBeTruthy();
expect("input" in (toolCall ?? {})).toBe(false);
expect("arguments" in (toolCall ?? {})).toBe(false);
});
it("removes empty assistant text blocks but preserves tool calls", async () => {
const input = [
{
role: "assistant",
content: [
{ type: "text", text: "" },
{ type: "toolCall", id: "call_1", name: "read", arguments: {} },
],
},
] as unknown as AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test");
expectSingleAssistantContentEntry(out, (entry) => {
expect(entry.type).toBe("toolCall");
});
});
it("sanitizes tool ids in strict mode (alphanumeric only)", async () => {
const input = [
{
role: "assistant",
content: [
{ type: "toolUse", id: "call_abc|item:123", name: "test", input: {} },
{
type: "toolCall",
id: "call_abc|item:456",
name: "exec",
arguments: {},
},
],
},
{
role: "toolResult",
toolUseId: "call_abc|item:123",
content: [{ type: "text", text: "ok" }],
},
] as unknown as AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test", {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
});
// Strict mode strips all non-alphanumeric characters
const assistant = out[0] as { content?: Array<{ id?: string }> };
expect(assistant.content?.[0]?.id).toBe("callabcitem123");
expect(assistant.content?.[1]?.id).toBe("callabcitem456");
const toolResult = out[1] as { toolUseId?: string };
expect(toolResult.toolUseId).toBe("callabcitem123");
});
it("does not sanitize tool IDs in images-only mode", async () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call_123|fc_456", name: "read", arguments: {} }],
},
{
role: "toolResult",
toolCallId: "call_123|fc_456",
toolName: "read",
content: [{ type: "text", text: "ok" }],
isError: false,
},
] as unknown as AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test", {
sanitizeMode: "images-only",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
});
const assistant = out[0] as unknown as { content?: Array<{ type?: string; id?: string }> };
const toolCall = assistant.content?.find((b) => b.type === "toolCall");
expect(toolCall?.id).toBe("call_123|fc_456");
const toolResult = out[1] as unknown as { toolCallId?: string };
expect(toolResult.toolCallId).toBe("call_123|fc_456");
});
it("filters whitespace-only assistant text blocks", async () => {
const input = [
{
role: "assistant",
content: [
{ type: "text", text: " " },
{ type: "text", text: "ok" },
],
},
] as unknown as AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test");
expectSingleAssistantContentEntry(out, (entry) => {
expect(entry.text).toBe("ok");
});
});
it("drops assistant messages that only contain empty text", async () => {
const input = [
{ role: "user", content: "hello" },
{ role: "assistant", content: [{ type: "text", text: "" }] },
] as unknown as AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test");
expect(out).toHaveLength(1);
expect(out[0]?.role).toBe("user");
});
it("keeps empty assistant error messages", async () => {
const input = [
{ role: "user", content: "hello" },
{ role: "assistant", stopReason: "error", content: [] },
{ role: "assistant", stopReason: "error" },
] as unknown as AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test");
expect(out).toHaveLength(3);
expect(out[0]?.role).toBe("user");
expect(out[1]?.role).toBe("assistant");
expect(out[2]?.role).toBe("assistant");
});
it("leaves non-assistant messages unchanged", async () => {
const input = [
{ role: "user", content: "hello" },
{
role: "toolResult",
toolCallId: "tool-1",
content: [{ type: "text", text: "result" }],
},
] as unknown as AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test");
expect(out).toHaveLength(2);
expect(out[0]?.role).toBe("user");
expect(out[1]?.role).toBe("toolResult");
});
describe("thought_signature stripping", () => {
it("strips msg_-prefixed thought_signature from assistant message content blocks", async () => {
const input = [
{
role: "assistant",
content: [
{ type: "text", text: "hello", thought_signature: "msg_abc123" },
{
type: "thinking",
thinking: "reasoning",
thought_signature: "AQID",
},
],
},
] as unknown as AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test");
expect(out).toHaveLength(1);
const content = (out[0] as { content?: unknown[] }).content;
expect(content).toHaveLength(2);
expect("thought_signature" in ((content?.[0] ?? {}) as object)).toBe(false);
expect((content?.[1] as { thought_signature?: unknown })?.thought_signature).toBe("AQID");
});
});
});
describe("sanitizeGoogleTurnOrdering", () => {
it("prepends a synthetic user turn when history starts with assistant", () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
},
] as unknown as AgentMessage[];
const out = sanitizeGoogleTurnOrdering(input);
expect(out[0]?.role).toBe("user");
expect(out[1]?.role).toBe("assistant");
});
it("is a no-op when history starts with user", () => {
const input = [{ role: "user", content: "hi" }] as unknown as AgentMessage[];
const out = sanitizeGoogleTurnOrdering(input);
expect(out).toBe(input);
});
});