From a15a6f30e6a27dcd8c169954b71888361360c765 Mon Sep 17 00:00:00 2001 From: Shay Date: Wed, 24 Jun 2026 11:49:37 -0700 Subject: [PATCH] feat(workbench-ui): wire Trace practice evidence tab --- workbench-ui/src/api/client.ts | 9 + workbench-ui/src/api/queries.ts | 14 + workbench-ui/src/app/trace/TraceRoute.tsx | 23 ++ .../src/app/trace/TraceRoutePractice.test.tsx | 282 ++++++++++++++++++ 4 files changed, 328 insertions(+) create mode 100644 workbench-ui/src/app/trace/TraceRoutePractice.test.tsx diff --git a/workbench-ui/src/api/client.ts b/workbench-ui/src/api/client.ts index ab3415bb..6bb3ce57 100644 --- a/workbench-ui/src/api/client.ts +++ b/workbench-ui/src/api/client.ts @@ -42,6 +42,8 @@ import type { HealthStatus, } from "../types/api"; import type { ConstructionEvidence } from "../types/constructionEvidence"; +import type { PracticeEvidence } from "../types/practiceEvidence"; + export class WorkbenchApiError extends Error { @@ -223,6 +225,13 @@ export async function fetchTraceConstruction(turnId: number): Promise { + return apiFetch( + `/trace/${encodeURIComponent(String(turnId))}/practice`, + ); +} + + export async function fetchTour(): Promise { return apiFetch("/tour"); diff --git a/workbench-ui/src/api/queries.ts b/workbench-ui/src/api/queries.ts index 1b655e88..b9cd67bf 100644 --- a/workbench-ui/src/api/queries.ts +++ b/workbench-ui/src/api/queries.ts @@ -36,6 +36,7 @@ import { fetchTraceField, fetchTraceBundle, fetchTraceConstruction, + fetchTracePractice, fetchTour, fetchLivedLife, fetchTraceTurns, @@ -92,6 +93,8 @@ import type { MathRatifyResult, } from "../types/api"; import type { ConstructionEvidence } from "../types/constructionEvidence"; +import type { PracticeEvidence } from "../types/practiceEvidence"; + export { QueryClientProvider }; @@ -276,6 +279,17 @@ export function useTraceConstruction(turnId?: number | null) { }); } +export function useTracePractice(turnId?: number | null) { + return useQuery({ + queryKey: ["api", "trace", "practice", turnId ?? null], + queryFn: () => fetchTracePractice(turnId as number), + enabled: typeof turnId === "number", + staleTime: 30_000, + refetchOnWindowFocus: false, + }); +} + + export function useTour() { return useQuery({ diff --git a/workbench-ui/src/app/trace/TraceRoute.tsx b/workbench-ui/src/app/trace/TraceRoute.tsx index 49763b2d..13e75c80 100644 --- a/workbench-ui/src/app/trace/TraceRoute.tsx +++ b/workbench-ui/src/app/trace/TraceRoute.tsx @@ -9,8 +9,11 @@ import { useTraceTurn, useTraceTurns, useTraceConstruction, + useTracePractice, } from "../../api/queries"; import type { ConstructionEvidence } from "../../types/constructionEvidence"; +import type { PracticeEvidence } from "../../types/practiceEvidence"; +import { PracticeEvidencePanel } from "./PracticeEvidencePanel"; import { CONSTRUCTION_AUTHORITY_DISCLOSURES, assessmentBlockerSummary, @@ -59,6 +62,7 @@ const TRACE_TABS: readonly Tab[] = [ { id: "field", label: "Field" }, { id: "bundle", label: "Bundle" }, { id: "construction", label: "Construction" }, + { id: "practice", label: "Practice" }, { id: "surfaces", label: "Surfaces" }, { id: "grounding", label: "Grounding" }, { id: "verdicts", label: "Verdicts" }, @@ -855,6 +859,9 @@ function TraceDetail({ constructionEvidence, constructionLoading, constructionError, + practiceEvidence, + practiceLoading, + practiceError, }: { turn: TurnJournalEntry; pipelineRecord?: CognitivePipelineRecord | null; @@ -869,6 +876,9 @@ function TraceDetail({ constructionEvidence?: ConstructionEvidence | null; constructionLoading: boolean; constructionError: unknown; + practiceEvidence?: PracticeEvidence | null; + practiceLoading: boolean; + practiceError: unknown; }) { const [activeTab, setActiveTab] = useState("pipeline"); return ( @@ -915,6 +925,15 @@ function TraceDetail({ turnId={turn.turn_id} /> ) : null} + {activeTab === "practice" ? ( + + ) : null} {activeTab === "surfaces" ? : null} {activeTab === "grounding" ? : null} {activeTab === "verdicts" ? : null} @@ -938,6 +957,7 @@ export function TraceRoute() { const fieldQuery = useTraceField(selectedTurnId); const bundleQuery = useTraceBundle(selectedTurnId); const constructionQuery = useTraceConstruction(selectedTurnId); + const practiceQuery = useTracePractice(selectedTurnId); const turns = turnsQuery.data ?? []; const filteredTurns = useMemo(() => { @@ -1057,6 +1077,9 @@ export function TraceRoute() { constructionEvidence={constructionQuery.data} constructionLoading={constructionQuery.isLoading} constructionError={constructionQuery.isError ? constructionQuery.error : null} + practiceEvidence={practiceQuery.data} + practiceLoading={practiceQuery.isLoading} + practiceError={practiceQuery.isError ? practiceQuery.error : null} /> ) : null} diff --git a/workbench-ui/src/app/trace/TraceRoutePractice.test.tsx b/workbench-ui/src/app/trace/TraceRoutePractice.test.tsx new file mode 100644 index 00000000..02d224bc --- /dev/null +++ b/workbench-ui/src/app/trace/TraceRoutePractice.test.tsx @@ -0,0 +1,282 @@ +import { QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestQueryClient } from "../../test/createTestQueryClient"; +import { EvidenceProvider } from "../evidenceContext"; +import { TraceRoute } from "./TraceRoute"; + +function jsonOk(data: unknown) { + return Promise.resolve({ + json: () => Promise.resolve({ ok: true, generated_at: "now", data }), + }); +} + +function traceSummary(turnId: number) { + return { + turn_id: turnId, + timestamp: "2026-06-23T20:00:00Z", + prompt_excerpt: "Lena has 3 marbles.", + surface_excerpt: "Trace surface", + trace_hash: "sha256:777777777777abcdef", + grounding_source: "pack", + trace_integrity: "pipeline_trace", + }; +} + +function traceEntry(turnId: number) { + const summary = traceSummary(turnId); + return { + ...summary, + prompt: "Lena has 3 marbles.", + surface: "Trace surface", + articulation_surface: "Trace articulation", + walk_surface: "Trace walk", + epistemic_state: "evidenced", + normative_clearance: "cleared", + verdicts: { + identity: { outcome: "cleared", runtime_detail: "identity ok" }, + safety: { outcome: "cleared", runtime_detail: "safety ok" }, + ethics: { outcome: "cleared", runtime_detail: "ethics ok" }, + }, + refusal_emitted: false, + hedge_injected: false, + proposal_candidates: [], + turn_cost_ms: 12, + checkpoint_emitted: true, + journal_digest: "sha256:journal777abcdef", + pipeline_record: null, + }; +} + +function pipelineRecord() { + return { + schema_version: "cognitive_pipeline_record_v1", + status: "missing_evidence", + missing_reason: "pipeline_record_not_persisted", + trace_hash: "sha256:777777777777abcdef", + versor_condition: null, + field_digest: null, + stages: [], + edges: [], + }; +} + +function fieldEvidence() { + return { + schema_version: "field_evidence_v1", + status: "missing_evidence", + missing_reason: "field evidence was not persisted for this turn", + trace_hash: "sha256:777777777777abcdef", + versor_condition: null, + versor_condition_ceiling: 0.000001, + field_valid: null, + field_digest: null, + parent_field_digest: null, + transition_inner_product: null, + }; +} + +function evidenceBundle(turnId: number) { + return { + schema_version: "evidence_bundle_v1", + turn_id: turnId, + generated_from: "turn_journal", + trace_hash: "sha256:777777777777abcdef", + trace_integrity: "pipeline_trace", + prompt: "Lena has 3 marbles.", + surface: "Trace surface", + grounding_source: "pack", + epistemic_state: "evidenced", + normative_clearance: "cleared", + refusal_emitted: false, + journal_digest: "sha256:journal777abcdef", + pipeline_record: null, + field_evidence: null, + leeway_evidence: null, + replay_reproducer: `core replay turn ${turnId}`, + bundle_digest: "sha256:bundle777abcdef", + }; +} + +function missingPracticeEvidence(turnId: number) { + return { + schema_version: "practice_evidence_v1", + turn_id: turnId, + status: "missing_evidence", + missing_reason: "practice evidence was not persisted for this turn", + record_kind: null, + practice_disposition: null, + chain: [], + sealed_trace: null, + trace_refusal: null, + diagnostic_only: true, + serving_allowed: false, + mutation_allowed: false, + replay_execution_allowed: false, + replay_executed_by_workbench: false, + }; +} + +function recordedPracticeEvidence(turnId: number) { + return { + ...missingPracticeEvidence(turnId), + status: "recorded", + missing_reason: null, + record_kind: "sealed_trace", + practice_disposition: "accepted", + chain: [ + { + kind: "problem_frame", + status: "recorded", + refs: ["problem_frame.0"], + summary: "problem frame summary", + }, + ], + sealed_trace: { + trace_id: "trace_123", + trace_policy_version: "v1", + input_digest: "sha256:input", + problem_frame_digest: "sha256:pf", + original_contract_assessment_id: "assessment_1", + residual_ids: [], + search_gate_decision_id: "decision_1", + compute_budget_id: "budget_1", + geometric_search_run_id: "run_1", + candidate_attempt_ids: [], + candidate_attempt_binding_ids: [], + replay_result_ids: [], + replay_refusal_ids: [], + upstream_identity_chain: [], + practice_disposition: "accepted", + trace_records: [], + evidence_spans: [ + { + text: "Lena has 3 marbles.", + start: 0, + end: 18, + sentence_index: 0, + }, + ], + created_by_policy: "policy_1", + explanation: "all good", + }, + }; +} + +function stubTraceFetch(practiceEvidence: unknown) { + const fetchMock = vi.fn((input: unknown) => { + const path = new URL(String(input)).pathname; + if (path === "/trace/turns") { + return jsonOk({ items: [traceSummary(7)] }); + } + if (path === "/trace/7") { + return jsonOk(traceEntry(7)); + } + if (path === "/trace/7/pipeline") { + return jsonOk(pipelineRecord()); + } + if (path === "/trace/7/field") { + return jsonOk(fieldEvidence()); + } + if (path === "/trace/7/bundle") { + return jsonOk(evidenceBundle(7)); + } + if (path === "/trace/7/construction") { + return jsonOk({}); + } + if (path === "/trace/7/practice") { + return jsonOk(practiceEvidence); + } + return Promise.resolve({ + json: () => + Promise.resolve({ + ok: false, + generated_at: "now", + error: { code: "not_found", message: `unexpected path ${path}` }, + }), + }); + }); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; +} + +function renderRoute() { + return render( + + + + + } /> + + + + , + ); +} + +const offsetDescriptors = { + offsetHeight: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight"), + offsetWidth: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth"), +}; + +describe("TraceRoute practice tab", () => { + beforeEach(() => { + Object.defineProperty(HTMLElement.prototype, "offsetHeight", { + configurable: true, + get: () => 560, + }); + Object.defineProperty(HTMLElement.prototype, "offsetWidth", { + configurable: true, + get: () => 480, + }); + }); + + afterEach(() => { + if (offsetDescriptors.offsetHeight) { + Object.defineProperty(HTMLElement.prototype, "offsetHeight", offsetDescriptors.offsetHeight); + } + if (offsetDescriptors.offsetWidth) { + Object.defineProperty(HTMLElement.prototype, "offsetWidth", offsetDescriptors.offsetWidth); + } + vi.restoreAllMocks(); + localStorage.clear(); + }); + + it("fetches practice evidence through the trace practice endpoint", async () => { + const fetchMock = stubTraceFetch(missingPracticeEvidence(7)); + const user = userEvent.setup(); + renderRoute(); + + await user.click(await screen.findByRole("tab", { name: "Practice" })); + + await waitFor(() => + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/trace/7/practice"), + expect.any(Object), + ), + ); + const tab = await screen.findByTestId("practice-evidence-panel"); + expect(within(tab).getAllByText("practice evidence was not persisted for this turn")[0]).toBeInTheDocument(); + expect(within(tab).getByText("diagnostic_only")).toBeInTheDocument(); + expect(within(tab).getByText("serving_allowed")).toBeInTheDocument(); + expect(within(tab).getByText("curl /trace/7/practice")).toBeInTheDocument(); + }); + + it("renders recorded practice evidence", async () => { + stubTraceFetch(recordedPracticeEvidence(7)); + const user = userEvent.setup(); + renderRoute(); + + await user.click(await screen.findByRole("tab", { name: "Practice" })); + + const tab = await screen.findByTestId("practice-evidence-panel"); + expect(within(tab).getAllByText("problem_frame")[0]).toBeInTheDocument(); + expect(within(tab).getByText("problem frame summary")).toBeInTheDocument(); + expect(within(tab).getAllByText("trace_123")[0]).toBeInTheDocument(); + expect(within(tab).getAllByText(/Lena has 3 marbles\./)[0]).toBeInTheDocument(); + expect(within(tab).getByText("diagnostic_only")).toBeInTheDocument(); + expect(within(tab).getByText("serving_allowed")).toBeInTheDocument(); + }); +});