feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
import { render, screen } from "@testing-library/react";
|
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
|
import { TaskDrawer } from "./TaskDrawer";
|
|
|
|
|
import type { TaskDetail } from "../api";
|
|
|
|
|
|
|
|
|
|
const DETAIL: TaskDetail = {
|
|
|
|
|
ok: true,
|
|
|
|
|
thread_id: "aaaa1111bbbb",
|
|
|
|
|
short_id: "aaaa1111",
|
|
|
|
|
task: "remediate CVE-2026-1",
|
|
|
|
|
status: "done",
|
|
|
|
|
current_phase: "done",
|
|
|
|
|
partial: false,
|
|
|
|
|
timeline: [
|
2026-06-24 19:49:07 -04:00
|
|
|
{
|
|
|
|
|
from_phase: null,
|
|
|
|
|
to_phase: "intake",
|
|
|
|
|
entered_at: "2026-06-23T00:00:00+00:00",
|
|
|
|
|
exited_at: "2026-06-23T00:00:01+00:00",
|
|
|
|
|
duration_s: 1,
|
|
|
|
|
status: "active",
|
|
|
|
|
note: null,
|
|
|
|
|
cost_usd: 0,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
from_phase: "intake",
|
|
|
|
|
to_phase: "plan",
|
|
|
|
|
entered_at: "2026-06-23T00:00:01+00:00",
|
|
|
|
|
exited_at: null,
|
|
|
|
|
duration_s: null,
|
|
|
|
|
status: "active",
|
|
|
|
|
note: null,
|
|
|
|
|
cost_usd: 0.12,
|
|
|
|
|
},
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
],
|
|
|
|
|
qa_history: [{ turn: 0, answer: "scope it" }],
|
|
|
|
|
review_verdicts: [],
|
|
|
|
|
plan: { summary: "do the thing" },
|
|
|
|
|
cost_by_stage: { plan: 0.12 },
|
|
|
|
|
total_usd: 0.12,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe("TaskDrawer", () => {
|
|
|
|
|
it("renders the timeline steps with the task description and spend", () => {
|
2026-06-24 19:49:07 -04:00
|
|
|
render(
|
|
|
|
|
<TaskDrawer
|
|
|
|
|
detail={DETAIL}
|
|
|
|
|
loading={false}
|
|
|
|
|
error={null}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
expect(screen.getByText("remediate CVE-2026-1")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("intake")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("plan")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("in progress")).toBeInTheDocument(); // open final step
|
|
|
|
|
expect(screen.getByText(/spend \$0\.12/)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("shows a partial-history banner when partial", () => {
|
|
|
|
|
render(
|
|
|
|
|
<TaskDrawer
|
|
|
|
|
detail={{ ...DETAIL, partial: true }}
|
|
|
|
|
loading={false}
|
|
|
|
|
error={null}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
expect(screen.getByText(/Partial history/)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders nothing when idle (no detail, not loading)", () => {
|
|
|
|
|
const { container } = render(
|
2026-06-24 19:49:07 -04:00
|
|
|
<TaskDrawer
|
|
|
|
|
detail={null}
|
|
|
|
|
loading={false}
|
|
|
|
|
error={null}
|
|
|
|
|
onClose={() => {}}
|
|
|
|
|
/>,
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
);
|
|
|
|
|
expect(container).toBeEmptyDOMElement();
|
|
|
|
|
});
|
2026-06-23 21:14:04 -04:00
|
|
|
|
|
|
|
|
it("renders review findings as markdown (bold, not raw **)", () => {
|
|
|
|
|
const d: TaskDetail = {
|
|
|
|
|
...DETAIL,
|
|
|
|
|
review_verdicts: [
|
|
|
|
|
{
|
|
|
|
|
verdict: "request_changes",
|
|
|
|
|
round_index: 1,
|
|
|
|
|
outcome: "loop_back",
|
|
|
|
|
findings: "**Missing phase** and\n1. run the tests",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
2026-06-24 19:49:07 -04:00
|
|
|
render(
|
|
|
|
|
<TaskDrawer detail={d} loading={false} error={null} onClose={() => {}} />,
|
|
|
|
|
);
|
2026-06-23 21:14:04 -04:00
|
|
|
expect(screen.getByText("Missing phase").tagName).toBe("STRONG");
|
|
|
|
|
expect(screen.getByText("run the tests").closest("li")).not.toBeNull();
|
|
|
|
|
expect(screen.getByText("request changes")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders the plan as summary + phase/step lists", () => {
|
|
|
|
|
const d: TaskDetail = {
|
|
|
|
|
...DETAIL,
|
|
|
|
|
plan: {
|
|
|
|
|
summary: "Add a smoke test",
|
2026-06-24 19:49:07 -04:00
|
|
|
phases: [
|
|
|
|
|
{ name: "Recon", steps: ["read conftest", "read test_graph"] },
|
|
|
|
|
],
|
2026-06-23 21:14:04 -04:00
|
|
|
},
|
|
|
|
|
};
|
2026-06-24 19:49:07 -04:00
|
|
|
render(
|
|
|
|
|
<TaskDrawer detail={d} loading={false} error={null} onClose={() => {}} />,
|
|
|
|
|
);
|
2026-06-23 21:14:04 -04:00
|
|
|
expect(screen.getByText("Recon")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("read conftest").closest("li")).not.toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders both string and dict Q&A entries", () => {
|
|
|
|
|
const d: TaskDetail = {
|
|
|
|
|
...DETAIL,
|
2026-06-24 19:49:07 -04:00
|
|
|
qa_history: [
|
|
|
|
|
"plain answer text",
|
|
|
|
|
{ turn: 1, answer: "dict answer text" },
|
|
|
|
|
],
|
2026-06-23 21:14:04 -04:00
|
|
|
};
|
2026-06-24 19:49:07 -04:00
|
|
|
render(
|
|
|
|
|
<TaskDrawer detail={d} loading={false} error={null} onClose={() => {}} />,
|
|
|
|
|
);
|
2026-06-23 21:14:04 -04:00
|
|
|
expect(screen.getByText("plain answer text")).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText("dict answer text")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does NOT render raw HTML embedded in agent text (no XSS)", () => {
|
|
|
|
|
const d: TaskDetail = {
|
|
|
|
|
...DETAIL,
|
|
|
|
|
review_verdicts: [
|
|
|
|
|
{
|
|
|
|
|
verdict: "approve",
|
|
|
|
|
round_index: 1,
|
|
|
|
|
outcome: "build",
|
|
|
|
|
findings: 'before <img src=x onerror="alert(1)"> after',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<TaskDrawer detail={d} loading={false} error={null} onClose={() => {}} />,
|
|
|
|
|
);
|
|
|
|
|
expect(container.querySelector("img")).toBeNull();
|
|
|
|
|
});
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
});
|