import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { describe, expect, it, vi } from "vitest"
import type { ToolPart } from "../types/parts"
import { AgentTimeline, type AgentTimelineItem } from "./agent-timeline"
const editPart: ToolPart = {
type: "tool",
id: "tool-1",
tool: "edit",
state: {
status: "completed",
input: { path: "src/batch-writer.ts" },
output: "Applied 1 edit",
},
}
const readPart: ToolPart = {
type: "tool",
id: "tool-2",
tool: "read",
state: {
status: "completed",
input: { path: "src/lib/jitter.ts" },
output: "export function jitter() {}",
},
}
function toolItem(part: ToolPart): AgentTimelineItem {
return {
id: "item-1",
kind: "tool",
call: {
id: part.id,
type: "edit",
label: "Edit src/batch-writer.ts",
status: "success",
detail: "src/batch-writer.ts",
output: "Applied 1 edit",
},
part,
}
}
describe("AgentTimeline tool actions", () => {
it("renders a tool call's label and clean detail", () => {
render()
expect(screen.getByText("Edit src/batch-writer.ts")).toBeInTheDocument()
expect(screen.getByText("src/batch-writer.ts")).toBeInTheDocument()
})
it("renders renderToolActions beside the tool item, called with the source part", () => {
const renderToolActions = vi.fn((part: ToolPart) => (
))
render(
,
)
expect(renderToolActions).toHaveBeenCalledWith(editPart)
expect(
screen.getByRole("button", { name: /open tool-1/i }),
).toBeInTheDocument()
})
it("renders no action when the item carries no source part", () => {
const renderToolActions = vi.fn(() => )
const item = { ...toolItem(editPart), part: undefined }
render(
,
)
expect(renderToolActions).not.toHaveBeenCalled()
expect(screen.queryByRole("button", { name: /open/i })).not.toBeInTheDocument()
})
it("renders tool items unchanged when no renderToolActions is provided", () => {
render()
expect(screen.getByText("Edit src/batch-writer.ts")).toBeInTheDocument()
expect(screen.queryByRole("button", { name: /open/i })).not.toBeInTheDocument()
})
it("renders renderToolActions for each call in a tool group, with its part", () => {
const renderToolActions = vi.fn((part: ToolPart) => (
))
const groupItem: AgentTimelineItem = {
id: "group-1",
kind: "tool_group",
title: "Tool activity",
calls: [
{
id: "tool-1",
type: "edit",
label: "Edit src/batch-writer.ts",
status: "success",
detail: "src/batch-writer.ts",
},
{
id: "tool-2",
type: "read",
label: "Read src/lib/jitter.ts",
status: "success",
detail: "src/lib/jitter.ts",
},
],
parts: [editPart, readPart],
}
render(
,
)
expect(renderToolActions).toHaveBeenCalledWith(editPart)
expect(renderToolActions).toHaveBeenCalledWith(readPart)
expect(
screen.getByRole("button", { name: /open tool-1/i }),
).toBeInTheDocument()
expect(
screen.getByRole("button", { name: /open tool-2/i }),
).toBeInTheDocument()
})
it("renders every row and no toggle when collapseAfter is unset", () => {
const items: AgentTimelineItem[] = Array.from({ length: 5 }, (_, i) => ({
id: `s-${i}`,
kind: "status",
label: `Step ${i}`,
}))
render()
expect(screen.getByText("Step 4")).toBeInTheDocument()
expect(
screen.queryByRole("button", { name: /more step/i }),
).not.toBeInTheDocument()
})
it("collapses to the first N spine rows behind a toggle and reveals the rest", async () => {
const user = userEvent.setup()
const items: AgentTimelineItem[] = Array.from({ length: 5 }, (_, i) => ({
id: `s-${i}`,
kind: "status",
label: `Step ${i}`,
}))
render()
// First 2 visible, the other 3 hidden behind the toggle
expect(screen.getByText("Step 0")).toBeInTheDocument()
expect(screen.getByText("Step 1")).toBeInTheDocument()
expect(screen.queryByText("Step 4")).not.toBeInTheDocument()
const toggle = screen.getByRole("button", { name: /show 3 more steps/i })
await user.click(toggle)
expect(screen.getByText("Step 4")).toBeInTheDocument()
expect(screen.getByRole("button", { name: /show less/i })).toBeInTheDocument()
})
it("renders the source part's real input in the expanded detail", async () => {
const user = userEvent.setup()
const probePart: ToolPart = {
type: "tool",
id: "tool-3",
tool: "custom_probe",
state: {
status: "completed",
input: { marker: "deep-input-value" },
output: "done",
},
}
const item: AgentTimelineItem = {
id: "item-3",
kind: "tool",
call: {
id: "tool-3",
type: "unknown",
label: "custom_probe",
status: "success",
detail: "probe",
},
part: probePart,
}
render()
await user.click(screen.getByRole("button", { name: /custom_probe/i }))
// ExpandedToolDetail (fed the real part) renders labelled Input/Output
// sections — the synthesized-part path showed only a bare output block.
expect(screen.getByText("Input")).toBeInTheDocument()
expect(screen.getByText("Output")).toBeInTheDocument()
})
})