feat(workstate): UI live-state des conversations/délégations (Lot A frontend)
Ajoute le port et l'adaptateur workState (+ mock) côté frontend, le type domaine associé, et la feature `workstate` (panneau ProjectWorkStatePanel + hook useProjectWorkState) consommant le read-model live exposé par le backend. Intègre le panneau dans ProjectsView. Tests verts (workstate + projects). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
111
frontend/src/features/workstate/workstate.test.tsx
Normal file
111
frontend/src/features/workstate/workstate.test.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { MockSystemGateway, MockWorkStateGateway } from "@/adapters/mock";
|
||||
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(),
|
||||
) {
|
||||
const gateways = { workState, system } 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();
|
||||
});
|
||||
|
||||
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("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());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user