mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
test: move chat tool disclosure coverage
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render } from "lit";
|
||||
import { html, render } from "lit";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { MessageGroup } from "../types/chat-types.ts";
|
||||
import {
|
||||
@@ -66,6 +66,42 @@ function renderGroupedMessage(
|
||||
);
|
||||
}
|
||||
|
||||
function createMessageGroup(message: unknown, role: string): MessageGroup {
|
||||
const timestamp =
|
||||
typeof message === "object" &&
|
||||
message !== null &&
|
||||
typeof (message as { timestamp?: unknown }).timestamp === "number"
|
||||
? (message as { timestamp: number }).timestamp
|
||||
: Date.now();
|
||||
return {
|
||||
kind: "group",
|
||||
key: `${role}:${timestamp}`,
|
||||
role,
|
||||
messages: [{ key: `${role}:${timestamp}:message`, message }],
|
||||
timestamp,
|
||||
isStreaming: false,
|
||||
};
|
||||
}
|
||||
|
||||
function renderMessageGroups(
|
||||
container: HTMLElement,
|
||||
groups: MessageGroup[],
|
||||
opts: Partial<RenderMessageGroupOptions> = {},
|
||||
) {
|
||||
render(
|
||||
html`${groups.map((group) =>
|
||||
renderMessageGroup(group, {
|
||||
showReasoning: true,
|
||||
showToolCalls: true,
|
||||
assistantName: "OpenClaw",
|
||||
assistantAvatar: null,
|
||||
...opts,
|
||||
}),
|
||||
)}`,
|
||||
container,
|
||||
);
|
||||
}
|
||||
|
||||
async function flushAssistantAttachmentAvailabilityChecks() {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await Promise.resolve();
|
||||
@@ -78,6 +114,180 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("grouped chat rendering", () => {
|
||||
it("keeps inline tool cards collapsed by default and renders expanded state", () => {
|
||||
const container = document.createElement("div");
|
||||
const message = {
|
||||
id: "assistant-1",
|
||||
role: "assistant",
|
||||
toolCallId: "call-1",
|
||||
content: [
|
||||
{
|
||||
type: "toolcall",
|
||||
id: "call-1",
|
||||
name: "browser.open",
|
||||
arguments: { url: "https://example.com" },
|
||||
},
|
||||
{
|
||||
type: "toolresult",
|
||||
id: "call-1",
|
||||
name: "browser.open",
|
||||
text: "Opened page",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
renderAssistantMessage(container, message, {
|
||||
isToolMessageExpanded: () => false,
|
||||
});
|
||||
|
||||
expect(container.textContent).not.toContain("Input");
|
||||
expect(container.textContent).not.toContain("Output");
|
||||
|
||||
renderAssistantMessage(container, message, {
|
||||
isToolMessageExpanded: () => true,
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("Tool input");
|
||||
expect(container.textContent).toContain("Tool output");
|
||||
expect(container.textContent).toContain("https://example.com");
|
||||
expect(container.textContent).toContain("Opened page");
|
||||
});
|
||||
|
||||
it("renders expanded standalone tool-call rows", () => {
|
||||
const container = document.createElement("div");
|
||||
const message = {
|
||||
id: "assistant-4b",
|
||||
role: "assistant",
|
||||
toolCallId: "call-4b",
|
||||
content: [
|
||||
{
|
||||
type: "toolcall",
|
||||
id: "call-4b",
|
||||
name: "sessions_spawn",
|
||||
arguments: { mode: "session", thread: true },
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
renderAssistantMessage(container, message, {
|
||||
isToolMessageExpanded: () => false,
|
||||
});
|
||||
|
||||
const summary = container.querySelector<HTMLElement>(".chat-tool-msg-summary");
|
||||
expect(summary?.textContent).toContain("Tool call");
|
||||
expect(container.textContent).not.toContain('"thread": true');
|
||||
|
||||
renderAssistantMessage(container, message, {
|
||||
isToolMessageExpanded: () => true,
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("Tool input");
|
||||
expect(container.textContent).toContain('"thread": true');
|
||||
});
|
||||
|
||||
it("renders expanded tool output rows and their json content", () => {
|
||||
const container = document.createElement("div");
|
||||
renderMessageGroups(
|
||||
container,
|
||||
[
|
||||
createMessageGroup(
|
||||
{
|
||||
id: "assistant-5",
|
||||
role: "assistant",
|
||||
toolCallId: "call-5",
|
||||
content: [
|
||||
{
|
||||
type: "toolcall",
|
||||
id: "call-5",
|
||||
name: "sessions_spawn",
|
||||
arguments: { mode: "session", thread: true },
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
"assistant",
|
||||
),
|
||||
createMessageGroup(
|
||||
{
|
||||
id: "tool-5",
|
||||
role: "tool",
|
||||
toolCallId: "call-5",
|
||||
toolName: "sessions_spawn",
|
||||
content: JSON.stringify(
|
||||
{
|
||||
status: "error",
|
||||
error: "Session mode is unavailable for this target.",
|
||||
childSessionKey: "agent:test:subagent:abc123",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
timestamp: Date.now() + 1,
|
||||
},
|
||||
"tool",
|
||||
),
|
||||
],
|
||||
{
|
||||
isToolExpanded: () => true,
|
||||
isToolMessageExpanded: () => true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("Tool input");
|
||||
expect(container.textContent).toContain('"thread": true');
|
||||
expect(container.textContent).toContain("Tool output");
|
||||
expect(container.textContent).toContain('"status": "error"');
|
||||
expect(container.textContent).toContain('"childSessionKey": "agent:test:subagent:abc123"');
|
||||
});
|
||||
|
||||
it("collapses an inline tool call while keeping matching tool output visible", () => {
|
||||
const container = document.createElement("div");
|
||||
const groups = [
|
||||
createMessageGroup(
|
||||
{
|
||||
id: "assistant-tool-messages",
|
||||
role: "assistant",
|
||||
toolCallId: "call-tool-messages",
|
||||
content: [
|
||||
{
|
||||
type: "toolcall",
|
||||
id: "call-tool-messages",
|
||||
name: "sessions_spawn",
|
||||
arguments: { mode: "session", thread: true },
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
"assistant",
|
||||
),
|
||||
createMessageGroup(
|
||||
{
|
||||
id: "tool-tool-messages",
|
||||
role: "tool",
|
||||
toolCallId: "call-tool-messages",
|
||||
toolName: "sessions_spawn",
|
||||
content: JSON.stringify({ status: "error" }, null, 2),
|
||||
timestamp: Date.now() + 1,
|
||||
},
|
||||
"tool",
|
||||
),
|
||||
];
|
||||
renderMessageGroups(container, groups, {
|
||||
isToolMessageExpanded: () => true,
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("Tool input");
|
||||
expect(container.textContent).toContain('"thread": true');
|
||||
expect(container.textContent).toContain('"status": "error"');
|
||||
|
||||
renderMessageGroups(container, groups, {
|
||||
isToolMessageExpanded: (messageId) => !messageId.startsWith("toolmsg:assistant:"),
|
||||
});
|
||||
|
||||
expect(container.textContent).not.toContain("Tool input");
|
||||
expect(container.textContent).toContain('"status": "error"');
|
||||
});
|
||||
|
||||
it("renders assistant MEDIA attachments, voice-note badge, and reply pill", () => {
|
||||
const container = document.createElement("div");
|
||||
renderAssistantMessage(
|
||||
|
||||
@@ -47,10 +47,6 @@ function createSessions(): SessionsListResult {
|
||||
};
|
||||
}
|
||||
|
||||
function flushTasks() {
|
||||
return new Promise<void>((resolve) => queueMicrotask(resolve));
|
||||
}
|
||||
|
||||
function createProps(overrides: Partial<ChatProps> = {}): ChatProps {
|
||||
return {
|
||||
sessionKey: "main",
|
||||
@@ -320,60 +316,6 @@ describe("chat view", () => {
|
||||
expect(assistantConfirm?.classList.contains("chat-delete-confirm--right")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps tool cards collapsed by default and expands them inline on demand", async () => {
|
||||
const container = document.createElement("div");
|
||||
const props = createProps({
|
||||
messages: [
|
||||
{
|
||||
id: "assistant-1",
|
||||
role: "assistant",
|
||||
toolCallId: "call-1",
|
||||
content: [
|
||||
{
|
||||
type: "toolcall",
|
||||
id: "call-1",
|
||||
name: "browser.open",
|
||||
arguments: { url: "https://example.com" },
|
||||
},
|
||||
{
|
||||
type: "toolresult",
|
||||
id: "call-1",
|
||||
name: "browser.open",
|
||||
text: "Opened page",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const rerender = () => {
|
||||
render(renderChat({ ...props, onRequestUpdate: rerender }), container);
|
||||
};
|
||||
rerender();
|
||||
|
||||
expect(container.textContent).not.toContain("Input");
|
||||
expect(container.textContent).not.toContain("Output");
|
||||
|
||||
container
|
||||
.querySelector<HTMLElement>(".chat-tool-msg-summary")
|
||||
?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await flushTasks();
|
||||
|
||||
expect(container.textContent).toContain("Tool input");
|
||||
expect(container.textContent).toContain("Tool output");
|
||||
expect(container.textContent).toContain("https://example.com");
|
||||
expect(container.textContent).toContain("Opened page");
|
||||
|
||||
container
|
||||
.querySelector<HTMLElement>(".chat-tool-msg-summary")
|
||||
?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await flushTasks();
|
||||
|
||||
expect(container.textContent).not.toContain("Tool input");
|
||||
expect(container.textContent).not.toContain("Opened page");
|
||||
});
|
||||
|
||||
it("expands already-visible tool cards when auto-expand is turned on", () => {
|
||||
const container = document.createElement("div");
|
||||
const baseProps = createProps({
|
||||
@@ -409,99 +351,6 @@ describe("chat view", () => {
|
||||
expect(container.textContent).toContain("Tool output");
|
||||
});
|
||||
|
||||
it("routes standalone tool-call rows through the same top-level disclosure as tool output", async () => {
|
||||
const container = document.createElement("div");
|
||||
const props = createProps({
|
||||
messages: [
|
||||
{
|
||||
id: "assistant-4b",
|
||||
role: "assistant",
|
||||
toolCallId: "call-4b",
|
||||
content: [
|
||||
{
|
||||
type: "toolcall",
|
||||
id: "call-4b",
|
||||
name: "sessions_spawn",
|
||||
arguments: { mode: "session", thread: true },
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const rerender = () => {
|
||||
render(renderChat({ ...props, onRequestUpdate: rerender }), container);
|
||||
};
|
||||
rerender();
|
||||
|
||||
const summary = container.querySelector<HTMLElement>(".chat-tool-msg-summary");
|
||||
expect(summary?.textContent).toContain("Tool call");
|
||||
expect(container.textContent).not.toContain('"thread": true');
|
||||
|
||||
summary?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await flushTasks();
|
||||
|
||||
expect(container.textContent).toContain("Tool input");
|
||||
expect(container.textContent).toContain('"thread": true');
|
||||
|
||||
summary?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await flushTasks();
|
||||
|
||||
expect(container.textContent).not.toContain("Tool input");
|
||||
expect(container.textContent).not.toContain('"thread": true');
|
||||
});
|
||||
|
||||
it("auto-expand opens separate tool output rows and their json content", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderChat(
|
||||
createProps({
|
||||
autoExpandToolCalls: true,
|
||||
messages: [
|
||||
{
|
||||
id: "assistant-5",
|
||||
role: "assistant",
|
||||
toolCallId: "call-5",
|
||||
content: [
|
||||
{
|
||||
type: "toolcall",
|
||||
id: "call-5",
|
||||
name: "sessions_spawn",
|
||||
arguments: { mode: "session", thread: true },
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
id: "tool-5",
|
||||
role: "tool",
|
||||
toolCallId: "call-5",
|
||||
toolName: "sessions_spawn",
|
||||
content: JSON.stringify(
|
||||
{
|
||||
status: "error",
|
||||
error: "Session mode is unavailable for this target.",
|
||||
childSessionKey: "agent:test:subagent:abc123",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
timestamp: Date.now() + 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("Tool input");
|
||||
expect(container.textContent).toContain('"thread": true');
|
||||
expect(container.textContent).toContain("Tool output");
|
||||
expect(container.textContent).toContain('"status": "error"');
|
||||
expect(container.textContent).toContain('"childSessionKey": "agent:test:subagent:abc123"');
|
||||
});
|
||||
|
||||
it("renders hidden assistant_message canvas results with the configured sandbox", () => {
|
||||
const container = document.createElement("div");
|
||||
const renderCanvas = (params: { embedSandboxMode?: "trusted"; suffix: string }) =>
|
||||
@@ -843,53 +692,4 @@ describe("chat view", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("lets a tool call collapse while keeping matching tool output visible", async () => {
|
||||
const container = document.createElement("div");
|
||||
const props = createProps({
|
||||
autoExpandToolCalls: true,
|
||||
messages: [
|
||||
{
|
||||
id: "assistant-tool-messages",
|
||||
role: "assistant",
|
||||
toolCallId: "call-tool-messages",
|
||||
content: [
|
||||
{
|
||||
type: "toolcall",
|
||||
id: "call-tool-messages",
|
||||
name: "sessions_spawn",
|
||||
arguments: { mode: "session", thread: true },
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
toolMessages: [
|
||||
{
|
||||
id: "tool-tool-messages",
|
||||
role: "tool",
|
||||
toolCallId: "call-tool-messages",
|
||||
toolName: "sessions_spawn",
|
||||
content: JSON.stringify({ status: "error" }, null, 2),
|
||||
timestamp: Date.now() + 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
const rerender = () => {
|
||||
render(renderChat({ ...props, onRequestUpdate: rerender }), container);
|
||||
};
|
||||
rerender();
|
||||
|
||||
expect(container.textContent).toContain("Tool input");
|
||||
expect(container.textContent).toContain('"thread": true');
|
||||
expect(container.textContent).toContain('"status": "error"');
|
||||
|
||||
const summaries = container.querySelectorAll<HTMLElement>(".chat-tool-msg-summary");
|
||||
expect(summaries.length).toBeGreaterThan(1);
|
||||
summaries[0]?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await flushTasks();
|
||||
|
||||
expect(container.textContent).not.toContain("Tool input");
|
||||
expect(container.textContent).toContain('"status": "error"');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user