import { mount } from "@vue/test-utils"; import { describe, expect, it } from "vitest"; import type { ActionSuggestion } from "@/composables/agentActions"; import { ActionType } from "@/composables/agentActions"; import type { ChatMessage } from "./chatTypes"; import ChatMessageCell from "./ChatMessageCell.vue"; function makeUserMessage(overrides: Partial = {}): ChatMessage { return { id: "msg-1", role: "user", content: "What tools can analyze my data?", timestamp: new Date(), feedback: null, ...overrides, }; } function makeAssistantMessage(overrides: Partial = {}): ChatMessage { return { id: "msg-2", role: "assistant", content: "You can use the **Dataset Analyzer** tool.", timestamp: new Date(), agentType: "auto", feedback: null, ...overrides, }; } const defaultRenderMarkdown = (text: string) => `

${text}

`; function mountCell(message: ChatMessage, props: Record = {}) { return mount(ChatMessageCell as any, { propsData: { message, renderMarkdown: defaultRenderMarkdown, processingAction: false, ...props, }, stubs: { FontAwesomeIcon: true, ActionCard: true, }, }); } describe("ChatMessageCell", () => { describe("user messages", () => { it("renders user query with content", () => { const wrapper = mountCell(makeUserMessage()); expect(wrapper.find(".exchange-entry").classes()).toContain("entry-query"); expect(wrapper.find(".query-text").text()).toBe("What tools can analyze my data?"); }); it("does not render feedback buttons", () => { const wrapper = mountCell(makeUserMessage()); expect(wrapper.find(".response-meta").exists()).toBe(false); }); }); describe("assistant messages", () => { it("renders response entry", () => { const wrapper = mountCell(makeAssistantMessage()); expect(wrapper.find(".exchange-entry").classes()).toContain("entry-response"); }); it("renders markdown via renderMarkdown prop", () => { const wrapper = mountCell(makeAssistantMessage()); expect(wrapper.find(".response-content").html()).toContain( "

You can use the **Dataset Analyzer** tool.

", ); }); it("shows agent label in metadata", () => { const wrapper = mountCell(makeAssistantMessage({ agentType: "error_analysis" })); const tags = wrapper.findAll(".meta-tag"); const labels = tags.wrappers.map((w) => w.text()); expect(labels.some((l) => l.includes("Error Analysis"))).toBe(true); }); }); describe("system messages", () => { it("renders system notice", () => { const wrapper = mountCell(makeAssistantMessage({ isSystemMessage: true, content: "Welcome" })); expect(wrapper.find(".system-notice").text()).toBe("Welcome"); }); it("does not render feedback for system messages", () => { const wrapper = mountCell(makeAssistantMessage({ isSystemMessage: true })); expect(wrapper.find(".response-meta").exists()).toBe(false); }); }); describe("feedback", () => { it("renders feedback buttons for normal assistant messages", () => { const wrapper = mountCell(makeAssistantMessage()); const buttons = wrapper.findAll(".feedback-btn"); expect(buttons.length).toBe(2); }); it("emits feedback event on thumbs-up click", async () => { const wrapper = mountCell(makeAssistantMessage()); const upBtn = wrapper.findAll(".feedback-btn").at(0); await upBtn!.trigger("click"); expect(wrapper.emitted("feedback")).toEqual([["msg-2", "up"]]); }); it("emits feedback event on thumbs-down click", async () => { const wrapper = mountCell(makeAssistantMessage()); const downBtn = wrapper.findAll(".feedback-btn").at(1); await downBtn!.trigger("click"); expect(wrapper.emitted("feedback")).toEqual([["msg-2", "down"]]); }); it("disables feedback buttons after feedback given", () => { const wrapper = mountCell(makeAssistantMessage({ feedback: "up" })); const buttons = wrapper.findAll(".feedback-btn"); expect((buttons.at(0)!.element as HTMLButtonElement).disabled).toBe(true); expect((buttons.at(1)!.element as HTMLButtonElement).disabled).toBe(true); }); it("shows thanks text after feedback", () => { const wrapper = mountCell(makeAssistantMessage({ feedback: "up" })); expect(wrapper.find(".feedback-ack").text()).toBe("Thanks!"); }); it("hides meta for error messages", () => { const wrapper = mountCell(makeAssistantMessage({ content: "❌ Something failed" })); expect(wrapper.find(".response-meta").exists()).toBe(false); }); }); describe("response metadata", () => { it("shows model name when metadata.model present", () => { const message = makeAssistantMessage({ agentResponse: { content: "test", agent_type: "auto", confidence: "high", suggestions: [], metadata: { model: "openai/gpt-4" }, }, }); const wrapper = mountCell(message); const tags = wrapper.findAll(".meta-tag"); const text = tags.wrappers.map((w) => w.text()).join(" "); expect(text).toContain("gpt-4"); }); it("shows token count when metadata.total_tokens present", () => { const message = makeAssistantMessage({ agentResponse: { content: "test", agent_type: "auto", confidence: "high", suggestions: [], metadata: { total_tokens: 150 }, }, }); const wrapper = mountCell(message); const tags = wrapper.findAll(".meta-tag"); const text = tags.wrappers.map((w) => w.text()).join(" "); expect(text).toContain("150 tok"); }); }); describe("action suggestions", () => { it("renders ActionCard when suggestions present", () => { const suggestions: ActionSuggestion[] = [ { action_type: ActionType.TOOL_RUN, description: "Run the tool", parameters: {}, confidence: "high", priority: 1, }, ]; const wrapper = mountCell(makeAssistantMessage({ suggestions })); expect(wrapper.find(".action-card").exists()).toBe(true); }); it("does not render ActionCard when no suggestions", () => { const wrapper = mountCell(makeAssistantMessage()); expect(wrapper.find(".action-card").exists()).toBe(false); }); it("emits handle-action with action and resolved agentResponse", async () => { const action: ActionSuggestion = { action_type: ActionType.TOOL_RUN, description: "Run tool", parameters: { tool_id: "filter1" }, confidence: "high", priority: 1, }; const agentResponse = { content: "test", agent_type: "auto", confidence: "high" as const, suggestions: [], metadata: {}, }; const message = makeAssistantMessage({ suggestions: [action], agentResponse, }); const wrapper = mount(ChatMessageCell as any, { propsData: { message, renderMarkdown: defaultRenderMarkdown, processingAction: false, }, stubs: { FontAwesomeIcon: true, }, }); await wrapper.find(".g-button").trigger("click"); const emitted = wrapper.emitted("handle-action"); expect(emitted).toHaveLength(1); expect(emitted![0]![0]).toEqual(action); expect(emitted![0]![1]).toBe(agentResponse); }); }); describe("slots", () => { it("renders after-content slot", () => { const wrapper = mount(ChatMessageCell as any, { propsData: { message: makeAssistantMessage(), renderMarkdown: defaultRenderMarkdown, processingAction: false, }, stubs: { FontAwesomeIcon: true, ActionCard: true, }, slots: { "after-content": "
Extra content
", }, }); expect(wrapper.find(".custom-slot").exists()).toBe(true); expect(wrapper.find(".custom-slot").text()).toBe("Extra content"); }); }); });