From c1be9ac0a7066e91232bce93d9bb2f2841bfe181 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 23:21:44 +0100 Subject: [PATCH] test: move chat tool disclosure coverage --- ui/src/ui/chat/grouped-render.test.ts | 212 +++++++++++++++++++++++++- ui/src/ui/views/chat.test.ts | 200 ------------------------ 2 files changed, 211 insertions(+), 201 deletions(-) diff --git a/ui/src/ui/chat/grouped-render.test.ts b/ui/src/ui/chat/grouped-render.test.ts index fa603a3a0dd..ae34f59730e 100644 --- a/ui/src/ui/chat/grouped-render.test.ts +++ b/ui/src/ui/chat/grouped-render.test.ts @@ -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 = {}, +) { + 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(".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( diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 9f0ee4334b2..cfbc56a3d40 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -47,10 +47,6 @@ function createSessions(): SessionsListResult { }; } -function flushTasks() { - return new Promise((resolve) => queueMicrotask(resolve)); -} - function createProps(overrides: Partial = {}): 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(".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(".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(".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(".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"'); - }); });