import { assert } from "convex-helpers"; import { describe, expect, it } from "vitest"; import { toUIMessages } from "./UIMessages.js"; import type { MessageDoc } from "./validators.js"; // Helper to create a base message doc function baseMessageDoc(overrides: Partial = {}): MessageDoc { return { _id: "msg1", _creationTime: Date.now(), order: 0, stepOrder: 0, status: "success", threadId: "thread1", tool: false, ...overrides, }; } describe("toUIMessages", () => { it("handles user message", () => { const messages = [ baseMessageDoc({ message: { role: "user", content: "Hello!", }, text: "Hello!", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("user"); expect(uiMessages[0].text).toBe("Hello!"); expect(uiMessages[0].parts[0]).toEqual({ type: "text", text: "Hello!" }); }); it("handles assistant message", () => { const messages = [ baseMessageDoc({ message: { role: "assistant", content: "Hi, how can I help?", }, text: "Hi, how can I help?", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); expect(uiMessages[0].text).toBe("Hi, how can I help?"); expect(uiMessages[0].parts[0]).toEqual({ type: "text", text: "Hi, how can I help?", state: "done", }); }); it("handles multiple messages", () => { const messages = [ baseMessageDoc({ message: { role: "user", content: "Hello!", }, text: "Hello!", }), baseMessageDoc({ message: { role: "assistant", content: [ { type: "reasoning", text: "I'm thinking...", }, { type: "redacted-reasoning", data: "asdfasdfasdf", }, { type: "text", text: "I'm thinking...", }, { type: "file", mimeType: "text/plain", data: "https://example.com/file.txt", }, { type: "tool-call", toolName: "myTool", toolCallId: "call1", input: "an arg", args: "an arg", }, ], }, tool: true, reasoning: "I'm thinking...", text: "I'm thinking...", }), baseMessageDoc({ message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "myTool", output: { type: "text", value: "42", }, }, ], }, tool: true, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(2); expect(uiMessages[0].role).toBe("user"); expect(uiMessages[0].parts.filter((p) => p.type === "text")).toHaveLength( 1, ); expect(uiMessages[1].role).toBe("assistant"); expect( uiMessages[1].parts.filter((p) => p.type === "tool-myTool"), ).toHaveLength(1); expect( uiMessages[1].parts.filter((p) => p.type === "tool-myTool")[0], ).toMatchObject({ type: "tool-myTool", toolCallId: "call1", state: "output-available", output: "42", }); }); it("handles multiple text and reasoning parts", () => { const messages = [ baseMessageDoc({ message: { role: "assistant", content: [ { type: "reasoning", text: "I'm thinking...", }, { type: "text", text: "Here's one idea.", }, { type: "reasoning", text: "I'm thinking...", }, { type: "text", text: "Here's another idea.", }, ], }, reasoning: "I'm thinking... I'm thinking...", text: "Here's one idea. Here's another idea.", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); expect(uiMessages[0].text).toBe("Here's one idea. Here's another idea."); expect(uiMessages[0].parts.filter((p) => p.type === "reasoning")).toEqual([ { providerOptions: undefined, state: "done", text: "I'm thinking...", type: "reasoning", }, { providerOptions: undefined, state: "done", text: "I'm thinking...", type: "reasoning", }, ]); expect(uiMessages[0].parts[0].type).toBe("reasoning"); assert(uiMessages[0].parts[0].type === "reasoning"); expect(uiMessages[0].parts[0].text).toBe("I'm thinking..."); expect(uiMessages[0].parts[1].type).toBe("text"); assert(uiMessages[0].parts[1].type === "text"); expect(uiMessages[0].parts[1].text).toBe("Here's one idea."); expect(uiMessages[0].parts[2].type).toBe("reasoning"); assert(uiMessages[0].parts[2].type === "reasoning"); expect(uiMessages[0].parts[2].text).toBe("I'm thinking..."); expect(uiMessages[0].parts.filter((p) => p.type === "text")).toHaveLength( 2, ); expect(uiMessages[0].parts.filter((p) => p.type === "text")[0].text).toBe( "Here's one idea.", ); expect(uiMessages[0].parts.filter((p) => p.type === "text")[1].text).toBe( "Here's another idea.", ); }); it("combines text from between messages", () => { const messages = [ baseMessageDoc({ message: { role: "assistant", content: [ { type: "reasoning", text: "I'm thinking...", }, { type: "text", text: "I'm going to ask a question.", }, { type: "tool-call", input: "What's the meaning of life?", args: "What's the meaning of life?", toolCallId: "call1", toolName: "myTool", }, ], }, reasoning: "I'm thinking...", text: "Here's one idea.", tool: true, order: 1, stepOrder: 1, }), baseMessageDoc({ message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "myTool", output: { type: "text", value: "42", }, }, ], }, text: "", tool: true, order: 1, stepOrder: 2, }), baseMessageDoc({ message: { role: "assistant", content: [ { type: "reasoning", text: "Thinking again...", }, { type: "text", text: "Ok now I know.", }, ], }, text: "One last thing.", order: 1, stepOrder: 3, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); expect(uiMessages[0].text).toBe( "I'm going to ask a question. Ok now I know.", ); }); it("handles system message", () => { const messages = [ baseMessageDoc({ message: { role: "system", content: "System message here", }, text: "System message here", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("system"); expect(uiMessages[0].text).toBe("System message here"); expect(uiMessages[0].parts[0]).toEqual({ type: "text", text: "System message here", state: "done", providerMetadata: undefined, }); }); it("handles wrapped JSON tool output", () => { const messages = [ baseMessageDoc({ message: { role: "assistant", content: [ { type: "tool-call", toolName: "myTool", toolCallId: "call1", input: { query: "test" }, args: { query: "test" }, }, ], }, tool: true, }), baseMessageDoc({ message: { role: "tool", content: [ { type: "tool-result", toolName: "myTool", toolCallId: "call1", output: { type: "json", value: { data: "wrapped result", success: true }, }, }, ], }, tool: true, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); const toolPart = uiMessages[0].parts.find((p) => p.type === "tool-myTool"); expect(toolPart).toMatchObject({ type: "tool-myTool", toolCallId: "call1", state: "output-available", input: { query: "test" }, output: { data: "wrapped result", success: true }, // Should be unwrapped }); }); it("handles tool call", () => { const messages = [ baseMessageDoc({ message: { role: "assistant", content: [ { type: "tool-call", toolName: "myTool", toolCallId: "call1", input: "hi", args: "hi", }, ], }, text: "", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); expect( uiMessages[0].parts.filter((p) => p.type === "tool-myTool")[0], ).toMatchObject({ type: "tool-myTool", toolCallId: "call1", input: "hi", state: "input-available", }); }); it("handles tool result", () => { const messages = [ baseMessageDoc({ tool: true, message: { role: "assistant", content: [ { type: "tool-call", toolName: "myTool", toolCallId: "call1", input: "", args: "", }, ], }, text: "", }), baseMessageDoc({ message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "myTool", result: "42", }, ], }, text: "", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); // Should have a tool-invocation part expect(uiMessages[0].parts.some((p) => p.type === "tool-myTool")).toBe( true, ); }); it("does not duplicate text content", () => { const messages = [ baseMessageDoc({ message: { role: "assistant", content: "Hello!", }, text: "Hello!", }), ]; const uiMessages = toUIMessages(messages); // There should only be one text part const textParts = uiMessages[0].parts.filter((p) => p.type === "text"); expect(textParts).toHaveLength(1); expect(textParts[0].text).toBe("Hello!"); }); it("sets text field correctly when message has many parts with text part as final part", () => { const messages = [ baseMessageDoc({ message: { role: "assistant", content: [ { type: "reasoning", text: "Let me think about this...", }, { type: "tool-call", toolName: "calculator", toolCallId: "call1", input: { operation: "add", a: 2, b: 3 }, args: { operation: "add", a: 2, b: 3 }, }, { type: "file", mimeType: "application/json", data: "some-file-data", }, { type: "text", text: "Here's my final answer.", }, ], }, text: "Here's my final answer.", reasoning: "Let me think about this...", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); expect(uiMessages[0].text).toBe("Here's my final answer."); // Verify that the text part exists in parts const textParts = uiMessages[0].parts.filter((p) => p.type === "text"); expect(textParts).toHaveLength(1); expect(textParts[0].text).toBe("Here's my final answer."); }); // Add more tests for array content, tool calls, etc. as needed it("should update tool call state from input-available to output-available", () => { const messages = [ baseMessageDoc({ message: { role: "assistant", content: [ { type: "tool-call", toolName: "calculator", toolCallId: "call1", input: { operation: "add", a: 1, b: 2 }, args: { operation: "add", a: 1, b: 2 }, }, ], }, tool: true, }), baseMessageDoc({ message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "calculator", result: { sum: 3 }, }, ], }, tool: true, }), ]; const uiMessages = toUIMessages(messages); // Should have one assistant message expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); // Should have a single tool-calculator part (not separate tool-call and tool-result parts) const toolParts = uiMessages[0].parts.filter( (p) => p.type === "tool-calculator", ); expect(toolParts).toHaveLength(1); const toolPart = toolParts[0]; expect(toolPart).toMatchObject({ type: "tool-calculator", toolCallId: "call1", state: "output-available", input: { operation: "add", a: 1, b: 2 }, output: { sum: 3 }, }); // Should NOT have a tool-call part (which is what currently happens) const toolCallParts = uiMessages[0].parts.filter( (p) => p.type === "tool-call", ); expect(toolCallParts).toHaveLength(0); }); it("sets text field correctly in multi-step tool-call + final-text flow", () => { const messages = [ baseMessageDoc({ text: "what time is it in paris?", message: { role: "user", content: "what time is it in paris?", }, }), baseMessageDoc({ tool: true, finishReason: "tool-calls", text: "", stepOrder: 1, message: { role: "assistant", content: [ { type: "reasoning", text: "**Finding the Time**\n\nI've pinpointed the core task: obtaining the current time in Paris. It involves using the `dateTime` tool. I've identified \"Europe/Paris\" as the necessary timezone identifier to provide to the tool. My next step is to test the tool.\n\n\n", }, { input: { timezone: "Europe/Paris", }, args: { timezone: "Europe/Paris", }, type: "tool-call", toolName: "dateTime", toolCallId: "tool_0_dateTime", }, ], }, reasoning: "**Finding the Time**\n\nI've pinpointed the core task: obtaining the current time in Paris. It involves using the `dateTime` tool. I've identified \"Europe/Paris\" as the necessary timezone identifier to provide to the tool. My next step is to test the tool.\n\n\n", reasoningDetails: [ { text: "**Finding the Time**\n\nI've pinpointed the core task: obtaining the current time in Paris. It involves using the `dateTime` tool. I've identified \"Europe/Paris\" as the necessary timezone identifier to provide to the tool. My next step is to test the tool.\n\n\n", type: "reasoning", }, ], warnings: [], }), baseMessageDoc({ tool: true, stepOrder: 2, message: { role: "tool", content: [ { type: "tool-result", toolCallId: "tool_0_dateTime", toolName: "dateTime", result: { type: "json", value: { day: "20", hours: 16, minutes: 3, month: "August", year: "2025", }, }, }, ], }, sources: [], }), baseMessageDoc({ finishReason: "stop", text: "The time in Paris, France is 4:03 PM on August 20, 2025.", stepOrder: 3, message: { role: "assistant", content: [ { type: "text", text: "The time in Paris, France is 4:03 PM on August 20, 2025.", }, ], }, reasoningDetails: [], sources: [], warnings: [], }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(2); expect(uiMessages[0].role).toBe("user"); expect(uiMessages[0].text).toBe("what time is it in paris?"); expect(uiMessages[1].role).toBe("assistant"); expect(uiMessages[1].text).toBe( "The time in Paris, France is 4:03 PM on August 20, 2025.", ); }); it("handles messages in reverse order (assistant response, then tool response, then tool call)", () => { const messages = [ // Final assistant response comes first (stepOrder 3) - no tool flag since it's the final message baseMessageDoc({ _id: "msg3", order: 1, stepOrder: 3, finishReason: "stop", text: "The result is 42.", tool: false, // This is the final message without tool calls message: { role: "assistant", content: [ { type: "text", text: "The result is 42.", }, ], }, }), // Tool result comes second (stepOrder 2) baseMessageDoc({ _id: "msg2", order: 1, stepOrder: 2, tool: true, message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "calculator", output: { type: "text", value: "42", }, }, ], }, }), // Tool call comes last (stepOrder 1) baseMessageDoc({ _id: "msg1", order: 1, stepOrder: 1, tool: true, text: "", message: { role: "assistant", content: [ { type: "tool-call", toolName: "calculator", toolCallId: "call1", input: { operation: "add", a: 40, b: 2 }, args: { operation: "add", a: 40, b: 2 }, }, ], }, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); // Should concatenate text from all messages (only the final response has text) expect(uiMessages[0].text).toBe("The result is 42."); // Should use first message's fields (msg1 with stepOrder 1) expect(uiMessages[0].id).toBe("msg1"); expect(uiMessages[0].order).toBe(1); expect(uiMessages[0].stepOrder).toBe(1); // Should have both tool call and result parts const toolParts = uiMessages[0].parts.filter( (p) => p.type === "tool-calculator", ); expect(toolParts).toHaveLength(1); expect(toolParts[0]).toMatchObject({ type: "tool-calculator", toolCallId: "call1", state: "output-available", input: { operation: "add", a: 40, b: 2 }, output: "42", }); // Should also have text part const textParts = uiMessages[0].parts.filter((p) => p.type === "text"); expect(textParts).toHaveLength(1); expect(textParts[0].text).toBe("The result is 42."); }); it("shows output-error state when tool result has isError: true (issue #162)", () => { const messages = [ // Tool call baseMessageDoc({ _id: "msg1", order: 1, stepOrder: 1, tool: true, message: { role: "assistant", content: [ { type: "tool-call", toolName: "generateImage", toolCallId: "call1", input: { id: "invalid-id" }, args: { id: "invalid-id" }, }, ], }, }), // Tool result with error baseMessageDoc({ _id: "msg2", order: 1, stepOrder: 2, tool: true, message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "generateImage", output: { type: "text", value: 'ArgumentValidationError: Value does not match validator.\nPath: .id\nValue: "invalid-id"\nValidator: v.id("images")', }, isError: true, }, ], }, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); const toolParts = uiMessages[0].parts.filter( (p) => p.type === "tool-generateImage", ); expect(toolParts).toHaveLength(1); const toolPart = toolParts[0] as any; expect(toolPart.toolCallId).toBe("call1"); // Should show output-error, not output-available expect(toolPart.state).toBe("output-error"); expect(toolPart.output).toContain("ArgumentValidationError"); }); it("shows output-error when tool result has isError: true without tool call present (issue #162)", () => { // This simulates the case where the tool-call message wasn't saved const messages = [ baseMessageDoc({ _id: "msg1", order: 1, stepOrder: 1, tool: true, message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "generateImage", output: { type: "text", value: "Error: Something went wrong", }, isError: true, }, ], }, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); const toolParts = uiMessages[0].parts.filter( (p) => p.type === "tool-generateImage", ); expect(toolParts).toHaveLength(1); const toolPart = toolParts[0] as any; expect(toolPart.state).toBe("output-error"); }); describe("userId preservation", () => { it("preserves userId in user messages", () => { const messages = [ baseMessageDoc({ userId: "user123", message: { role: "user", content: "Hello!", }, text: "Hello!", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("user"); expect(uiMessages[0].userId).toBe("user123"); }); it("preserves userId in system messages", () => { const messages = [ baseMessageDoc({ userId: "user456", message: { role: "system", content: "System prompt", }, text: "System prompt", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("system"); expect(uiMessages[0].userId).toBe("user456"); }); it("preserves userId in assistant messages", () => { const messages = [ baseMessageDoc({ userId: "user789", message: { role: "assistant", content: "Hi there!", }, text: "Hi there!", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); expect(uiMessages[0].userId).toBe("user789"); }); it("preserves userId from first message in grouped assistant messages", () => { const messages = [ baseMessageDoc({ _id: "msg1", userId: "userA", order: 1, stepOrder: 1, tool: true, message: { role: "assistant", content: [ { type: "tool-call", toolName: "myTool", toolCallId: "call1", input: {}, args: {}, }, ], }, text: "", }), baseMessageDoc({ _id: "msg2", userId: "userA", order: 1, stepOrder: 2, tool: true, message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "myTool", output: { type: "text", value: "result" }, }, ], }, text: "", }), baseMessageDoc({ _id: "msg3", userId: "userA", order: 1, stepOrder: 3, message: { role: "assistant", content: "Done!", }, text: "Done!", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].role).toBe("assistant"); expect(uiMessages[0].userId).toBe("userA"); }); it("handles undefined userId gracefully", () => { const messages = [ baseMessageDoc({ // No userId provided message: { role: "user", content: "Hello!", }, text: "Hello!", }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); expect(uiMessages[0].userId).toBeUndefined(); }); }); describe("tool approval workflow", () => { it("sets state to approval-requested when tool-approval-request is present", () => { const messages = [ baseMessageDoc({ _id: "msg1", order: 1, stepOrder: 1, tool: true, message: { role: "assistant", content: [ { type: "tool-call", toolName: "dangerousTool", toolCallId: "call1", input: { action: "delete" }, args: { action: "delete" }, }, { type: "tool-approval-request", approvalId: "approval1", toolCallId: "call1", }, ], }, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); const toolPart = uiMessages[0].parts.find( (p) => p.type === "tool-dangerousTool", ) as any; expect(toolPart).toBeDefined(); expect(toolPart.state).toBe("approval-requested"); expect(toolPart.approval).toEqual({ id: "approval1" }); }); it("sets state to approval-responded when tool-approval-response with approved: true", () => { const messages = [ baseMessageDoc({ _id: "msg1", order: 1, stepOrder: 1, tool: true, message: { role: "assistant", content: [ { type: "tool-call", toolName: "dangerousTool", toolCallId: "call1", input: { action: "delete" }, args: { action: "delete" }, }, { type: "tool-approval-request", approvalId: "approval1", toolCallId: "call1", }, ], }, }), baseMessageDoc({ _id: "msg2", order: 1, stepOrder: 2, tool: true, message: { role: "tool", content: [ { type: "tool-approval-response", approvalId: "approval1", approved: true, reason: "User confirmed", }, ], }, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); const toolPart = uiMessages[0].parts.find( (p) => p.type === "tool-dangerousTool", ) as any; expect(toolPart).toBeDefined(); expect(toolPart.state).toBe("approval-responded"); expect(toolPart.approval).toEqual({ id: "approval1", approved: true, reason: "User confirmed", }); }); it("sets state to output-denied when tool-approval-response with approved: false", () => { const messages = [ baseMessageDoc({ _id: "msg1", order: 1, stepOrder: 1, tool: true, message: { role: "assistant", content: [ { type: "tool-call", toolName: "dangerousTool", toolCallId: "call1", input: { action: "delete" }, args: { action: "delete" }, }, { type: "tool-approval-request", approvalId: "approval1", toolCallId: "call1", }, ], }, }), baseMessageDoc({ _id: "msg2", order: 1, stepOrder: 2, tool: true, message: { role: "tool", content: [ { type: "tool-approval-response", approvalId: "approval1", approved: false, reason: "User declined the operation", }, ], }, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); const toolPart = uiMessages[0].parts.find( (p) => p.type === "tool-dangerousTool", ) as any; expect(toolPart).toBeDefined(); expect(toolPart.state).toBe("output-denied"); expect(toolPart.approval).toEqual({ id: "approval1", approved: false, reason: "User declined the operation", }); }); it("sets state to output-denied when tool-result has execution-denied output", () => { const messages = [ baseMessageDoc({ _id: "msg1", order: 1, stepOrder: 1, tool: true, message: { role: "assistant", content: [ { type: "tool-call", toolName: "dangerousTool", toolCallId: "call1", input: { action: "delete" }, args: { action: "delete" }, }, ], }, }), baseMessageDoc({ _id: "msg2", order: 1, stepOrder: 2, tool: true, message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "dangerousTool", output: { type: "execution-denied", reason: "Tool execution was denied by the user", }, }, ], }, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); const toolPart = uiMessages[0].parts.find( (p) => p.type === "tool-dangerousTool", ) as any; expect(toolPart).toBeDefined(); expect(toolPart.state).toBe("output-denied"); expect(toolPart.approval).toEqual({ id: "", approved: false, reason: "Tool execution was denied by the user", }); }); it("handles full approval flow: request → approved → executed → output-available", () => { const messages = [ baseMessageDoc({ _id: "msg1", order: 1, stepOrder: 1, tool: true, message: { role: "assistant", content: [ { type: "tool-call", toolName: "dangerousTool", toolCallId: "call1", input: { action: "delete" }, args: { action: "delete" }, }, { type: "tool-approval-request", approvalId: "approval1", toolCallId: "call1", }, ], }, }), baseMessageDoc({ _id: "msg2", order: 1, stepOrder: 2, tool: true, message: { role: "tool", content: [ { type: "tool-approval-response", approvalId: "approval1", approved: true, }, ], }, }), baseMessageDoc({ _id: "msg3", order: 1, stepOrder: 3, tool: true, message: { role: "tool", content: [ { type: "tool-result", toolCallId: "call1", toolName: "dangerousTool", output: { type: "json", value: { deleted: true }, }, }, ], }, }), ]; const uiMessages = toUIMessages(messages); expect(uiMessages).toHaveLength(1); const toolPart = uiMessages[0].parts.find( (p) => p.type === "tool-dangerousTool", ) as any; expect(toolPart).toBeDefined(); // After tool-result, state should be output-available expect(toolPart.state).toBe("output-available"); expect(toolPart.output).toEqual({ deleted: true }); // approval should still be preserved from earlier expect(toolPart.approval).toEqual({ id: "approval1", approved: true, reason: undefined, }); }); }); });