feat(frontend): v2 artifact viewers for cte_plan/cte_result/phase

Add CtePlanViewer, CteResultViewer, and PhaseSummaryViewer to render the
structured v2 payloads (schema_version: 2) the harness now emits for
artifact-gate widgets, per contracts.md. Extract PreviewGrid from
ResultsPanel as a reusable AG Grid component shared by CteResultViewer.
ArtifactView dispatches to the new viewers on a schema_version/shape
guard, falling back to the existing legacy renderers unchanged for
older sessions and replay fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 00:57:20 +02:00
co-authored by Claude Fable 5
parent 3ad93cd02f
commit 1c97289ed8
12 changed files with 937 additions and 26 deletions
+90 -2
View File
@@ -5,14 +5,20 @@ vi.mock("./mermaid", () => ({
renderMermaid: vi.fn().mockResolvedValue('<svg data-testid="mm"></svg>'),
}));
test("sql artifact renders the SQL text", () => {
vi.mock("./highlight", () => ({
highlightSql: vi.fn().mockResolvedValue('<pre data-testid="hl">SQL</pre>'),
}));
test("sql artifact renders the SQL text", async () => {
render(<ArtifactView artifact={{ kind: "sql", data: "SELECT 1" }} />);
expect(screen.getByText("SELECT 1")).toBeInTheDocument();
await screen.findByTestId("hl");
});
test("sql artifact accepts a { content } wrapper", () => {
test("sql artifact accepts a { content } wrapper", async () => {
render(<ArtifactView artifact={{ kind: "sql", data: { content: "SELECT 7" } }} />);
expect(screen.getByText("SELECT 7")).toBeInTheDocument();
await screen.findByTestId("hl");
});
test("cte_plan renders an ordered list of names", () => {
@@ -56,3 +62,85 @@ test("schema_linking renders the schema viewer", async () => {
);
expect(await screen.findByTestId("mm")).toBeInTheDocument();
});
test("cte_plan v2 dispatches to CtePlanViewer", () => {
render(
<ArtifactView
artifact={{
kind: "cte_plan",
data: {
schema_version: 2,
strategy: "Filtriamo prima i pazienti idonei.",
ctes: [{ name: "pazienti_idonei", index: 1, purpose: "Seleziona pazienti" }],
},
}}
/>,
);
expect(screen.getByText("Filtriamo prima i pazienti idonei.")).toBeInTheDocument();
expect(screen.getByText("CTE 1/1")).toBeInTheDocument();
});
test("cte_plan v1 (legacy names list) still renders via the legacy path", () => {
render(<ArtifactView artifact={{ kind: "cte_plan", data: { names: ["a_cte", "b_cte"] } }} />);
expect(screen.getByText("a_cte")).toBeInTheDocument();
expect(screen.getByText("b_cte")).toBeInTheDocument();
});
test("cte_result v2 dispatches to CteResultViewer", async () => {
render(
<ArtifactView
artifact={{
kind: "cte_result",
data: {
schema_version: 2,
name: "pazienti_idonei",
index: 1,
total: 1,
sql: "SELECT 1",
status: "ok",
},
}}
/>,
);
await screen.findByTestId("hl");
expect(screen.getByText("CTE 1/1")).toBeInTheDocument();
});
test("cte_result v1 (bare sql string) still renders via toSqlBlocks legacy path", async () => {
render(<ArtifactView artifact={{ kind: "cte_result", data: "SELECT 42" }} />);
await screen.findByTestId("hl");
expect(screen.queryByText("CTE")).not.toBeInTheDocument();
});
test("phase v2 dispatches to PhaseSummaryViewer", () => {
render(
<ArtifactView
artifact={{
kind: "phase",
data: {
schema_version: 2,
phase: { id: "F5", num: 5, name: "sintesi" },
summary: "Riassunto della fase.",
},
}}
/>,
);
expect(screen.getByText("F5 — sintesi")).toBeInTheDocument();
expect(screen.getByText("Riassunto della fase.")).toBeInTheDocument();
});
test("phase v1 (no schema_version) still renders via legacy markdown/structured path", () => {
render(
<ArtifactView
artifact={{
kind: "phase",
data: {
recap: "Domanda: lista pazienti.",
chiarimenti_risolti: ["finestra_temporale — 5 anni"],
},
}}
/>,
);
expect(screen.getByText("Recap")).toBeInTheDocument();
expect(screen.getByText("Chiarimenti risolti")).toBeInTheDocument();
});