mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-13 02:01:16 +00:00
200 lines
5.7 KiB
TypeScript
200 lines
5.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
extractAssistantTextForPhase,
|
|
extractAssistantVisibleText,
|
|
extractFirstTextBlock,
|
|
resolveAssistantMessagePhase,
|
|
} from "./chat-message-content.js";
|
|
|
|
describe("shared/chat-message-content", () => {
|
|
it("extracts the first text block from array content", () => {
|
|
expect(
|
|
extractFirstTextBlock({
|
|
content: [{ text: "hello" }, { text: "world" }],
|
|
}),
|
|
).toBe("hello");
|
|
});
|
|
|
|
it("returns plain string content", () => {
|
|
expect(
|
|
extractFirstTextBlock({
|
|
content: "hello from string content",
|
|
}),
|
|
).toBe("hello from string content");
|
|
});
|
|
|
|
it("preserves empty-string text in the first block", () => {
|
|
expect(
|
|
extractFirstTextBlock({
|
|
content: [{ text: "" }, { text: "later" }],
|
|
}),
|
|
).toBe("");
|
|
});
|
|
|
|
it("only considers the first content block even if later blocks have text", () => {
|
|
expect(
|
|
extractFirstTextBlock({
|
|
content: [null, { text: "later" }],
|
|
}),
|
|
).toBeUndefined();
|
|
expect(
|
|
extractFirstTextBlock({
|
|
content: [{ type: "image" }, { text: "later" }],
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined for missing, empty, or non-text content", () => {
|
|
expect(extractFirstTextBlock(null)).toBeUndefined();
|
|
expect(extractFirstTextBlock({ content: [] })).toBeUndefined();
|
|
expect(extractFirstTextBlock({ content: [{ type: "image" }] })).toBeUndefined();
|
|
expect(extractFirstTextBlock({ content: ["hello"] })).toBeUndefined();
|
|
expect(extractFirstTextBlock({ content: [{ text: 1 }, { text: "later" }] })).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("extractAssistantVisibleText", () => {
|
|
it("preserves boundary spacing when joining adjacent final_answer text blocks", () => {
|
|
expect(
|
|
extractAssistantTextForPhase(
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Hi ",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_final_1", phase: "final_answer" }),
|
|
},
|
|
{
|
|
type: "text",
|
|
text: "there",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_final_2", phase: "final_answer" }),
|
|
},
|
|
],
|
|
},
|
|
{ phase: "final_answer", joinWith: "" },
|
|
),
|
|
).toBe("Hi there");
|
|
});
|
|
|
|
it("prefers final_answer text over commentary text", () => {
|
|
expect(
|
|
extractAssistantVisibleText({
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "thinking like caveman",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_commentary", phase: "commentary" }),
|
|
},
|
|
{
|
|
type: "text",
|
|
text: "Actual final answer",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_final", phase: "final_answer" }),
|
|
},
|
|
],
|
|
}),
|
|
).toBe("Actual final answer");
|
|
});
|
|
|
|
it("does not fall back to commentary-only text", () => {
|
|
expect(
|
|
extractAssistantVisibleText({
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "thinking like caveman",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_commentary", phase: "commentary" }),
|
|
},
|
|
],
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("does not fall back to unphased legacy text when final_answer is empty", () => {
|
|
expect(
|
|
extractAssistantVisibleText({
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "text", text: "Legacy answer" },
|
|
{
|
|
type: "text",
|
|
text: " ",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_final", phase: "final_answer" }),
|
|
},
|
|
],
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("falls back to unphased legacy text", () => {
|
|
expect(
|
|
extractAssistantVisibleText({
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "Legacy answer" }],
|
|
}),
|
|
).toBe("Legacy answer");
|
|
});
|
|
|
|
it("does not mix unphased legacy text into final_answer output", () => {
|
|
expect(
|
|
extractAssistantVisibleText({
|
|
role: "assistant",
|
|
phase: "final_answer",
|
|
content: [
|
|
{ type: "text", text: "Legacy answer" },
|
|
{
|
|
type: "text",
|
|
text: "Actual final answer",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_final", phase: "final_answer" }),
|
|
},
|
|
],
|
|
}),
|
|
).toBe("Actual final answer");
|
|
});
|
|
});
|
|
|
|
describe("resolveAssistantMessagePhase", () => {
|
|
it("prefers the top-level assistant phase when present", () => {
|
|
expect(resolveAssistantMessagePhase({ role: "assistant", phase: "commentary" })).toBe(
|
|
"commentary",
|
|
);
|
|
});
|
|
|
|
it("resolves a single explicit phase from textSignature metadata", () => {
|
|
expect(
|
|
resolveAssistantMessagePhase({
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Actual final answer",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_final", phase: "final_answer" }),
|
|
},
|
|
],
|
|
}),
|
|
).toBe("final_answer");
|
|
});
|
|
|
|
it("returns undefined when text blocks contain mixed explicit phases", () => {
|
|
expect(
|
|
resolveAssistantMessagePhase({
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: "Working...",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_commentary", phase: "commentary" }),
|
|
},
|
|
{
|
|
type: "text",
|
|
text: "Done.",
|
|
textSignature: JSON.stringify({ v: 1, id: "msg_final", phase: "final_answer" }),
|
|
},
|
|
],
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
});
|