- Prettier 3.6.2: .prettierrc.json + .prettierignore + format/format:check scripts; formatted the web/ tree. - ESLint 9 flat config (@eslint/js + typescript-eslint + react-hooks + react-refresh, eslint-config-prettier last so Prettier owns formatting); lint script; src/components/ui/** opts out of react-refresh (shadcn primitives co-export their cva variants). 0 problems. - Playwright 1.61.1: playwright.config.ts (build+preview webServer) + an e2e Board-renders smoke against a mocked /api; test:e2e script. Reusable workflow installs chromium itself. - ci-web.yaml: required-scripts -> format:check,lint,build,test,test:e2e; run-format-check/lint/e2e -> true; e2e-browser chromium. All five scripts verified green locally.
153 lines
4.3 KiB
TypeScript
153 lines
4.3 KiB
TypeScript
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: [
|
|
{
|
|
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,
|
|
},
|
|
],
|
|
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", () => {
|
|
render(
|
|
<TaskDrawer
|
|
detail={DETAIL}
|
|
loading={false}
|
|
error={null}
|
|
onClose={() => {}}
|
|
/>,
|
|
);
|
|
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(
|
|
<TaskDrawer
|
|
detail={null}
|
|
loading={false}
|
|
error={null}
|
|
onClose={() => {}}
|
|
/>,
|
|
);
|
|
expect(container).toBeEmptyDOMElement();
|
|
});
|
|
|
|
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",
|
|
},
|
|
],
|
|
};
|
|
render(
|
|
<TaskDrawer detail={d} loading={false} error={null} onClose={() => {}} />,
|
|
);
|
|
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",
|
|
phases: [
|
|
{ name: "Recon", steps: ["read conftest", "read test_graph"] },
|
|
],
|
|
},
|
|
};
|
|
render(
|
|
<TaskDrawer detail={d} loading={false} error={null} onClose={() => {}} />,
|
|
);
|
|
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,
|
|
qa_history: [
|
|
"plain answer text",
|
|
{ turn: 1, answer: "dict answer text" },
|
|
],
|
|
};
|
|
render(
|
|
<TaskDrawer detail={d} loading={false} error={null} onClose={() => {}} />,
|
|
);
|
|
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();
|
|
});
|
|
});
|