| 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | |
| 3 | import type { MuseItem } from "./protocol.ts"; |
| 4 | import { museItemStatus, museToolPresentation } from "./itemPresentation.ts"; |
| 5 | |
| 6 | const item = (fields: Partial<MuseItem>): MuseItem => ({ |
| 7 | itemId: "native-item", |
| 8 | kind: "toolCall", |
| 9 | status: "completed", |
| 10 | revision: 1, |
| 11 | ...fields, |
| 12 | }); |
| 13 | |
| 14 | describe("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 | }); |