feat(workbench-ui): wire Trace practice evidence tab
This commit is contained in:
parent
d3df22e24a
commit
a15a6f30e6
4 changed files with 328 additions and 0 deletions
|
|
@ -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<Constructi
|
|||
);
|
||||
}
|
||||
|
||||
export async function fetchTracePractice(turnId: number): Promise<PracticeEvidence> {
|
||||
return apiFetch<PracticeEvidence>(
|
||||
`/trace/${encodeURIComponent(String(turnId))}/practice`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function fetchTour(): Promise<DeterminismTour> {
|
||||
return apiFetch<DeterminismTour>("/tour");
|
||||
|
|
|
|||
|
|
@ -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<PracticeEvidence, WorkbenchApiError>({
|
||||
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<DeterminismTour, WorkbenchApiError>({
|
||||
|
|
|
|||
|
|
@ -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" ? (
|
||||
<PracticeEvidencePanel
|
||||
evidence={practiceEvidence}
|
||||
isLoading={practiceLoading}
|
||||
error={practiceError}
|
||||
turnId={turn.turn_id}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === "surfaces" ? <SurfacesTab turn={turn} /> : null}
|
||||
{activeTab === "grounding" ? <GroundingTab turn={turn} /> : null}
|
||||
{activeTab === "verdicts" ? <VerdictsTab turn={turn} /> : 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}
|
||||
</section>
|
||||
|
|
|
|||
282
workbench-ui/src/app/trace/TraceRoutePractice.test.tsx
Normal file
282
workbench-ui/src/app/trace/TraceRoutePractice.test.tsx
Normal file
|
|
@ -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(
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<MemoryRouter initialEntries={["/trace/7"]}>
|
||||
<EvidenceProvider>
|
||||
<Routes>
|
||||
<Route path="/trace/:turnId?" element={<TraceRoute />} />
|
||||
</Routes>
|
||||
</EvidenceProvider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue