feat(workbench/W-030): eval center (safe lanes only, ADR-0160 §Phase 5) (#327)

This commit is contained in:
Shay 2026-05-26 20:39:30 -07:00 committed by GitHub
parent a0e9ca8535
commit 00f5056209
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 951 additions and 16 deletions

View file

@ -1,4 +1,4 @@
import type { ApiResponse, ErrorCode } from "../types/api";
import type { ApiResponse, ErrorCode, EvalLaneSummary, EvalRunRequest, EvalRunResult } from "../types/api";
export class WorkbenchApiError extends Error {
constructor(
@ -27,3 +27,35 @@ export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T>
clearTimeout(timeout);
}
}
export async function fetchEvalLanes(): Promise<EvalLaneSummary[]> {
return apiFetch<EvalLaneSummary[]>("/evals");
}
export async function runEvalLane(req: EvalRunRequest): Promise<EvalRunResult> {
if (req.split === "holdout") {
const hasConfig = typeof window !== "undefined" && (window as any).sealedEvalConfig === true;
if (!hasConfig) {
throw new WorkbenchApiError(
"client_refused_sealed_holdout",
"Holdout runs require sealed-eval config — use CLI"
);
}
}
const lanes = await fetchEvalLanes();
const lane = lanes.find((l) => l.lane === req.lane);
if (!lane) {
throw new WorkbenchApiError("not_found", `Eval lane not found: ${req.lane}`);
}
if (!lane.read_only) {
throw new WorkbenchApiError("client_refused_unsafe_lane", "API run disabled — use CLI");
}
return apiFetch<EvalRunResult>("/evals/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
});
}

View file

@ -4,7 +4,7 @@ import {
useMutation,
QueryClientProvider,
} from "@tanstack/react-query";
import { apiFetch } from "./client";
import { apiFetch, fetchEvalLanes, runEvalLane } from "./client";
import type { WorkbenchApiError } from "./client";
import type {
RuntimeStatus,
@ -15,6 +15,7 @@ import type {
EvalLaneSummary,
EvalRunResult,
ChatTurnResult,
EvalRunRequest,
} from "../types/api";
export { QueryClientProvider };
@ -70,7 +71,9 @@ export function useProposal(id: string) {
export function useEvalLanes() {
return useQuery<EvalLaneSummary[]>({
queryKey: ["api", "evals"],
queryFn: () => apiFetch<EvalLaneSummary[]>("/evals"),
queryFn: fetchEvalLanes,
staleTime: 60_000,
refetchOnWindowFocus: false,
});
}
@ -82,6 +85,13 @@ export function useEvalLane(name: string) {
});
}
export function useEvalRun() {
return useMutation<EvalRunResult, WorkbenchApiError, EvalRunRequest>({
mutationKey: ["eval-run"],
mutationFn: runEvalLane,
});
}
export function useChatTurn() {
return useMutation<ChatTurnResult, WorkbenchApiError, { prompt: string }>({
mutationKey: ["chat-turn"],
@ -93,3 +103,4 @@ export function useChatTurn() {
}),
});
}

View file

