Files
IdeaSDK/frontend/src/features/workstate/workstate.test.tsx
Blomios 3d603cebcc feat(workstate): dette de contrat DTO work-state (frontend)
Aligne le contrat DTO du panneau work-state (#5).

- ajout summary: string|null à BackgroundCompletion, mappé depuis task.summary
- affichage discret du résumé sur une tâche terminale
- suppression du chemin de merge top-level mort des background tasks

Tests verts : vitest workstate.test.tsx (21), suite complète (507),
npm run build (exit 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:05:03 +02:00

948 lines
30 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { DIProvider } from "@/app/di";
import {
MockAgentGateway,
MockLayoutGateway,
MockSystemGateway,
MockWorkStateGateway,
} from "@/adapters/mock";
import { leaves } from "@/features/layout/layout";
import type { Gateways } from "@/ports";
import {
ProjectWorkStatePanel,
WorkStatePanelErrorBoundary,
} from "./ProjectWorkStatePanel";
const PROJECT_ID = "project-work-state-test";
function renderPanel(
workState: MockWorkStateGateway = new MockWorkStateGateway(),
system: MockSystemGateway = new MockSystemGateway(),
overrides: Partial<Gateways> = {},
) {
const gateways = { workState, system, ...overrides } as unknown as Gateways;
return {
workState,
system,
...render(
<DIProvider gateways={gateways}>
<ProjectWorkStatePanel projectId={PROJECT_ID} />
</DIProvider>,
),
};
}
describe("ProjectWorkStatePanel", () => {
it("renders the empty state", async () => {
renderPanel();
expect(await screen.findByText("No agent work state.")).toBeTruthy();
});
it("renders an idle offline agent", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-1",
name: "Planner",
profileId: "codex",
busy: { state: "idle" },
},
],
});
renderPanel(workState);
expect(await screen.findByText("Planner")).toBeTruthy();
expect(screen.getByText("Offline")).toBeTruthy();
expect(screen.getByText("Idle")).toBeTruthy();
expect(screen.queryByLabelText("Planner tickets")).toBeNull();
});
it("renders a live busy agent with a short ticket", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-2",
name: "Builder",
profileId: "claude",
live: {
nodeId: "node-a",
sessionId: "session-a",
kind: "pty",
},
busy: {
state: "busy",
ticket: "4c65d981-da5e-412d-85ed-3b9779b670fa",
sinceMs: 123,
},
},
],
});
renderPanel(workState);
expect(await screen.findByText("Builder")).toBeTruthy();
expect(screen.getByText("Live")).toBeTruthy();
expect(screen.getByText("Busy")).toBeTruthy();
expect(screen.getByLabelText("busy ticket 4c65d981")).toBeTruthy();
});
it("renders an in-progress human ticket", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-4",
name: "Implementer",
profileId: "codex",
busy: {
state: "busy",
ticket: "ticket-in-progress-abcdef",
sinceMs: 789,
},
tickets: [
{
ticketId: "ticket-in-progress-abcdef",
conversationId: "conversation-a",
position: 0,
status: "inProgress",
source: { kind: "human" },
requesterLabel: "Anthony",
taskPreview: "Wire the workstate queue UI",
taskLen: 36,
},
],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Implementer tickets");
expect(within(list).getByText("#1 In progress")).toBeTruthy();
expect(within(list).getByText("Human")).toBeTruthy();
expect(within(list).getByText("Wire the workstate queue UI")).toBeTruthy();
expect(within(list).getByLabelText("ticket ticket-i")).toBeTruthy();
expect(within(list).getByLabelText("9 more characters")).toBeTruthy();
});
it("renders two tickets in FIFO order", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-5",
name: "Reviewer",
profileId: "claude",
busy: { state: "idle" },
tickets: [
{
ticketId: "ticket-second-abcdef",
conversationId: "conversation-b",
position: 1,
status: "queued",
source: { kind: "agent", agentId: "agent-main-123456" },
requesterLabel: "Main",
taskPreview: "Then review the result",
taskLen: 22,
},
{
ticketId: "ticket-first-abcdef",
conversationId: "conversation-a",
position: 0,
status: "inProgress",
source: { kind: "agent", agentId: "agent-qa-123456" },
requesterLabel: "QA",
taskPreview: "Check behavior",
taskLen: 14,
},
],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Reviewer tickets");
expect(within(list).getByText("QA (agent-qa)")).toBeTruthy();
expect(within(list).getByText("Main (agent-ma)")).toBeTruthy();
expect(list.textContent?.indexOf("#1 In progress")).toBeLessThan(
list.textContent?.indexOf("#2 Queued") ?? -1,
);
});
it("normalizes the real backend background-task payload shape", async () => {
// Mirrors AgentBackgroundTaskStateDto (crates/app-tauri/src/dto.rs): camelCase,
// `state` (not `status`), optional exitCode/summary/stdoutTail/stderrTail omitted
// when absent, createdAtMs/updatedAtMs present, NO ownerAgentId/projectId/finishedAtMs.
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-bg",
name: "Runner",
profileId: "codex",
busy: { state: "idle" },
backgroundTasks: [
{
taskId: "task-waiting-1",
kind: "command",
state: "waiting",
createdAtMs: 10,
updatedAtMs: 11,
},
{
taskId: "task-queued-1",
kind: "headlessRendezvous",
state: "queued",
createdAtMs: 12,
updatedAtMs: 13,
},
{
taskId: "task-failed-1",
kind: "command",
state: "failed",
exitCode: 1,
summary: "process failed",
stderrTail: "boom",
createdAtMs: 14,
updatedAtMs: 15,
},
{
taskId: "task-expired-1",
kind: "sessionResume",
state: "expired",
createdAtMs: 16,
updatedAtMs: 17,
},
],
},
],
});
// Adapter-level assertion: locks the mapping against the real payload.
const state = await workState.getProjectWorkState(PROJECT_ID);
const tasks = state.agents[0]?.backgroundTasks ?? [];
expect(tasks.map((t) => [t.taskId, t.status])).toEqual([
["task-waiting-1", "pending"],
["task-queued-1", "pending"],
["task-failed-1", "failed"],
["task-expired-1", "cancelled"],
]);
// Optional fields omitted by the backend normalize to null (not undefined/NaN).
expect(tasks[0]?.exitCode).toBeNull();
expect(tasks[0]?.stdoutTail).toBeNull();
expect(tasks[0]?.summary).toBeNull();
expect(tasks[2]?.stderrTail).toBe("boom");
// `summary` (ticket #5) is carried through when present.
expect(tasks[2]?.summary).toBe("process failed");
// updatedAtMs (backend-emitted) is carried through for chronological ordering.
expect(tasks.map((t) => t.updatedAtMs)).toEqual([11, 13, 15, 17]);
renderPanel(workState);
const list = await screen.findByLabelText("Runner background tasks");
const cancelButtons = within(list).getAllByRole("button", { name: "Cancel" });
const retryButtons = within(list).getAllByRole("button", { name: "Retry" });
// waiting + queued are cancellable; failed + expired are retryable.
expect(cancelButtons.filter((b) => !(b as HTMLButtonElement).disabled)).toHaveLength(2);
expect(retryButtons.filter((b) => !(b as HTMLButtonElement).disabled)).toHaveLength(2);
// Rows are ordered most-recently-updated first (updatedAtMs desc), taskId as
// the deterministic tiebreak — locks the fix for the dead finishedAtMs sort.
const text = list.textContent ?? "";
const order = ["task-exp", "task-fai", "task-que", "task-wai"].map((id) =>
text.indexOf(id),
);
expect(order).toEqual([...order].sort((a, b) => a - b));
expect(order.every((i) => i >= 0)).toBe(true);
});
it("shows a terminal task summary and hides it when absent or non-terminal (#5)", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-summary",
name: "Reporter",
profileId: "codex",
busy: { state: "idle" },
backgroundTasks: [
{
taskId: "run-with-summary",
kind: "command",
state: "running",
summary: "should-not-appear-while-running",
createdAtMs: 40,
updatedAtMs: 40,
},
{
taskId: "done-with-summary",
kind: "command",
state: "completed",
summary: "Build succeeded in 12s",
createdAtMs: 30,
updatedAtMs: 30,
},
{
taskId: "fail-with-summary",
kind: "command",
state: "failed",
exitCode: 1,
summary: "Compile error: missing semicolon",
createdAtMs: 20,
updatedAtMs: 20,
},
{
taskId: "done-no-summary",
kind: "command",
state: "completed",
createdAtMs: 10,
updatedAtMs: 10,
},
],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Reporter background tasks");
// Terminal tasks with a summary render it discreetly.
expect(
within(list).getByLabelText("task done-wit summary").textContent,
).toBe("Build succeeded in 12s");
expect(
within(list).getByText("Compile error: missing semicolon"),
).toBeTruthy();
// A completed task without a summary shows no summary line.
expect(within(list).queryByLabelText("task done-no- summary")).toBeNull();
// A still-running task never shows its (non-terminal) summary.
expect(within(list).queryByLabelText("task run-with summary")).toBeNull();
expect(
within(list).queryByText("should-not-appear-while-running"),
).toBeNull();
});
it("renders a legacy agent without tickets", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-6",
name: "Legacy",
profileId: "gemini",
busy: { state: "idle" },
},
],
});
renderPanel(workState);
expect(await screen.findByText("Legacy")).toBeTruthy();
expect(screen.getByText("Offline")).toBeTruthy();
expect(screen.getByText("Idle")).toBeTruthy();
expect(screen.queryByLabelText("Legacy tickets")).toBeNull();
});
it("ignores a stray top-level backgroundTasks array (nested tasks intact) (#5)", async () => {
// The backend nests tasks under their agent and emits no `ownerAgentId`; a
// project-root `backgroundTasks` array is not part of the contract and must
// not be merged in (dead path removed in ticket #5).
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-nested",
name: "Nested",
profileId: "codex",
busy: { state: "idle" },
backgroundTasks: [
{
taskId: "nested-1",
kind: "command",
state: "completed",
createdAtMs: 1,
updatedAtMs: 2,
},
],
},
],
backgroundTasks: [
{
taskId: "orphan-top-level",
kind: "command",
state: "completed",
ownerAgentId: "agent-nested",
createdAtMs: 3,
updatedAtMs: 4,
},
],
});
const state = await workState.getProjectWorkState(PROJECT_ID);
expect((state.agents[0]?.backgroundTasks ?? []).map((t) => t.taskId)).toEqual(
["nested-1"],
);
});
it("normalizes missing top-level work state arrays", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {});
await expect(workState.getProjectWorkState(PROJECT_ID)).resolves.toEqual({
agents: [],
conversations: [],
});
});
it("renders an error fallback instead of blanking the app when Work throws", () => {
function BrokenWorkPanel(): JSX.Element {
throw new Error("render exploded");
}
render(
<WorkStatePanelErrorBoundary>
<BrokenWorkPanel />
</WorkStatePanelErrorBoundary>,
);
expect(screen.getByRole("alert").textContent).toContain(
"Work panel failed to render.",
);
expect(screen.getByText("render exploded")).toBeTruthy();
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
});
it("renders conversation objective and summary joined by conversation id", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-8",
name: "Summarizer",
profileId: "codex",
busy: { state: "busy", ticket: "ticket-goal-abcdef", sinceMs: 123 },
tickets: [
{
ticketId: "ticket-goal-abcdef",
conversationId: "conversation-goal",
position: 0,
status: "inProgress",
source: { kind: "human" },
requesterLabel: "Anthony",
taskPreview: "Continue implementation",
taskLen: 23,
},
{
ticketId: "ticket-summary-abcdef",
conversationId: "conversation-summary",
position: 1,
status: "queued",
source: { kind: "agent", agentId: "agent-main-123456" },
requesterLabel: "Main",
taskPreview: "Follow up",
taskLen: 9,
},
],
},
],
conversations: [
{
conversationId: "conversation-summary",
status: "ready",
objectivePreview: null,
summaryPreview: "Previous turn established the adapter boundary.",
summaryLen: 47,
upTo: "turn-2",
recentTurns: [],
},
{
conversationId: "conversation-goal",
status: "ready",
objectivePreview: "Ship the Work panel summaries",
summaryPreview: "This should be lower priority than the goal.",
summaryLen: 48,
upTo: "turn-1",
recentTurns: [],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Summarizer tickets");
expect(within(list).getAllByText("Summary")).toHaveLength(2);
expect(
within(list).getByText("Goal: Ship the Work panel summaries"),
).toBeTruthy();
expect(
within(list).getByText(
"Previous turn established the adapter boundary.",
),
).toBeTruthy();
});
it("keeps partial and unavailable summaries compact without hiding tickets", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-9",
name: "Contextualizer",
profileId: "claude",
busy: { state: "idle" },
tickets: [
{
ticketId: "ticket-partial-abcdef",
conversationId: "conversation-partial",
position: 0,
status: "inProgress",
source: { kind: "agent", agentId: "agent-qa-123456" },
requesterLabel: "QA",
taskPreview: "Validate partial context",
taskLen: 24,
},
{
ticketId: "ticket-unavailable-abcdef",
conversationId: "conversation-unavailable",
position: 1,
status: "queued",
source: { kind: "human" },
requesterLabel: "Anthony",
taskPreview: "Validate unavailable context",
taskLen: 28,
},
],
},
],
conversations: [
{
conversationId: "conversation-partial",
status: "partial",
objectivePreview: null,
summaryPreview: null,
summaryLen: 0,
upTo: null,
recentTurns: [
{
role: "prompt",
source: { kind: "agent", agentId: "agent-qa-123456" },
atMs: 100,
textPreview: "Only the latest prompt was available.",
textLen: 37,
},
],
},
{
conversationId: "conversation-unavailable",
status: "unavailable",
objectivePreview: null,
summaryPreview: "Summary service unavailable right now.",
summaryLen: 38,
upTo: null,
recentTurns: [],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Contextualizer tickets");
expect(within(list).getByText("#1 In progress")).toBeTruthy();
expect(within(list).getByText("Validate partial context")).toBeTruthy();
expect(within(list).getByText("Partial")).toBeTruthy();
expect(
within(list).getByText("Last: Only the latest prompt was available."),
).toBeTruthy();
expect(within(list).getByText("#2 Queued")).toBeTruthy();
expect(within(list).getByText("Validate unavailable context")).toBeTruthy();
expect(within(list).getByText("Unavailable")).toBeTruthy();
expect(
within(list).getByText("Summary service unavailable right now."),
).toBeTruthy();
});
it("refreshes when a relevant domain event fires", async () => {
const workState = new MockWorkStateGateway();
const system = new MockSystemGateway();
renderPanel(workState, system);
await screen.findByText("No agent work state.");
const spy = vi.spyOn(workState, "getProjectWorkState");
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-3",
name: "Responder",
profileId: "gemini",
busy: { state: "busy", ticket: "ticket-123456789", sinceMs: 456 },
},
],
});
system.emit({
type: "agentBusyChanged",
agentId: "agent-3",
busy: true,
});
expect(await screen.findByText("Responder")).toBeTruthy();
await waitFor(() => expect(spy).toHaveBeenCalled());
});
it("refreshes when delegationReady fires", async () => {
const workState = new MockWorkStateGateway();
const system = new MockSystemGateway();
renderPanel(workState, system);
await screen.findByText("No agent work state.");
const spy = vi.spyOn(workState, "getProjectWorkState");
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-7",
name: "Delegatee",
profileId: "codex",
busy: { state: "busy", ticket: "ticket-ready-123456", sinceMs: 123 },
tickets: [
{
ticketId: "ticket-ready-123456",
conversationId: "conversation-ready",
position: 0,
status: "inProgress",
source: { kind: "agent", agentId: "agent-main-123456" },
requesterLabel: "Main",
taskPreview: "Handle this delegated task",
taskLen: 26,
},
],
},
],
});
system.emit({
type: "delegationReady",
agentId: "agent-7",
ticket: "ticket-ready-123456",
text: "Handle this delegated task",
});
expect(await screen.findByText("Delegatee")).toBeTruthy();
expect(screen.getByText("#1 In progress")).toBeTruthy();
await waitFor(() => expect(spy).toHaveBeenCalled());
});
it("Open focuses a visible live cell and does not launch", async () => {
const workState = new MockWorkStateGateway();
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const leafId = leaves(await layout.loadLayout(PROJECT_ID))[0].id;
const host = document.createElement("div");
host.dataset.nodeId = leafId;
document.body.appendChild(host);
const launchSpy = vi.spyOn(agent, "launchAgent");
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-open",
name: "Visible",
profileId: "codex",
live: { nodeId: leafId, sessionId: "session-open", kind: "pty" },
busy: { state: "idle" },
tickets: [],
},
],
conversations: [],
});
renderPanel(workState, new MockSystemGateway(), { layout, agent });
fireEvent.click(await screen.findByRole("button", { name: "Open" }));
expect(host.style.outline).toContain("2px solid");
expect(launchSpy).not.toHaveBeenCalled();
host.remove();
});
it("Attach rebinds a hidden live agent to the deterministic empty cell", async () => {
const workState = new MockWorkStateGateway();
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const created = await agent.createAgent(PROJECT_ID, {
name: "Hidden",
profileId: "codex",
});
await agent.launchAgent(
PROJECT_ID,
created.id,
{ cwd: "/x", rows: 24, cols: 80, nodeId: "hidden-node" },
() => {},
);
const attachSpy = vi.spyOn(agent, "attachLiveAgent");
const launchSpy = vi.spyOn(agent, "launchAgent");
const target = leaves(await layout.loadLayout(PROJECT_ID))[0].id;
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: created.id,
name: "Hidden",
profileId: "codex",
live: { nodeId: "hidden-node", sessionId: "mock-agent-session-1", kind: "pty" },
busy: { state: "idle" },
tickets: [],
},
],
conversations: [],
});
renderPanel(workState, new MockSystemGateway(), { layout, agent });
const attach = await screen.findByRole("button", { name: "Attach" });
await waitFor(() => expect((attach as HTMLButtonElement).disabled).toBe(false));
fireEvent.click(attach);
await waitFor(() =>
expect(attachSpy).toHaveBeenCalledWith(PROJECT_ID, created.id, target),
);
await waitFor(async () => {
const leaf = leaves(await layout.loadLayout(PROJECT_ID))[0];
expect(leaf.agent).toBe(created.id);
expect(leaf.session).toBe("mock-agent-session-1");
});
expect(launchSpy).not.toHaveBeenCalled();
});
it("disables Attach when no visible target cell exists", async () => {
const workState = new MockWorkStateGateway();
const layout = new MockLayoutGateway();
const leafId = leaves(await layout.loadLayout(PROJECT_ID))[0].id;
await layout.mutateLayout(PROJECT_ID, {
type: "setCellAgent",
target: leafId,
agent: "other-agent",
});
await layout.mutateLayout(PROJECT_ID, {
type: "setSession",
target: leafId,
session: "occupied-session",
});
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-hidden",
name: "Hidden",
profileId: "codex",
live: { nodeId: "hidden-node", sessionId: "session-hidden", kind: "pty" },
busy: { state: "idle" },
tickets: [],
},
],
conversations: [],
});
renderPanel(workState, new MockSystemGateway(), {
layout,
agent: new MockAgentGateway(),
});
const attach = await screen.findByRole("button", { name: "Attach" });
await waitFor(() => expect((attach as HTMLButtonElement).disabled).toBe(true));
expect(await screen.findByText("No empty visible cell.")).toBeTruthy();
});
it("Stop confirms with a busy warning, stops the live agent, and refreshes Work", async () => {
const workState = new MockWorkStateGateway();
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const created = await agent.createAgent(PROJECT_ID, {
name: "Runner",
profileId: "codex",
});
const leafId = leaves(await layout.loadLayout(PROJECT_ID))[0].id;
await agent.launchAgent(
PROJECT_ID,
created.id,
{ cwd: "/x", rows: 24, cols: 80, nodeId: leafId },
() => {},
);
await layout.mutateLayout(PROJECT_ID, {
type: "setCellAgent",
target: leafId,
agent: created.id,
});
await layout.mutateLayout(PROJECT_ID, {
type: "setSession",
target: leafId,
session: "mock-agent-session-1",
});
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: created.id,
name: "Runner",
profileId: "codex",
live: { nodeId: leafId, sessionId: "mock-agent-session-1", kind: "pty" },
busy: { state: "busy", ticket: "ticket-stop", sinceMs: 1 },
tickets: [
{
ticketId: "ticket-stop",
conversationId: "conv-stop",
position: 0,
status: "inProgress",
source: { kind: "human" },
requesterLabel: "Human",
taskPreview: "Do work",
taskLen: 7,
},
],
},
],
conversations: [],
});
const stopSpy = vi.spyOn(agent, "stopLiveAgent");
const refreshSpy = vi.spyOn(workState, "getProjectWorkState");
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
renderPanel(workState, new MockSystemGateway(), { layout, agent });
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
await waitFor(() =>
expect(confirmSpy).toHaveBeenCalledWith(
"Stop Runner? The current turn will be interrupted.",
),
);
await waitFor(() => expect(stopSpy).toHaveBeenCalledWith(PROJECT_ID, created.id));
await waitFor(() => expect(refreshSpy).toHaveBeenCalled());
await waitFor(async () => {
const leaf = leaves(await layout.loadLayout(PROJECT_ID))[0];
expect(leaf.session).toBeNull();
});
confirmSpy.mockRestore();
});
it("View conversation exposes only Lot C preview fields", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-view",
name: "Viewer",
profileId: "codex",
busy: { state: "idle" },
tickets: [
{
ticketId: "ticket-view",
conversationId: "conversation-view",
position: 0,
status: "queued",
source: { kind: "human" },
requesterLabel: "Human",
taskPreview: "Inspect preview",
taskLen: 15,
},
],
},
],
conversations: [
{
conversationId: "conversation-view",
status: "ready",
objectivePreview: "Keep it compact",
summaryPreview: "Only the exposed summary preview.",
summaryLen: 33,
upTo: "internal-turn-id",
recentTurns: [
{
role: "response",
source: { kind: "agent", agentId: "agent-view" },
atMs: 42,
textPreview: "Recent exposed turn.",
textLen: 20,
},
],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Viewer tickets");
fireEvent.click(within(list).getByRole("button", { name: "View" }));
expect(within(list).getByText("Goal:")).toBeTruthy();
expect(within(list).getByText("Keep it compact")).toBeTruthy();
expect(within(list).getByText("Summary:")).toBeTruthy();
expect(
within(list).getByText("Only the exposed summary preview."),
).toBeTruthy();
expect(within(list).getByText("response:")).toBeTruthy();
expect(within(list).getByText("Recent exposed turn.")).toBeTruthy();
expect(list.textContent).not.toContain("internal-turn-id");
});
it("Copy summary copies only the exposed preview text", async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
vi.stubGlobal("navigator", { clipboard: { writeText } });
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-copy",
name: "Copier",
profileId: "codex",
busy: { state: "idle" },
tickets: [
{
ticketId: "ticket-copy",
conversationId: "conversation-copy",
position: 0,
status: "queued",
source: { kind: "human" },
requesterLabel: "Human",
taskPreview: "Copy preview",
taskLen: 12,
},
],
},
],
conversations: [
{
conversationId: "conversation-copy",
status: "ready",
objectivePreview: null,
summaryPreview: "Copy this summary preview only.",
summaryLen: 31,
upTo: null,
recentTurns: [
{
role: "prompt",
source: { kind: "human" },
atMs: 1,
textPreview: "Do not copy this turn.",
textLen: 22,
},
],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Copier tickets");
fireEvent.click(within(list).getByRole("button", { name: "Copy" }));
await waitFor(() =>
expect(writeText).toHaveBeenCalledWith("Copy this summary preview only."),
);
expect(await within(list).findByRole("button", { name: "Copied" })).toBeTruthy();
vi.unstubAllGlobals();
});
});