Câble les actions contrôlées dans ProjectWorkStatePanel : port et adaptateur agent étendus aux nouvelles commandes, adaptateur mock aligné. Tests verts : workstate + projects (25), agent (8), singletonAgent + agentAlreadyRunning (9), tsc --noEmit OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
729 lines
23 KiB
TypeScript
729 lines
23 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 } 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 mock work state without tickets", async () => {
|
|
const workState = new MockWorkStateGateway();
|
|
workState._setProjectWorkState(PROJECT_ID, {
|
|
agents: [
|
|
{
|
|
agentId: "agent-6",
|
|
name: "Legacy",
|
|
profileId: "gemini",
|
|
busy: { state: "idle" },
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(workState.getProjectWorkState(PROJECT_ID)).resolves.toEqual({
|
|
agents: [
|
|
{
|
|
agentId: "agent-6",
|
|
name: "Legacy",
|
|
profileId: "gemini",
|
|
busy: { state: "idle" },
|
|
tickets: [],
|
|
},
|
|
],
|
|
conversations: [],
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|