@ -7,7 +7,7 @@ import { ChatRoute } from "../routes/ChatRoute";
import { TraceRoutePlaceholder } from "../routes/TraceRoutePlaceholder";
import { ReplayRoutePlaceholder } from "../routes/ReplayRoutePlaceholder";
import { ProposalsRoutePlaceholder } from "../routes/ProposalsRoutePlaceholder";
import { EvalsRoutePlaceholder } from "../routes/EvalsRoutePlaceholder";
import { EvalsRoute } from "./evals/EvalsRoute";
import { RunsRoutePlaceholder } from "../routes/RunsRoutePlaceholder";
import { PacksRoutePlaceholder } from "../routes/PacksRoutePlaceholder";
import { VaultRoutePlaceholder } from "../routes/VaultRoutePlaceholder";
@ -25,7 +25,7 @@ export function App() {
<Route path="trace" element={<TraceRoutePlaceholder />} />
<Route path="replay" element={<ReplayRoutePlaceholder />} />
<Route path="proposals" element={<ProposalsRoutePlaceholder />} />
<Route path="evals" element={<EvalsRoutePlaceholder />} />
<Route path="evals" element={<EvalsRoute />} />
<Route path="runs" element={<RunsRoutePlaceholder />} />
<Route path="packs" element={<PacksRoutePlaceholder />} />
<Route path="vault" element={<VaultRoutePlaceholder />} />

View file

@ -0,0 +1,32 @@
import { InfoBadge } from "../../design/components/badges/Badge";
export function EvalArtifactLink({
lane,
sourceDigest,
}: {
lane: string;
sourceDigest: string;
}) {
const label = sourceDigest.slice(0, 12);
const location = lane.includes("contemplation")
? "engine_state/"
: `evals/${lane}/results/`;
return (
<div className="flex flex-wrap items-center gap-2 text-xs" data-testid="eval-artifact-link">
<span className="text-[var(--color-text-secondary)] font-medium">Source Digest:</span>
<InfoBadge
label={label}
colorToken="--color-text-mono"
meaning="Deterministic source digest for this run. Click to copy."
adr="ADR-0160 / ADR-0162"
evidence="EvalRunResult.source_digest is present."
mono
onCopy={sourceDigest}
/>
<span className="text-[var(--color-text-muted)]">
Located at: <code className="font-mono bg-[var(--color-surface-inset)] px-1.5 py-0.5 rounded border border-[var(--color-border-subtle)] text-[var(--color-text-primary)]">{location}</code>
</span>
</div>
);
}

View file

@ -0,0 +1,106 @@
import { StableJsonViewer } from "../../design/components/StableJsonViewer/StableJsonViewer";
import { EmptyState } from "../../design/components/states/EmptyState";
interface CaseItem {
case_id?: string;
id?: string;
passed?: boolean;
expected?: unknown;
actual?: unknown;
failure_reason?: string;
failure_reasons?: string[];
[key: string]: unknown;
}
function ensureJsonString(val: unknown): string {
if (val === undefined) return "";
if (typeof val === "string") {
try {
JSON.parse(val);
return val;
} catch {
return JSON.stringify(val);
}
}
return JSON.stringify(val, null, 2);
}
export function EvalFailureViewer({
cases,
passed,
laneName,
}: {
cases: any[];
passed: boolean | null;
laneName: string;
}) {
const typedCases = cases as CaseItem[];
const failures = typedCases.filter((c) => c.passed === false);
if (passed === true || failures.length === 0) {
return (
<EmptyState
statement={`All checks passed for eval lane ${laneName}.`}
nextAction={{ kind: "cli", command: `core eval --lane ${laneName}` }}
/>
);
}
return (
<div className="grid gap-4" data-testid="eval-failures">
<h3 className="font-semibold text-lg text-[var(--color-state-contradicted)]">
Failures ({failures.length})
</h3>
<div className="grid gap-4">
{failures.map((c, idx) => {
const caseId = c.case_id || c.id || `case-${idx}`;
const reason = c.failure_reason || (Array.isArray(c.failure_reasons) ? c.failure_reasons.join(", ") : "") || "No reason specified";
const hasExpectedActual = c.expected !== undefined || c.actual !== undefined;
const expectedStr = c.expected !== undefined
? ensureJsonString(c.expected)
: JSON.stringify(c, null, 2);
const actualStr = c.actual !== undefined
? ensureJsonString(c.actual)
: "";
return (
<div
key={caseId}
className="rounded-lg border border-[var(--color-state-danger-border)] bg-[var(--color-surface-raised)] p-4 shadow-sm"
data-testid="failure-card"
>
<div className="flex items-center justify-between gap-2 border-b border-[var(--color-border-subtle)] pb-2 mb-3">
<span className="font-mono text-sm font-semibold text-[var(--color-text-primary)]">
{caseId}
</span>
<span className="rounded bg-[var(--color-state-contradicted)]/10 px-2 py-0.5 text-xs font-semibold text-[var(--color-state-contradicted)]">
Failed
</span>
</div>
<div className="mb-3">
<div className="font-semibold text-xs text-[var(--color-text-secondary)] mb-1">Reason</div>
<div className="text-sm text-[var(--color-text-primary)] font-mono bg-[var(--color-surface-inset)] p-2 rounded border border-[var(--color-border-subtle)]">
{reason}
</div>
</div>
<div>
<div className="font-semibold text-xs text-[var(--color-text-secondary)] mb-1">
{hasExpectedActual ? "Expected vs Actual" : "Case Details"}
</div>
{hasExpectedActual ? (
<StableJsonViewer source={expectedStr} compareSource={actualStr} />
) : (
<StableJsonViewer source={expectedStr} />
)}
</div>
</div>
);
})}
</div>
</div>
);
}

View file

@ -0,0 +1,67 @@
import type { EvalLaneSummary } from "../../types/api";
export function EvalLaneCard({
lane,
isSelected,
onSelect,
}: {
lane: EvalLaneSummary;
isSelected: boolean;
onSelect: () => void;
}) {
const readOnlyStyle = lane.read_only
? {
backgroundColor: "color-mix(in srgb, var(--color-state-verified) 18%, transparent)",
borderColor: "var(--color-state-verified)",
color: "var(--color-state-verified)",
}
: {
backgroundColor: "color-mix(in srgb, var(--color-state-undetermined) 18%, transparent)",
borderColor: "var(--color-state-undetermined)",
color: "var(--color-text-muted)",
};
return (
<button
onClick={onSelect}
className={`w-full text-left p-3 rounded-lg border transition-all duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)] ${
isSelected
? "border-[var(--color-border-strong)] bg-[var(--color-surface-raised)] shadow-[var(--shadow-panel)]"
: "border-[var(--color-border-subtle)] bg-[var(--color-surface-base)] hover:bg-[var(--color-surface-raised)]"
}`}
type="button"
data-testid="eval-lane-card"
>
<div className="flex items-start justify-between gap-2">
<h4 className="font-mono text-sm font-semibold text-[var(--color-text-primary)] truncate">
{lane.lane}
</h4>
<span
style={readOnlyStyle}
className="shrink-0 inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-semibold transition-colors"
title={lane.read_only ? "API runs enabled" : "API run disabled — use CLI"}
data-testid="read-only-badge"
>
{lane.read_only ? "Read-Only" : "CLI-Only"}
</span>
</div>
{lane.description && (
<p className="mt-1 text-xs text-[var(--color-text-secondary)] line-clamp-2">
{lane.description}
</p>
)}
<div className="mt-2.5 flex flex-wrap gap-1" data-testid="version-badges">
{lane.versions.map((version) => (
<span
key={version}
className="inline-flex items-center rounded bg-[var(--color-state-neutral-bg)] border border-[var(--color-state-neutral-border)] px-1 py-0.5 font-mono text-[9px] text-[var(--color-state-neutral-text)]"
>
{version}
</span>
))}
</div>
</button>
);
}

View file

@ -0,0 +1,98 @@
import { InfoBadge } from "../../design/components/badges/Badge";
export function EvalMetricGrid({
metrics,
}: {
metrics: Record<string, unknown>;
}) {
const sortedKeys = Object.keys(metrics).sort();
function formatValue(value: unknown): string {
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "number") {
return String(value);
}
if (typeof value === "object" && value !== null) {
return JSON.stringify(value, null, 2);
}
return String(value ?? "");
}
function getUnit(key: string): string | undefined {
const k = key.toLowerCase();
if (k.endsWith("_ms") || k.includes("ms") || k.includes("latency")) {
return "ms";
}
if (k.endsWith("_rate") || k.endsWith("_pct") || k.includes("percentage")) {
return "%";
}
if (k.endsWith("_sec") || k.includes("seconds")) {
return "s";
}
return undefined;
}
function renderPassFailBadge(key: string, value: unknown) {
const isBool = typeof value === "boolean";
const k = key.toLowerCase();
const isPassKey = k.includes("pass") || k.includes("success") || k === "passed";
if (isBool && isPassKey) {
if (value === true) {
return (
<InfoBadge
label="Passed"
colorToken="--color-state-verified"
meaning="Metric passed the verification criteria."
adr="ADR-0160 / ADR-0162"
evidence="Metric value indicates success."
/>
);
} else {
return (
<InfoBadge
label="Failed"
colorToken="--color-state-contradicted"
meaning="Metric failed the verification criteria."
adr="ADR-0160 / ADR-0162"
evidence="Metric value indicates failure."
/>
);
}
}
return null;
}
return (
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3" data-testid="eval-metric-grid">
{sortedKeys.map((key) => {
const val = metrics[key];
const unit = getUnit(key);
const badge = renderPassFailBadge(key, val);
const formatted = formatValue(val);
const isObj = typeof val === "object" && val !== null;
return (
<div
key={key}
className="flex flex-col justify-between rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-3 shadow-sm"
data-testid="metric-card"
>
<div>
<div className="text-[10px] font-semibold text-[var(--color-text-muted)] uppercase tracking-wider truncate" title={key}>
{key.replaceAll("_", " ")}
</div>
<div className={`mt-1 font-mono text-sm font-semibold text-[var(--color-text-primary)] ${isObj ? "whitespace-pre overflow-x-auto text-[10px] bg-[var(--color-surface-inset)] p-1.5 rounded" : ""}`}>
{formatted}
{unit && <span className="ml-1 text-xs text-[var(--color-text-muted)] font-sans font-normal">{unit}</span>}
</div>
</div>
{badge && <div className="mt-2 flex justify-start">{badge}</div>}
</div>
);
})}
</div>
);
}

