Files
storkit/frontend/src/components/WorkItemDetailPanel.test.tsx

345 lines
9.5 KiB
TypeScript
Raw Normal View History

import { act, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AgentEvent, AgentInfo } from "../api/agents";
import { agentsApi, subscribeAgentStream } from "../api/agents";
import { api } from "../api/client";
vi.mock("../api/client", () => ({
api: {
getWorkItemContent: vi.fn(),
},
}));
vi.mock("../api/agents", () => ({
agentsApi: {
listAgents: vi.fn(),
},
subscribeAgentStream: vi.fn(() => () => {}),
}));
// Dynamic import so mocks are in place before the module loads
const { WorkItemDetailPanel } = await import("./WorkItemDetailPanel");
const mockedGetWorkItemContent = vi.mocked(api.getWorkItemContent);
const mockedListAgents = vi.mocked(agentsApi.listAgents);
const mockedSubscribeAgentStream = vi.mocked(subscribeAgentStream);
const DEFAULT_CONTENT = {
content: "# Big Title\n\nSome content here.",
stage: "current",
name: "Big Title Story",
};
describe("WorkItemDetailPanel", () => {
beforeEach(() => {
vi.clearAllMocks();
mockedGetWorkItemContent.mockResolvedValue(DEFAULT_CONTENT);
mockedListAgents.mockResolvedValue([]);
mockedSubscribeAgentStream.mockReturnValue(() => {});
});
it("renders the story name in the header", async () => {
render(<WorkItemDetailPanel storyId="237_bug_test" onClose={() => {}} />);
await waitFor(() => {
expect(screen.getByTestId("detail-panel-title")).toHaveTextContent(
"Big Title Story",
);
});
});
it("shows loading state initially", () => {
render(<WorkItemDetailPanel storyId="237_bug_test" onClose={() => {}} />);
expect(screen.getByTestId("detail-panel-loading")).toBeInTheDocument();
});
it("calls onClose when close button is clicked", async () => {
const onClose = vi.fn();
render(<WorkItemDetailPanel storyId="237_bug_test" onClose={onClose} />);
const closeButton = screen.getByTestId("detail-panel-close");
closeButton.click();
expect(onClose).toHaveBeenCalledTimes(1);
});
it("renders markdown headings with constrained inline font size", async () => {
render(<WorkItemDetailPanel storyId="237_bug_test" onClose={() => {}} />);
await waitFor(() => {
const content = screen.getByTestId("detail-panel-content");
const h1 = content.querySelector("h1");
expect(h1).not.toBeNull();
expect(h1?.style.fontSize).toBeTruthy();
});
});
});
describe("WorkItemDetailPanel - Agent Logs", () => {
beforeEach(() => {
vi.clearAllMocks();
mockedGetWorkItemContent.mockResolvedValue(DEFAULT_CONTENT);
mockedListAgents.mockResolvedValue([]);
mockedSubscribeAgentStream.mockReturnValue(() => {});
});
it("shows placeholder when no agent is assigned to the story", async () => {
render(<WorkItemDetailPanel storyId="42_story_test" onClose={() => {}} />);
await screen.findByTestId("detail-panel-content");
const placeholder = screen.getByTestId("placeholder-agent-logs");
expect(placeholder).toBeInTheDocument();
expect(placeholder).toHaveTextContent("Coming soon");
});
it("shows agent name and running status when agent is running", async () => {
const agentList: AgentInfo[] = [
{
story_id: "42_story_test",
agent_name: "coder-1",
status: "running",
session_id: null,
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedListAgents.mockResolvedValue(agentList);
render(<WorkItemDetailPanel storyId="42_story_test" onClose={() => {}} />);
const statusBadge = await screen.findByTestId("agent-status-badge");
expect(statusBadge).toHaveTextContent("coder-1");
expect(statusBadge).toHaveTextContent("running");
});
it("shows log output when agent emits output events", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "42_story_test",
agent_name: "coder-1",
status: "running",
session_id: null,
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedListAgents.mockResolvedValue(agentList);
render(<WorkItemDetailPanel storyId="42_story_test" onClose={() => {}} />);
await screen.findByTestId("agent-status-badge");
await act(async () => {
emitEvent?.({
type: "output",
story_id: "42_story_test",
agent_name: "coder-1",
text: "Writing tests...",
});
});
const logOutput = screen.getByTestId("agent-log-output");
expect(logOutput).toHaveTextContent("Writing tests...");
});
it("appends multiple output events to the log", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "42_story_test",
agent_name: "coder-1",
status: "running",
session_id: null,
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedListAgents.mockResolvedValue(agentList);
render(<WorkItemDetailPanel storyId="42_story_test" onClose={() => {}} />);
await screen.findByTestId("agent-status-badge");
await act(async () => {
emitEvent?.({
type: "output",
story_id: "42_story_test",
agent_name: "coder-1",
text: "Line one\n",
});
});
await act(async () => {
emitEvent?.({
type: "output",
story_id: "42_story_test",
agent_name: "coder-1",
text: "Line two\n",
});
});
const logOutput = screen.getByTestId("agent-log-output");
expect(logOutput.textContent).toContain("Line one");
expect(logOutput.textContent).toContain("Line two");
});
it("updates status to completed after done event", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "42_story_test",
agent_name: "coder-1",
status: "running",
session_id: null,
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedListAgents.mockResolvedValue(agentList);
render(<WorkItemDetailPanel storyId="42_story_test" onClose={() => {}} />);
await screen.findByTestId("agent-status-badge");
await act(async () => {
emitEvent?.({
type: "done",
story_id: "42_story_test",
agent_name: "coder-1",
session_id: "session-123",
});
});
const statusBadge = screen.getByTestId("agent-status-badge");
expect(statusBadge).toHaveTextContent("completed");
});
it("shows failed status after error event", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "42_story_test",
agent_name: "coder-1",
status: "running",
session_id: null,
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedListAgents.mockResolvedValue(agentList);
render(<WorkItemDetailPanel storyId="42_story_test" onClose={() => {}} />);
await screen.findByTestId("agent-status-badge");
await act(async () => {
emitEvent?.({
type: "error",
story_id: "42_story_test",
agent_name: "coder-1",
message: "Process failed",
});
});
const statusBadge = screen.getByTestId("agent-status-badge");
expect(statusBadge).toHaveTextContent("failed");
const logOutput = screen.getByTestId("agent-log-output");
expect(logOutput.textContent).toContain("[ERROR] Process failed");
});
it("shows completed agent status without subscribing to stream", async () => {
const agentList: AgentInfo[] = [
{
story_id: "42_story_test",
agent_name: "coder-1",
status: "completed",
session_id: "session-123",
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedListAgents.mockResolvedValue(agentList);
render(<WorkItemDetailPanel storyId="42_story_test" onClose={() => {}} />);
const statusBadge = await screen.findByTestId("agent-status-badge");
expect(statusBadge).toHaveTextContent("completed");
expect(mockedSubscribeAgentStream).not.toHaveBeenCalled();
});
it("shows failed agent status for a failed agent without subscribing to stream", async () => {
const agentList: AgentInfo[] = [
{
story_id: "42_story_test",
agent_name: "coder-1",
status: "failed",
session_id: null,
worktree_path: null,
base_branch: "master",
log_session_id: null,
},
];
mockedListAgents.mockResolvedValue(agentList);
render(<WorkItemDetailPanel storyId="42_story_test" onClose={() => {}} />);
const statusBadge = await screen.findByTestId("agent-status-badge");
expect(statusBadge).toHaveTextContent("failed");
expect(mockedSubscribeAgentStream).not.toHaveBeenCalled();
});
it("shows agent logs section (not placeholder) when agent is assigned", async () => {
const agentList: AgentInfo[] = [
{
story_id: "42_story_test",
agent_name: "coder-1",
status: "running",
session_id: null,
worktree_path: "/tmp/wt",
base_branch: "master",
log_session_id: null,
},
];
mockedListAgents.mockResolvedValue(agentList);
render(<WorkItemDetailPanel storyId="42_story_test" onClose={() => {}} />);
await screen.findByTestId("agent-logs-section");
expect(
screen.queryByTestId("placeholder-agent-logs"),
).not.toBeInTheDocument();
});
});