





















@@ -0,0 +1,255 @@
1+// @vitest-environment node
2+3+import { describe, expect, it, vi } from "vitest";
4+import { buildToolCardSidebarContent, extractToolCards } from "./tool-cards.ts";
5+6+vi.mock("../icons.ts", () => ({
7+icons: {},
8+}));
9+10+vi.mock("../tool-display.ts", () => ({
11+formatToolDetail: () => undefined,
12+resolveToolDisplay: ({ name }: { name: string }) => ({
13+ name,
14+label: name
15+.split(/[._-]/g)
16+.map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part))
17+.join(" "),
18+icon: "zap",
19+}),
20+}));
21+22+describe("tool-card extraction", () => {
23+it("pretty-prints structured args and pairs tool output onto the same card", () => {
24+const cards = extractToolCards(
25+{
26+role: "assistant",
27+toolCallId: "call-1",
28+content: [
29+{
30+type: "toolcall",
31+id: "call-1",
32+name: "browser.open",
33+arguments: { url: "https://example.com", retry: 0 },
34+},
35+{
36+type: "toolresult",
37+id: "call-1",
38+name: "browser.open",
39+text: "Opened page",
40+},
41+],
42+},
43+"msg:1",
44+);
45+46+expect(cards).toHaveLength(1);
47+expect(cards[0]).toMatchObject({
48+id: "msg:1:call-1",
49+name: "browser.open",
50+outputText: "Opened page",
51+});
52+expect(cards[0]?.inputText).toContain('"url": "https://example.com"');
53+expect(cards[0]?.inputText).toContain('"retry": 0');
54+});
55+56+it("preserves string args verbatim and keeps empty-output cards", () => {
57+const cards = extractToolCards(
58+{
59+role: "assistant",
60+toolCallId: "call-2",
61+content: [
62+{
63+type: "toolcall",
64+name: "deck_manage",
65+arguments: "with Example Deck",
66+},
67+],
68+},
69+"msg:2",
70+);
71+72+expect(cards).toHaveLength(1);
73+expect(cards[0]?.inputText).toBe("with Example Deck");
74+expect(cards[0]?.outputText).toBeUndefined();
75+});
76+77+it("preserves tool-call input payloads from tool_use blocks", () => {
78+const cards = extractToolCards(
79+{
80+role: "assistant",
81+content: [
82+{
83+type: "tool_use",
84+id: "call-2b",
85+name: "deck_manage",
86+input: { deck: "Example Deck", mode: "preview" },
87+},
88+],
89+},
90+"msg:2b",
91+);
92+93+expect(cards).toHaveLength(1);
94+expect(cards[0]?.inputText).toContain('"deck": "Example Deck"');
95+expect(cards[0]?.inputText).toContain('"mode": "preview"');
96+});
97+98+it("pairs interleaved nameless tool results in content order", () => {
99+const cards = extractToolCards(
100+{
101+role: "assistant",
102+content: [
103+{
104+type: "tool_use",
105+name: "browser.open",
106+input: { url: "https://example.com/a" },
107+},
108+{
109+type: "tool_result",
110+name: "browser.open",
111+text: "Opened A",
112+},
113+{
114+type: "tool_use",
115+name: "browser.open",
116+input: { url: "https://example.com/b" },
117+},
118+{
119+type: "tool_result",
120+name: "browser.open",
121+text: "Opened B",
122+},
123+],
124+},
125+"msg:ordered",
126+);
127+128+expect(cards).toHaveLength(2);
129+expect(cards[0]).toMatchObject({
130+inputText: '{\n "url": "https://example.com/a"\n}',
131+outputText: "Opened A",
132+});
133+expect(cards[1]).toMatchObject({
134+inputText: '{\n "url": "https://example.com/b"\n}',
135+outputText: "Opened B",
136+});
137+});
138+139+it("builds sidebar content with input and empty output status", () => {
140+const [card] = extractToolCards(
141+{
142+role: "assistant",
143+toolCallId: "call-3",
144+content: [
145+{
146+type: "toolcall",
147+name: "deck_manage",
148+arguments: "with Example Deck",
149+},
150+],
151+},
152+"msg:3",
153+);
154+155+const sidebar = buildToolCardSidebarContent(card);
156+expect(sidebar).toContain("## Deck Manage");
157+expect(sidebar).toContain("### Tool input");
158+expect(sidebar).toContain("with Example Deck");
159+expect(sidebar).toContain("### Tool output");
160+expect(sidebar).toContain("No output");
161+});
162+163+it("extracts canvas handle payloads into canvas previews", () => {
164+const [card] = extractToolCards(
165+{
166+role: "tool",
167+toolName: "canvas_render",
168+content: JSON.stringify({
169+kind: "canvas",
170+view: {
171+backend: "canvas",
172+id: "cv_inline",
173+url: "/__openclaw__/canvas/documents/cv_inline/index.html",
174+},
175+presentation: {
176+target: "assistant_message",
177+title: "Inline demo",
178+preferred_height: 420,
179+},
180+}),
181+},
182+"msg:view:1",
183+);
184+185+expect(card?.preview).toMatchObject({
186+kind: "canvas",
187+surface: "assistant_message",
188+render: "url",
189+viewId: "cv_inline",
190+url: "/__openclaw__/canvas/documents/cv_inline/index.html",
191+title: "Inline demo",
192+preferredHeight: 420,
193+});
194+});
195+196+it("does not create previews for non-assistant canvas or generic outputs", () => {
197+const cases = [
198+{
199+name: "tool-card target",
200+toolName: "canvas_render",
201+content: JSON.stringify({
202+kind: "canvas",
203+view: {
204+backend: "canvas",
205+id: "cv_tool_card",
206+url: "/__openclaw__/canvas/documents/cv_tool_card/index.html",
207+},
208+presentation: {
209+target: "tool_card",
210+title: "Tool card demo",
211+},
212+}),
213+},
214+{
215+name: "inline html",
216+toolName: "canvas_render",
217+content: JSON.stringify({
218+kind: "canvas",
219+source: {
220+type: "html",
221+content: "<div>hello</div>",
222+},
223+presentation: {
224+target: "assistant_message",
225+title: "Status",
226+preferred_height: 300,
227+},
228+}),
229+},
230+{
231+name: "malformed json",
232+toolName: "canvas_render",
233+content: '{"kind":"present_view","view":{"id":"broken"}',
234+},
235+{
236+name: "generic text",
237+toolName: "browser.open",
238+content: "present_view: cv_widget",
239+},
240+] as const;
241+242+for (const testCase of cases) {
243+const [card] = extractToolCards(
244+{
245+role: "tool",
246+toolName: testCase.toolName,
247+content: testCase.content,
248+},
249+`msg:view:${testCase.name}`,
250+);
251+252+expect(card?.preview, testCase.name).toBeUndefined();
253+}
254+});
255+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。