View file

@ -0,0 +1,104 @@
import { useState, useEffect } from "react";
import { useEvalRun } from "../../api/queries";
import { Button } from "../../design/components/primitives/Button";
import type { EvalLaneSummary, EvalRunResult } from "../../types/api";
import type { WorkbenchApiError } from "../../api/client";
export function EvalRunButton({
lane,
onRunStart,
onRunSuccess,
onRunError,
}: {
lane: EvalLaneSummary | null;
onRunStart: () => void;
onRunSuccess: (result: EvalRunResult) => void;
onRunError: (error: WorkbenchApiError) => void;
}) {
const evalRun = useEvalRun();
const [version, setVersion] = useState("");
const [split, setSplit] = useState<"dev" | "public" | "holdout">("public");
// Reset selection when lane changes
useEffect(() => {
if (lane) {
setVersion(lane.versions[0] || "v1");
setSplit("public");
}
}, [lane]);
if (!lane || !lane.read_only) {
return null;
}
const handleRun = () => {
onRunStart();
evalRun.mutate(
{ lane: lane.lane, version, split },
{
onSuccess: (data) => {
onRunSuccess(data);
},
onError: (err) => {
onRunError(err);
},
}
);
};
const isPending = evalRun.isPending;
return (
<div className="rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-4 shadow-sm" data-testid="eval-run-form">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Run Configuration</h3>
<div className="flex flex-wrap gap-4 items-end">
<div className="flex flex-col gap-1.5">
<label htmlFor="version-select" className="text-xs text-[var(--color-text-secondary)] font-medium">
Version
</label>
<select
id="version-select"
value={version}
onChange={(e) => setVersion(e.target.value)}
disabled={isPending}
className="bg-[var(--color-surface-base)] border border-[var(--color-border-subtle)] rounded px-2.5 py-1.5 text-xs text-[var(--color-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-focus-ring)] disabled:opacity-50"
>
{lane.versions.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="split-select" className="text-xs text-[var(--color-text-secondary)] font-medium">
Split
</label>
<select
id="split-select"
value={split}
onChange={(e) => setSplit(e.target.value as any)}
disabled={isPending}
className="bg-[var(--color-surface-base)] border border-[var(--color-border-subtle)] rounded px-2.5 py-1.5 text-xs text-[var(--color-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-focus-ring)] disabled:opacity-50"
>
<option value="public">Public</option>
<option value="dev">Dev</option>
<option value="holdout" title="Holdout runs require sealed-eval config — use CLI" disabled>
Holdout
</option>
</select>
</div>
<Button
onClick={handleRun}
disabled={isPending}
className="h-8.5"
data-testid="run-button"
>
{isPending ? "Running..." : "Run Eval"}
</Button>
</div>
</div>
);
}

View file

@ -0,0 +1,188 @@
import { useState } from "react";
import { useSearchParams } from "react-router-dom";
import { useEvalLanes } from "../../api/queries";
import { EvalLaneCard } from "./EvalLaneCard";
import { EvalRunButton } from "./EvalRunButton";
import { EvalMetricGrid } from "./EvalMetricGrid";
import { EvalFailureViewer } from "./EvalFailureViewer";
import { EvalArtifactLink } from "./EvalArtifactLink";
import { EmptyState } from "../../design/components/states/EmptyState";
import { ErrorState } from "../../design/components/states/ErrorState";
import { LoadingState } from "../../design/components/states/LoadingState";
import type { EvalRunResult } from "../../types/api";
import { WorkbenchApiError } from "../../api/client";
export function EvalsRoute() {
const { data: lanes, isLoading, isError, error } = useEvalLanes();
const [searchParams, setSearchParams] = useSearchParams();
const selectedLaneName = searchParams.get("lane") || "";
// Maintain per-lane run states (pending, result, error)
const [runStates, setRunStates] = useState<
Record<
string,
{
isPending: boolean;
result?: EvalRunResult;
error?: WorkbenchApiError;
}
>
>({});
if (isLoading) {
return <LoadingState label="Loading eval lanes..." />;
}
if (isError) {
return (
<ErrorState
whatFailed={error instanceof Error ? error.message : "Failed to load eval lanes."}
mutationStatus="No corpus mutation occurred."
reproducer="curl -X GET http://127.0.0.1:8765/evals"
retrySafety="Retry: safe"
/>
);
}
const selectedLane = lanes?.find((l) => l.lane === selectedLaneName) || null;
const currentRunState = selectedLaneName ? runStates[selectedLaneName] : null;
return (
<div className="grid h-full grid-cols-1 gap-4 md:grid-cols-[18rem_1fr]" data-testid="evals-route">
{/* Left Pane: Lane List */}
<div className="flex flex-col gap-3 border-r border-[var(--color-border-subtle)] pr-4 overflow-y-auto">
<h2 className="text-md font-semibold text-[var(--color-text-primary)]">Eval Lanes</h2>
<div className="flex flex-col gap-2">
{lanes && lanes.length > 0 ? (
lanes.map((lane) => (
<EvalLaneCard
key={lane.lane}
lane={lane}
isSelected={lane.lane === selectedLaneName}
onSelect={() => setSearchParams({ lane: lane.lane })}
/>
))
) : (
<EmptyState
statement="No eval lanes discovered."
nextAction={{ kind: "cli", command: "core eval --list" }}
/>
)}
</div>
</div>
{/* Right Pane: Results / Form */}
<div className="flex flex-col gap-4 overflow-y-auto pl-2">
{selectedLane ? (
<>
{/* Header info */}
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--color-border-subtle)] pb-3">
<div>
<h2 className="text-lg font-semibold text-[var(--color-text-primary)] font-mono">
{selectedLane.lane}
</h2>
{selectedLane.description && (
<p className="mt-1 text-xs text-[var(--color-text-secondary)]">
{selectedLane.description}
</p>
)}
</div>
</div>
{/* Run Button configuration if read-only */}
{selectedLane.read_only ? (
<EvalRunButton
lane={selectedLane}
onRunStart={() => {
setRunStates((prev) => ({
...prev,
[selectedLane.lane]: { isPending: true },
}));
}}
onRunSuccess={(result) => {
setRunStates((prev) => ({
...prev,
[selectedLane.lane]: { isPending: false, result },
}));
}}
onRunError={(err) => {
setRunStates((prev) => ({
...prev,
[selectedLane.lane]: { isPending: false, error: err },
}));
}}
/>
) : (
<div className="text-xs text-[var(--color-text-muted)] bg-[var(--color-surface-inset)] p-3 rounded-lg border border-[var(--color-border-subtle)] font-medium">
API runs disabled for write-active lane. Use local CLI to execute this lane:
<code className="block mt-1 font-mono text-[var(--color-text-primary)] bg-[var(--color-surface-base)] p-1.5 rounded">
core eval --lane {selectedLane.lane}
</code>
</div>
)}
{/* Result display */}
{currentRunState?.isPending ? (
<LoadingState label="Running eval lane..." />
) : currentRunState?.error ? (
<ErrorState
whatFailed={currentRunState.error.message}
mutationStatus="No corpus mutation occurred."
reproducer={`core eval --lane ${selectedLane.lane}`}
retrySafety="Retry: safe"
/>
) : currentRunState?.result ? (
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-3">
<span className="text-xs text-[var(--color-text-secondary)] font-semibold">
Status:
</span>
<span
className={`rounded-md px-2 py-0.5 text-xs font-semibold ${
currentRunState.result.passed
? "bg-[var(--color-state-success-bg)] text-[var(--color-state-success-text)] border border-[var(--color-state-success-border)]"
: "bg-[var(--color-state-danger-bg)] text-[var(--color-state-danger-text)] border border-[var(--color-state-danger-border)]"
}`}
>
{currentRunState.result.passed ? "Passed" : "Failed"}
</span>
{currentRunState.result.source_digest && (
<EvalArtifactLink
lane={selectedLane.lane}
sourceDigest={currentRunState.result.source_digest}
/>
)}
</div>
<div className="flex flex-col gap-2">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">Metrics</h3>
<EvalMetricGrid metrics={currentRunState.result.metrics} />
</div>
<EvalFailureViewer
cases={currentRunState.result.cases}
passed={currentRunState.result.passed}
laneName={selectedLane.lane}
/>
</div>
) : (
<EmptyState
statement={
selectedLane.read_only
? `No run results for lane "${selectedLane.lane}" in this session. Trigger a run above.`
: `Eval lane "${selectedLane.lane}" is CLI-only. No session results available.`
}
nextAction={{ kind: "cli", command: `core eval --lane ${selectedLane.lane}` }}
/>
)}
</>
) : (
<EmptyState
statement="Select an eval lane from the list to view results or run checks."
nextAction={{ kind: "cli", command: "core eval --list" }}
/>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,299 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { MemoryRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { EvalLaneCard } from "./EvalLaneCard";
import { EvalRunButton } from "./EvalRunButton";
import { EvalMetricGrid } from "./EvalMetricGrid";
import { EvalFailureViewer } from "./EvalFailureViewer";
import { EvalsRoute } from "./EvalsRoute";
import { runEvalLane, WorkbenchApiError } from "../../api/client";
import type { EvalLaneSummary, EvalRunResult } from "../../types/api";
// Mock queries
vi.mock("../../api/queries", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../api/queries")>();
return {
...actual,
useEvalLanes: vi.fn(),
useEvalRun: vi.fn(),
};
});
import { useEvalLanes, useEvalRun } from "../../api/queries";
const mockLanes: EvalLaneSummary[] = [
{ lane: "contemplation_quality", versions: ["v1", "v2"], read_only: true, description: "Contemplation checks" },
{ lane: "unsafe_lane", versions: ["v1"], read_only: false, description: "Unsafe checks" },
];
const mockResult: EvalRunResult = {
lane: "contemplation_quality",
version: "v1",
split: "public",
passed: false,
metrics: { accuracy: 0.8, passed: 4, total: 5 },
cases: [
{ case_id: "c1", passed: true },
{ case_id: "c2", passed: false, expected: "val1", actual: "val2", failure_reason: "Mismatch value" },
],
source_digest: "abcdef1234567890",
};
function makeClient() {
return new QueryClient({ defaultOptions: { queries: { retry: false } } });
}
describe("W-030 Component Tests", () => {
const fetchMock = vi.fn();
beforeEach(() => {
vi.resetAllMocks();
fetchMock.mockImplementation((url: any) => {
const urlStr = typeof url === "string" ? url : String(url?.url || url || "");
if (urlStr.endsWith("/evals")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ ok: true, generated_at: "2026-05-26T00:00:00Z", data: mockLanes }),
});
}
if (urlStr.endsWith("/evals/run")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ ok: true, generated_at: "2026-05-26T00:00:00Z", data: mockResult }),
});
}
return Promise.reject(new Error(`Unhandled URL: ${urlStr}`));
});
vi.stubGlobal("fetch", fetchMock);
if (typeof window !== "undefined") {
delete (window as any).sealedEvalConfig;
}
});
// 1. EvalLaneCard
describe("EvalLaneCard", () => {
it("renders read_only badge correctly for both values; clicking selects via callback", () => {
const selectSpy = vi.fn();
// Test read_only === true
const { rerender } = render(
<EvalLaneCard lane={mockLanes[0]} isSelected={false} onSelect={selectSpy} />
);
expect(screen.getByText("Read-Only")).toBeInTheDocument();
// Click selects via callback
fireEvent.click(screen.getByTestId("eval-lane-card"));
expect(selectSpy).toHaveBeenCalledTimes(1);
// Test read_only === false
rerender(<EvalLaneCard lane={mockLanes[1]} isSelected={false} onSelect={selectSpy} />);
expect(screen.getByText("CLI-Only")).toBeInTheDocument();
expect(screen.getByTitle("API run disabled — use CLI")).toBeInTheDocument();
});
});
// 2. EvalRunButton
describe("EvalRunButton", () => {
it("hidden when lane.read_only === false; visible but holdout-disabled with correct hover-text otherwise", () => {
// Mock useEvalRun mutation
vi.mocked(useEvalRun).mockReturnValue({
mutate: vi.fn(),
isPending: false,
isError: false,
isSuccess: false,
error: null,
} as any);
const runStartSpy = vi.fn();
const runSuccessSpy = vi.fn();
const runErrorSpy = vi.fn();
// Non-read-only lane -> should be null
const { rerender } = render(
<EvalRunButton
lane={mockLanes[1]}
onRunStart={runStartSpy}
onRunSuccess={runSuccessSpy}
onRunError={runErrorSpy}
/>
);
expect(screen.queryByTestId("eval-run-form")).not.toBeInTheDocument();
// Read-only lane -> should render, holdout disabled
rerender(
<EvalRunButton
lane={mockLanes[0]}
onRunStart={runStartSpy}
onRunSuccess={runSuccessSpy}
onRunError={runErrorSpy}
/>
);
expect(screen.getByTestId("eval-run-form")).toBeInTheDocument();
const holdoutOption = screen.getByRole("option", { name: "Holdout" }) as HTMLOptionElement;
expect(holdoutOption).toBeDisabled();
expect(holdoutOption.getAttribute("title")).toBe("Holdout runs require sealed-eval config — use CLI");
});
});
// 3. EvalMetricGrid
describe("EvalMetricGrid", () => {
it("enforces deterministic key ordering across two identical runs", () => {
const metricsRun1 = {
total: 10,
passed: 9,
accuracy: 0.9,
latency_ms: 150,
};
const metricsRun2 = {
latency_ms: 150,
accuracy: 0.9,
passed: 9,
total: 10,
};
const { container: container1, unmount: unmount1 } = render(<EvalMetricGrid metrics={metricsRun1} />);
const cards1 = Array.from(container1.querySelectorAll('[data-testid="metric-card"]')).map(el => el.textContent);
unmount1();
const { container: container2 } = render(<EvalMetricGrid metrics={metricsRun2} />);
const cards2 = Array.from(container2.querySelectorAll('[data-testid="metric-card"]')).map(el => el.textContent);
// Verify lexicographical order
expect(cards1).toEqual(cards2);
expect(cards1[0]).toContain("accuracy");
expect(cards1[1]).toContain("latency ms");
expect(cards1[2]).toContain("passed");
expect(cards1[3]).toContain("total");
});
});
// 4. EvalFailureViewer
describe("EvalFailureViewer", () => {
it("renders calm-success EmptyState with 0 failures", () => {
render(
<EvalFailureViewer cases={[{ case_id: "c1", passed: true }]} passed={true} laneName="contemplation_quality" />
);
expect(screen.queryByTestId("eval-failures")).not.toBeInTheDocument();
expect(screen.getByText("All checks passed for eval lane contemplation_quality.")).toBeInTheDocument();
});
it("renders failure cards with expected vs actual diff for failed cases", () => {
render(
<EvalFailureViewer
cases={[
{ case_id: "c1", passed: true },
{ case_id: "c2", passed: false, expected: "val1", actual: "val2", failure_reason: "Wrong outcome" },
]}
passed={false}
laneName="contemplation_quality"
/>
);
expect(screen.getByText("Failures (1)")).toBeInTheDocument();
expect(screen.getByText("c2")).toBeInTheDocument();
expect(screen.getByText("Wrong outcome")).toBeInTheDocument();
expect(screen.getByTestId("json-diff")).toBeInTheDocument();
});
});
// 5. EvalsRoute
describe("EvalsRoute", () => {
it("full happy-path with selection, running and result viewing", async () => {
// Mock useEvalLanes query
vi.mocked(useEvalLanes).mockReturnValue({
data: mockLanes,
isLoading: false,
isError: false,
error: null,
} as any);
// Mock useEvalRun mutation
let mutateCallback: any;
vi.mocked(useEvalRun).mockReturnValue({
mutate: vi.fn((req, options) => {
mutateCallback = options;
}),
isPending: false,
isError: false,
isSuccess: false,
error: null,
} as any);
const client = makeClient();
render(
<QueryClientProvider client={client}>
<MemoryRouter initialEntries={["/evals?lane=contemplation_quality"]}>
<EvalsRoute />
</MemoryRouter>
</QueryClientProvider>
);
// Verify lane details are shown
expect(screen.getByText("contemplation_quality", { selector: "h2" })).toBeInTheDocument();
expect(screen.getByTestId("run-button")).toBeInTheDocument();
// Trigger run
fireEvent.click(screen.getByTestId("run-button"));
// Simulate success
expect(mutateCallback).toBeDefined();
mutateCallback.onSuccess(mockResult);
// Result should be visible
await waitFor(() => {
expect(screen.getByText("Status:")).toBeInTheDocument();
});
expect(screen.getByText("accuracy")).toBeInTheDocument();
expect(screen.getByText("Failures (1)")).toBeInTheDocument();
});
it("renders ErrorState when fetchEvalLanes fails", () => {
vi.mocked(useEvalLanes).mockReturnValue({
data: null,
isLoading: false,
isError: true,
error: new Error("Network Error"),
} as any);
const client = makeClient();
render(
<QueryClientProvider client={client}>
<MemoryRouter initialEntries={["/evals"]}>
<EvalsRoute />
</MemoryRouter>
</QueryClientProvider>
);
expect(screen.getByText("Network Error")).toBeInTheDocument();
expect(screen.getByText("Mutation status")).toBeInTheDocument();
});
it("verifies direct runEvalLane client-side refusal logic", async () => {
// Refusal 1: lane.read_only === false
await expect(
runEvalLane({ lane: "unsafe_lane", split: "public" })
).rejects.toThrowError(
new WorkbenchApiError("client_refused_unsafe_lane", "API run disabled — use CLI")
);
// Refusal 2: split === "holdout" without sealed config flag
await expect(
runEvalLane({ lane: "contemplation_quality", split: "holdout" })
).rejects.toThrowError(
new WorkbenchApiError("client_refused_sealed_holdout", "Holdout runs require sealed-eval config — use CLI")
);
// Success: split === "holdout" with sealed config flag
if (typeof window !== "undefined") {
(window as any).sealedEvalConfig = true;
}
// Expect it to call fetch successfully without throwing refusals
const res = await runEvalLane({ lane: "contemplation_quality", split: "holdout" });
expect(res).toEqual(mockResult);
});
});
});

View file

@ -1,10 +0,0 @@
import { EmptyState } from "../design/components/states/EmptyState";
export function EvalsRoutePlaceholder() {
return (
<EmptyState
statement="Evals — no data loaded yet."
nextAction={{ kind: "cli", command: "core eval --list" }}
/>
);
}

View file

@ -7,7 +7,9 @@ export type ErrorCode =
| "unsupported"
| "read_error"
| "eval_failed"
| "runtime_unavailable";
| "runtime_unavailable"
| "client_refused_unsafe_lane"
| "client_refused_sealed_holdout";
export type Backend = "numpy" | "mlx" | "rust" | "unknown";
export type MutationMode = "read_only" | "runtime_turn";
@ -130,6 +132,12 @@ export interface EvalLaneSummary {
description: string | null;
}
export interface EvalRunRequest {
lane: string;
version?: string;
split?: "dev" | "public" | "holdout";
}
export interface EvalRunResult {
lane: string;
version: string;