1import { describe, expect, it } from "@effect/vitest";
2
3import type { MuseItem } from "./protocol.ts";
4import { museItemStatus, museToolPresentation } from "./itemPresentation.ts";
5
6const item = (fields: Partial<MuseItem>): MuseItem => ({
7 itemId: "native-item",
8 kind: "toolCall",
9 status: "completed",
10 revision: 1,
11 ...fields,
12});
13
14describe("Muse item presentation", () => {
15 it("preserves an edit's file and exact old/new text for the shared diff UI", () => {
16 expect(
17 museToolPresentation(
18 item({
19 tool: "edit_file",
20 args: JSON.stringify({ file_path: "src/index.ts", old_string: "old\n", new_string: "" }),
21 }),
22 ),
23 ).toMatchObject({
24 type: "file_change",
25 status: "completed",
26 fileName: "src/index.ts",
27 oldStr: "old\n",
28 newStr: "",
29 });
30 });
31
32 it("preserves write content and provider-supplied patches", () => {
33 expect(
34 museToolPresentation(
35 item({
36 tool: "write_file",
37 args: JSON.stringify({ path: "README.md", content: "Hello\n" }),
38 }),
39 ),
40 ).toMatchObject({ type: "file_change", newStr: "Hello\n" });
41 expect(
42 museToolPresentation(
43 item({
44 tool: "apply_patch",
45 args: JSON.stringify({ path: "README.md", patch: "@@ -1 +1 @@\n-old\n+new" }),
46 }),
47 ),
48 ).toMatchObject({ type: "file_change", diffStr: "@@ -1 +1 @@\n-old\n+new" });
49 });
50
51 it.each(["rejected", "timedOut", "handedOff"])(
52 "does not report native %s activity as successful",
53 (status) => {
54 const native = item({ tool: "search_files", status, fallbackText: "Native explanation" });
55 expect(museItemStatus(native)).toBe("failed");
56 expect(museToolPresentation(native)).toMatchObject({
57 status: "failed",
58 output: `Muse reported ${status}.\nNative explanation`,
59 });
60 },
61 );
62
63 it("preserves failure details and a known cancelled status", () => {
64 expect(
65 museToolPresentation(
66 item({
67 status: "failed",
68 failureReason: "Permission denied",
69 visibleOutput: "Partial output",
70 }),
71 ),
72 ).toMatchObject({ status: "failed", output: "Permission denied\nPartial output" });
73 expect(museItemStatus(item({ status: "cancelled" }))).toBe("cancelled");
74 });
75
76 it("lists each web search hit and links a fetched page", () => {
77 const search = item({
78 tool: "web_search",
79 args: JSON.stringify({ query: "node lts" }),
80 visibleOutput: JSON.stringify({
81 query: "node lts",
82 results: [
83 { url: "https://nodejs.org/", title: "Node.js", snippet: "Node 24 is LTS", extra: 1 },
84 { url: "", title: "", snippet: "No link" },
85 ],
86 }),
87 });
88 expect(museToolPresentation(search)).toEqual({
89 status: "completed",
90 title: "web_search",
91 type: "web_search",
92 patterns: ["node lts"],
93 results: [
94 { url: "https://nodejs.org/", title: "Node.js", snippet: "Node 24 is LTS" },
95 { snippet: "No link" },
96 ],
97 });
98 const fetch = item({
99 tool: "web_fetch",
100 args: JSON.stringify({ url: "https://nodejs.org/en/about" }),
101 visibleOutput: "About Node.js",
102 });
103 expect(museToolPresentation(fetch)).toMatchObject({
104 type: "web_search",
105 results: [{ url: "https://nodejs.org/en/about", snippet: "About Node.js" }],
106 });
107 });
108
109 it("shows a settled workflow's result instead of its generic label", () => {
110 expect(
111 museToolPresentation(
112 item({
113 kind: "workflow",
114 fallbackText: "Workflow: model-chosen generated workflow",
115 finalSummary: "Both checks passed",
116 }),
117 ),
118 ).toMatchObject({ type: "dynamic_tool", output: "Both checks passed" });
119 });
120
121 it.each(["failed", "noop"])(
122 "keeps %s compaction outcomes from appearing completed",
123 (outcome) => {
124 expect(museItemStatus(item({ kind: "compaction", outcome }))).toBe("failed");
125 },
126 );
127});