Files
claw3d/tests/unit/chatItems.test.ts
T
Luke The Dev 4fa4f13558 First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
2026-03-19 23:14:04 -05:00

339 lines
10 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildAgentChatItems,
buildAgentChatRenderBlocks,
buildFinalAgentChatItems,
summarizeToolLabel,
} from "@/features/agents/components/chatItems";
import { formatMetaMarkdown, formatThinkingMarkdown, formatToolCallMarkdown, formatToolResultMarkdown } from "@/lib/text/message-extract";
describe("buildAgentChatItems", () => {
it("keeps thinking traces aligned with each assistant turn", () => {
const items = buildAgentChatItems({
outputLines: [
"> first question",
formatThinkingMarkdown("first plan"),
"first answer",
"> second question",
formatThinkingMarkdown("second plan"),
"second answer",
],
streamText: null,
liveThinkingTrace: "",
showThinkingTraces: true,
toolCallingEnabled: true,
});
expect(items.map((item) => item.kind)).toEqual([
"user",
"thinking",
"assistant",
"user",
"thinking",
"assistant",
]);
expect(items[1]).toMatchObject({ kind: "thinking", text: "_first plan_" });
expect(items[4]).toMatchObject({ kind: "thinking", text: "_second plan_" });
});
it("does not include saved traces when thinking traces are disabled", () => {
const items = buildAgentChatItems({
outputLines: [
"> first question",
formatThinkingMarkdown("first plan"),
"first answer",
],
streamText: null,
liveThinkingTrace: "live plan",
showThinkingTraces: false,
toolCallingEnabled: true,
});
expect(items.map((item) => item.kind)).toEqual(["user", "assistant"]);
});
it("adds a live trace before the live assistant stream", () => {
const items = buildAgentChatItems({
outputLines: ["first answer"],
streamText: "stream answer",
liveThinkingTrace: "first plan",
showThinkingTraces: true,
toolCallingEnabled: true,
});
expect(items.map((item) => item.kind)).toEqual(["assistant", "thinking", "assistant"]);
expect(items[1]).toMatchObject({ kind: "thinking", text: "_first plan_", live: true });
});
it("merges adjacent thinking traces into a single item", () => {
const items = buildAgentChatItems({
outputLines: [formatThinkingMarkdown("first plan"), formatThinkingMarkdown("second plan"), "answer"],
streamText: null,
liveThinkingTrace: "",
showThinkingTraces: true,
toolCallingEnabled: true,
});
expect(items.map((item) => item.kind)).toEqual(["thinking", "assistant"]);
expect(items[0]).toMatchObject({
kind: "thinking",
text: "_first plan_\n\n_second plan_",
});
});
});
describe("buildFinalAgentChatItems", () => {
it("does not include live thinking or live assistant items", () => {
const items = buildFinalAgentChatItems({
outputLines: ["> question", formatThinkingMarkdown("plan"), "answer"],
showThinkingTraces: true,
toolCallingEnabled: true,
});
expect(items.map((item) => item.kind)).toEqual(["user", "thinking", "assistant"]);
});
it("propagates meta timestamps and thinking duration into subsequent items", () => {
const items = buildFinalAgentChatItems({
outputLines: [
formatMetaMarkdown({ role: "user", timestamp: 1700000000000 }),
"> hello",
formatMetaMarkdown({ role: "assistant", timestamp: 1700000001234, thinkingDurationMs: 1800 }),
formatThinkingMarkdown("plan"),
"answer",
],
showThinkingTraces: true,
toolCallingEnabled: true,
});
expect(items[0]).toMatchObject({ kind: "user", text: "hello", timestampMs: 1700000000000 });
expect(items[1]).toMatchObject({
kind: "thinking",
text: "_plan_",
timestampMs: 1700000001234,
thinkingDurationMs: 1800,
});
expect(items[2]).toMatchObject({
kind: "assistant",
text: "answer",
timestampMs: 1700000001234,
thinkingDurationMs: 1800,
});
});
it("collapses adjacent duplicate user items when optimistic and persisted turns match", () => {
const items = buildFinalAgentChatItems({
outputLines: [
"> hello\n\nworld",
formatMetaMarkdown({ role: "user", timestamp: 1700000000000 }),
"> hello world",
],
showThinkingTraces: true,
toolCallingEnabled: true,
});
expect(items).toEqual([
{
kind: "user",
text: "hello world",
timestampMs: 1700000000000,
},
]);
});
it("does_not_collapse_repeated_user_message_when_second_turn_is_only_optimistic", () => {
const items = buildFinalAgentChatItems({
outputLines: [
formatMetaMarkdown({ role: "user", timestamp: 1700000000000 }),
"> repeat",
"> repeat",
],
showThinkingTraces: true,
toolCallingEnabled: true,
});
expect(items).toEqual([
{
kind: "user",
text: "repeat",
timestampMs: 1700000000000,
},
{
kind: "user",
text: "repeat",
},
]);
});
it("keeps assistant markdown as assistant content", () => {
const items = buildFinalAgentChatItems({
outputLines: ["- first item\n- second item"],
showThinkingTraces: true,
toolCallingEnabled: true,
});
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({ kind: "assistant" });
expect(items[0]?.text).toContain("- first item");
});
it("classifies tool markdown as tool items when tool calling is enabled", () => {
const callLine = formatToolCallMarkdown({
id: "call_123",
name: "exec",
arguments: { command: "pwd" },
});
const toolLine = formatToolResultMarkdown({
toolCallId: "call_123",
toolName: "exec",
details: { status: "completed", exitCode: 0 },
text: "pwd",
isError: false,
});
const items = buildFinalAgentChatItems({
outputLines: [callLine, toolLine],
showThinkingTraces: true,
toolCallingEnabled: true,
});
expect(items).toEqual([
{
kind: "tool",
text: callLine,
},
{
kind: "tool",
text: toolLine,
},
]);
});
it("hides tool results when tool calling is disabled", () => {
const toolLine = formatToolResultMarkdown({
toolCallId: "call_456",
toolName: "exec",
details: { status: "completed", exitCode: 0 },
text: "pwd",
isError: false,
});
const items = buildFinalAgentChatItems({
outputLines: [toolLine],
showThinkingTraces: true,
toolCallingEnabled: false,
});
expect(items).toEqual([]);
});
});
describe("summarizeToolLabel", () => {
it("hides long tool call ids and prefers showing the command/path/url value", () => {
const toolCallLine = formatToolCallMarkdown({
id: "call_ABC123|fc_456",
name: "functions.exec",
arguments: { command: "gh auth status" },
});
const { summaryText: callSummary } = summarizeToolLabel(toolCallLine);
expect(callSummary).toContain("gh auth status");
expect(callSummary).not.toContain("call_");
const toolResultLine = formatToolResultMarkdown({
toolCallId: "call_ABC123|fc_456",
toolName: "functions.exec",
details: { status: "completed", exitCode: 0, durationMs: 168 },
isError: false,
text: "ok",
});
const { summaryText: resultSummary } = summarizeToolLabel(toolResultLine);
expect(resultSummary).toContain("completed");
expect(resultSummary).toContain("exit 0");
expect(resultSummary).not.toContain("call_");
});
it("renders read file calls as inline path labels without JSON body", () => {
const toolCallLine = formatToolCallMarkdown({
id: "call_read_1",
name: "read",
arguments: { file_path: "/path/to/openclaw-agent-home/README.md" },
});
const summary = summarizeToolLabel(toolCallLine);
expect(summary.summaryText).toBe(
"read /path/to/openclaw-agent-home/README.md"
);
expect(summary.inlineOnly).toBe(true);
expect(summary.body).toBe("");
});
});
describe("buildAgentChatRenderBlocks", () => {
it("groups thinking and tool events into one assistant block in original order", () => {
const toolCallLine = formatToolCallMarkdown({
id: "call_1",
name: "exec",
arguments: { command: "pwd" },
});
const toolResultLine = formatToolResultMarkdown({
toolCallId: "call_1",
toolName: "exec",
details: { status: "completed", exitCode: 0 },
text: "/repo",
isError: false,
});
const blocks = buildAgentChatRenderBlocks([
{ kind: "thinking", text: "_plan before tool_", timestampMs: 100 },
{ kind: "tool", text: toolCallLine, timestampMs: 101 },
{ kind: "thinking", text: "_plan after tool_", timestampMs: 102 },
{ kind: "tool", text: toolResultLine, timestampMs: 103 },
{ kind: "assistant", text: "done", timestampMs: 104 },
]);
expect(blocks).toEqual([
{
kind: "assistant",
text: "done",
timestampMs: 100,
traceEvents: [
{ kind: "thinking", text: "_plan before tool_" },
{ kind: "tool", text: toolCallLine },
{ kind: "thinking", text: "_plan after tool_" },
{ kind: "tool", text: toolResultLine },
],
},
]);
});
it("starts a new assistant block after a user turn", () => {
const blocks = buildAgentChatRenderBlocks([
{ kind: "thinking", text: "_first plan_", timestampMs: 10 },
{ kind: "assistant", text: "first answer", timestampMs: 11 },
{ kind: "user", text: "next question", timestampMs: 12 },
{ kind: "thinking", text: "_second plan_", timestampMs: 13 },
{ kind: "assistant", text: "second answer", timestampMs: 14 },
]);
expect(blocks.map((block) => block.kind)).toEqual(["assistant", "user", "assistant"]);
});
it("merges adjacent incremental thinking updates", () => {
const blocks = buildAgentChatRenderBlocks([
{ kind: "thinking", text: "_a_", timestampMs: 10 },
{ kind: "thinking", text: "_a_\n\n_b_", timestampMs: 10 },
{ kind: "assistant", text: "answer", timestampMs: 10 },
]);
expect(blocks).toEqual([
{
kind: "assistant",
text: "answer",
timestampMs: 10,
traceEvents: [{ kind: "thinking", text: "_a_\n\n_b_" }],
},
]);
});
});