import { useState } from "react"; import type { ReactNode } from "react"; import { fireEvent, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { RenderItem } from "@/hooks/use-processed-messages"; import type { Message } from "@/lib/types/http"; const rendererSpy = vi.fn(); const mockStoreState = vi.hoisted(() => ({ taskSessions: { items: { s1: { metadata: { last_agent_error: { message: "agent process exited", occurred_at: "2026-06-14T12:00:00Z", }, }, }, }, }, dismissedAgentErrors: {} as Record, dismissAgentError: () => {}, setTaskSession: () => {}, })); vi.mock("@/components/task/chat/message-renderer", () => ({ MessageRenderer: (props: { onOpenFile?: unknown }) => { rendererSpy(props); return
; }, })); vi.mock("@/components/task/chat/messages/turn-group-message", () => ({ TurnGroupMessage: () =>
, })); vi.mock("@/components/session/prepare-progress", () => ({ PrepareProgress: () =>
, })); vi.mock("@/components/state-provider", () => ({ useAppStore: (selector: (state: typeof mockStoreState) => unknown) => selector(mockStoreState), useAppStoreApi: () => ({ getState: () => mockStoreState, }), })); vi.mock("@/hooks/use-lazy-load-messages", () => ({ useLazyLoadMessages: () => ({ loadMore: async () => 0, hasMore: false, isLoading: false, }), })); vi.mock("@/components/task/chat/messages/agent-status", () => ({ AgentStatus: () =>
, })); vi.mock("@kandev/ui/pannel-session", () => ({ SessionPanelContent: ({ children }: { children: ReactNode }) => (
{children}
), })); vi.mock("@/lib/api/domains/session-api", () => ({ dismissLastAgentError: vi.fn(), })); import { MessageItem } from "./message-list-shared"; const item: RenderItem = { type: "message", message: { id: "m1" } as Message }; const noop = () => {}; const perm = new Map(); const kids = new Map(); function row(onOpenFile: (p: string) => void) { return ( ); } function Harness({ onOpenFile }: { onOpenFile: (p: string) => void }) { const [, setTick] = useState(0); return (
{row(onOpenFile)}
); } describe("MessageItem memo boundary", () => { afterEach(() => { rendererSpy.mockClear(); }); it("does not re-render the row when the parent re-renders with stable props", () => { const { getByText } = render(); expect(rendererSpy).toHaveBeenCalledTimes(1); fireEvent.click(getByText("tick")); fireEvent.click(getByText("tick")); expect(rendererSpy).toHaveBeenCalledTimes(1); // memo bailed on stable props }); it("re-renders the row when onOpenFile identity changes (stability requirement)", () => { const { rerender } = render(row(() => {})); expect(rendererSpy).toHaveBeenCalledTimes(1); rerender(row(() => {})); expect(rendererSpy).toHaveBeenCalledTimes(2); // fresh callback ref breaks memo }); }); describe("MessageItem agent error notice", () => { it("shows retained agent errors even when there are no messages", () => { render( , ); expect(screen.getByTestId("last-agent-error-notice").getAttribute("role")).toBe("alert"); expect(screen.queryByText("agent process exited")).not.toBeNull(); }); });