import { act, fireEvent, render, screen } from "@testing-library/react"; import type { AgentActivityTurn } from "@tutti-os/agent-activity-core"; import type { JSX } from "react"; import { describe, expect, it, vi } from "vitest"; import type { AgentTranscriptRowVM } from "../contracts/agentTranscriptRowVM"; import type { AgentTranscriptTurnGroup } from "./agentTranscriptModel"; import { AgentTurnWorkSection as AgentTurnWorkSectionView } from "./AgentTurnWorkSection"; import type { AgentTurnDisclosureStore } from "./AgentTurnDisclosureContext"; import { buildAgentTurnWorkSectionModel } from "./agentTurnWorkSectionModel"; vi.mock("../../../i18n/index", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string, options?: Record) => key === "agentHost.agentGui.turnProcessedSeconds" ? `Processed for ${String(options?.seconds)}s` : key === "agentHost.agentGui.turnPeerDeviceOfflinePendingSync" ? "Other device offline · Progress pending sync" : key === "agentHost.agentGui.turnPeerDeviceProgressSynchronizing" ? "Synchronizing progress from other device…" : key }) }; }); const disclosureStore: AgentTurnDisclosureStore = { expandedOverrides: {}, setExpandedOverride: () => {} }; describe("AgentTurnWorkSection", () => { it("ticks only the duration label without rerendering transcript rows", () => { vi.useFakeTimers(); vi.setSystemTime(50_000); const renderRow = vi.fn((row: AgentTranscriptRowVM, rowIndex: number) => (
{row.id}
)); try { render( ); expect(screen.getByText("Processed for 45s")).toBeTruthy(); expect(vi.getTimerCount()).toBe(1); renderRow.mockClear(); act(() => vi.advanceTimersByTime(5_000)); expect(screen.getByText("Processed for 50s")).toBeTruthy(); expect(renderRow).not.toHaveBeenCalled(); } finally { vi.useRealTimers(); } }); it.each([ ["peer-offline" as const, "Other device offline · Progress pending sync"], ["synchronizing" as const, "Synchronizing progress from other device…"] ])("shows %s observation-gap copy instead of a live timer", (state, copy) => { vi.useFakeTimers(); vi.setSystemTime(50_000); try { render( (
{row.id}
)} /> ); expect(screen.getByText(copy)).toBeTruthy(); expect(screen.queryByText("Processed for 45s")).toBeNull(); expect(vi.getTimerCount()).toBe(0); } finally { vi.useRealTimers(); } }); it("shows and ticks wall-clock time while the active turn is waiting", () => { vi.useFakeTimers(); vi.setSystemTime(50_000); try { render( (
{row.id}
)} /> ); expect(screen.getByText("Processed for 45s")).toBeTruthy(); expect(vi.getTimerCount()).toBe(1); act(() => vi.advanceTimersByTime(5_000)); expect(screen.getByText("Processed for 50s")).toBeTruthy(); } finally { vi.useRealTimers(); } }); it("keeps the live timer when a running turn transitions to waiting", () => { vi.useFakeTimers(); vi.setSystemTime(50_000); const renderRow = (row: AgentTranscriptRowVM, rowIndex: number) => (
{row.id}
); try { const { rerender } = render( ); expect(screen.getByText("Processed for 45s")).toBeTruthy(); expect(vi.getTimerCount()).toBe(1); rerender( ); expect(screen.getByText("Processed for 45s")).toBeTruthy(); expect(vi.getTimerCount()).toBe(1); act(() => vi.advanceTimersByTime(5_000)); expect(screen.getByText("Processed for 50s")).toBeTruthy(); } finally { vi.useRealTimers(); } }); it("clears the live timer and freezes timing when the turn settles", () => { vi.useFakeTimers(); vi.setSystemTime(50_000); const renderRow = (row: AgentTranscriptRowVM, rowIndex: number) => (
{row.id}
); try { const { rerender } = render( ); expect(vi.getTimerCount()).toBe(1); rerender( ); expect(vi.getTimerCount()).toBe(0); } finally { vi.useRealTimers(); } }); it("renders interleaved turn rows in their original chronology", () => { const expandedStore: AgentTurnDisclosureStore = { expandedOverrides: { "session-1:turn-1": true }, setExpandedOverride: () => {} }; const { container } = render( (
{row.kind === "message" ? row.messages[0]?.body : row.id}
)} /> ); expect( [...container.querySelectorAll("[data-test-row-id]")].map( (element) => element.textContent ) ).toEqual([ "First request", "Earlier answer", "Follow-up", "tools", "Final answer" ]); }); it("collapses ordinary assistant replies while keeping follow-up guidance visible", () => { render( (
{row.kind === "message" ? row.messages[0]?.body : row.id}
)} /> ); expect(screen.queryByText("Earlier answer")).toBeNull(); expect(screen.getByText("Follow-up")).toBeTruthy(); expect(screen.getByText("Final answer")).toBeTruthy(); expect(screen.queryByText("tools")).toBeNull(); }); it("keeps the response-tail file diff panel visible when turn work is collapsed", () => { const group = interleavedTurnGroup(); const summary: AgentTranscriptRowVM = { kind: "turn-summary", id: "file-diff", turnId: "turn-1", files: [], fileCount: 0, modifiedCount: 0, createdCount: 0, occurredAtUnixMs: 15_000 }; group.rows.push({ row: summary, rowIndex: group.rows.length }); const { container } = render( (
{row.kind === "message" ? row.messages[0]?.body : row.id}
)} /> ); expect(screen.queryByText("tools")).toBeNull(); expect(screen.getByText("Final answer")).toBeTruthy(); expect(screen.getByText("file-diff")).toBeTruthy(); expect( [...container.querySelectorAll("[data-test-row-id]")].map( (element) => element.textContent ) ).toEqual(["First request", "Follow-up", "Final answer", "file-diff"]); }); it("makes the duration text part of the turn disclosure button", () => { const setExpandedOverride = vi.fn(); render(
{row.id}
} /> ); const duration = screen.getByText("agentHost.agentGui.turnTotalSeconds"); const toggle = screen.getByRole("button", { name: "agentHost.agentGui.expandTurnWork" }); expect(toggle).toContainElement(duration); fireEvent.click(duration); expect(setExpandedOverride).toHaveBeenCalledWith("session-1:turn-1", true); }); it("keeps dynamic section spacing inside the animated height", () => { const expandedStore: AgentTurnDisclosureStore = { expandedOverrides: { "session-1:turn-1": true }, setExpandedOverride: () => {} }; const { container } = render(
{row.id}
} /> ); expect( container.querySelector("[data-agent-turn-work-section]") ).toHaveClass("grid"); expect( container.querySelector("[data-agent-turn-work-section]") ).not.toHaveClass("gap-4"); expect( container.querySelector(".agent-collapsible-reveal__inner") ).toHaveClass("grid", "gap-4", "pt-4"); }); }); function AgentTurnWorkSection({ group, sessionId, turn, isActiveTurn, observationGapPresentationState, disclosureStore, renderRow }: { group: AgentTranscriptTurnGroup; sessionId: string; turn: AgentActivityTurn; isActiveTurn: boolean; observationGapPresentationState?: "peer-offline" | "synchronizing"; disclosureStore: AgentTurnDisclosureStore; renderRow: ( row: AgentTranscriptRowVM, rowIndex: number, renderKey?: string ) => JSX.Element; }): JSX.Element { const model = buildAgentTurnWorkSectionModel(group, turn, isActiveTurn, { collapseIntermediateAssistantReplies: true, liveObservationGapPresentationState: observationGapPresentationState }); if (!model) { throw new Error("Test expected a timing-enabled turn disclosure model"); } return ( ); } function canonicalTurn( overrides: Partial = {} ): AgentActivityTurn { return { agentSessionId: "session-1", origin: "user_prompt", phase: "running", startedAtUnixMs: 5_000, turnId: "turn-1", updatedAtUnixMs: 6_000, ...overrides }; } function turnGroup(): AgentTranscriptTurnGroup { const row: AgentTranscriptRowVM = { kind: "message", id: "user-row", turnId: "turn-1", speaker: "user", messages: [ { kind: "message-content", id: "user-message", turnId: "turn-1", body: "Please fix it", presentationKind: "content", occurredAtUnixMs: 5_000 } ], thinking: [], occurredAtUnixMs: 5_000 }; return { key: "turn-1", turnId: "turn-1", rows: [{ row, rowIndex: 0 }] }; } function interleavedTurnGroup(): AgentTranscriptTurnGroup { const messageRow = ( id: string, speaker: "user" | "assistant", body: string, isTurnFinalText = false ): AgentTranscriptRowVM => ({ kind: "message", id, turnId: "turn-1", speaker, messages: [ { kind: "message-content", id: `${id}:message`, turnId: "turn-1", body, presentationKind: "content", copyText: isTurnFinalText ? body : null, ...(isTurnFinalText ? { isTurnFinalText: true as const } : {}), occurredAtUnixMs: 5_000 } ], thinking: [], occurredAtUnixMs: 5_000 }); const rows: AgentTranscriptRowVM[] = [ messageRow("user-1", "user", "First request"), messageRow("assistant-1", "assistant", "Earlier answer"), messageRow("user-2", "user", "Follow-up"), { kind: "tool-group", id: "tools", turnId: "turn-1", grouped: true, calls: [], entries: [], occurredAtUnixMs: 8_000 }, messageRow("assistant-2", "assistant", "Final answer", true) ]; return { key: "turn-1", turnId: "turn-1", rows: rows.map((row, rowIndex) => ({ row, rowIndex })) }; }