feat(workbench/replay): polish states, severity labels, metadata layout (wave 1c)

This commit is contained in:
Shay 2026-05-28 08:09:21 -07:00
parent a4a24de836
commit ac947845c7
5 changed files with 244 additions and 26 deletions

View file

@ -3,6 +3,7 @@ import { ReplayStatusBadge, TraceHashBadge, ReplayStatus } from "../../design/co
import { EmptyState } from "../../design/components/states/EmptyState";
import { ReplayDiffViewer } from "./ReplayDiffViewer";
import { ReplayMetadataTable } from "./ReplayMetadataTable";
import { Link, useSearchParams } from "react-router-dom";
interface ReplayComparisonPanelProps {
artifact: ArtifactDetail;
@ -15,6 +16,9 @@ export function ReplayComparisonPanel({
comparison,
status,
}: ReplayComparisonPanelProps) {
const [searchParams] = useSearchParams();
const fromProposal = searchParams.get("fromProposal");
const finalComparison: ReplayComparison = comparison || {
artifact_id: artifact.artifact_id,
original_hash: artifact.digest,
@ -25,6 +29,17 @@ export function ReplayComparisonPanel({
return (
<div className="space-y-6" data-testid="replay-comparison-panel">
{fromProposal && (
<div className="mb-2">
<Link
to={`/proposals?proposal_id=${encodeURIComponent(fromProposal)}`}
className="text-xs text-[var(--color-link)] hover:underline inline-flex items-center gap-1 font-medium"
data-testid="back-to-proposal"
>
Back to proposal #{fromProposal}
</Link>
</div>
)}
<div className="flex items-center justify-between border-b border-[var(--color-border-subtle)] pb-4">
<h2 className="text-base font-semibold text-[var(--color-text-primary)]">Replay Evidence</h2>
<ReplayStatusBadge value={status} />

View file

@ -54,6 +54,12 @@ export function ReplayDiffViewer({ divergences }: ReplayDiffViewerProps) {
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-[var(--color-border-subtle)] pb-2">
<div className="flex items-center gap-2">
<ReplayDivergenceSeverityBadge value={div.severity as ReplayDivergenceSeverity} />
<span
className="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-muted)]"
data-testid={`severity-label-${div.severity}`}
>
{div.severity === "failure" ? "breaking" : div.severity === "warning" ? "material" : "low"}
</span>
<span className="font-mono text-xs text-[var(--color-text-secondary)] break-all select-all">
{div.path}
</span>

View file

@ -15,6 +15,31 @@ export function ReplayMetadataTable({ artifact, comparison }: ReplayMetadataTabl
{ info: 0, warning: 0, failure: 0 } as Record<"info" | "warning" | "failure", number>
);
// Extract run ID if present in content
let runId: string | null = null;
if (artifact.content && typeof artifact.content === "object") {
const contentObj = artifact.content as any;
runId = contentObj.run_id || contentObj.runId || contentObj.workflow_run_id || contentObj.session_id || contentObj.reasoning_trace_id || null;
}
// Extract lane if present in content or parsed from path
let lane: string | null = null;
if (artifact.content && typeof artifact.content === "object") {
const contentObj = artifact.content as any;
lane = contentObj.lane || contentObj.lane_name || contentObj.laneName || contentObj.split || null;
}
if (!lane && artifact.path) {
const parts = artifact.path.split("/");
if (parts[0] === "evals" && parts.length > 1) {
lane = parts[1];
}
}
// Format timestamp nicely
const timestamp = artifact.created_at
? new Date(artifact.created_at).toLocaleString()
: null;
return (
<section className="rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-4">
<h3 className="mb-3 text-sm font-semibold text-[var(--color-text-primary)]">Metadata Audit Details</h3>
@ -22,8 +47,8 @@ export function ReplayMetadataTable({ artifact, comparison }: ReplayMetadataTabl
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="border-b border-[var(--color-border-subtle)] text-[var(--color-text-muted)]">
<th className="pb-2 font-medium">Property</th>
<th className="pb-2 font-medium">Value</th>
<th className="pb-2 font-medium w-1/3">Property</th>
<th className="pb-2 font-medium w-2/3">Value</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--color-border-subtle)] text-[var(--color-text-secondary)]">
@ -35,12 +60,32 @@ export function ReplayMetadataTable({ artifact, comparison }: ReplayMetadataTabl
<td className="py-2 font-medium">Kind</td>
<td className="py-2">{artifact.kind}</td>
</tr>
<tr>
<td className="py-2 font-medium">Content Type</td>
<td className="py-2 font-mono">{artifact.content_type}</td>
</tr>
<tr>
<td className="py-2 font-medium">Path</td>
<td className="py-2 font-mono" data-testid="artifact-path-text">
{artifact.path}
</td>
</tr>
<tr>
<td className="py-2 font-medium">Created At</td>
<td className="py-2" data-testid="artifact-created-at">
{timestamp || <span className="text-[var(--color-text-muted)]">None</span>}
</td>
</tr>
<tr>
<td className="py-2 font-medium">Artifact Digest / Trace Hash</td>
<td className="py-2">
{artifact.digest ? (
<TraceHashBadge value={artifact.digest} />
) : (
<span className="text-[var(--color-text-muted)]">None</span>
)}
</td>
</tr>
<tr>
<td className="py-2 font-medium">Original Hash</td>
<td className="py-2">
@ -62,24 +107,32 @@ export function ReplayMetadataTable({ artifact, comparison }: ReplayMetadataTabl
</td>
</tr>
<tr>
<td className="py-2 font-medium">Divergences</td>
<td className="py-2">
<span className="inline-flex gap-2">
<span className="text-[var(--color-review-rejected)]">
Failure: {divergenceCounts.failure}
</span>
<span className="text-[var(--color-review-pending)]">
Warning: {divergenceCounts.warning}
</span>
<span className="text-[var(--color-grounding-vault)]">
Info: {divergenceCounts.info}
</span>
</span>
<td className="py-2 font-medium">Run ID</td>
<td className="py-2 font-mono" data-testid="artifact-run-id">
{runId || <span className="text-[var(--color-text-muted)]">None</span>}
</td>
</tr>
<tr>
<td className="py-2 font-medium">Content Type</td>
<td className="py-2 font-mono">{artifact.content_type}</td>
<td className="py-2 font-medium">Lane</td>
<td className="py-2" data-testid="artifact-lane">
{lane || <span className="text-[var(--color-text-muted)]">None</span>}
</td>
</tr>
<tr>
<td className="py-2 font-medium">Divergences</td>
<td className="py-2">
<span className="inline-flex flex-wrap gap-x-3 gap-y-1">
<span className="text-[var(--color-review-rejected)] font-medium">
Failure (breaking): {divergenceCounts.failure}
</span>
<span className="text-[var(--color-review-pending)] font-medium">
Warning (material): {divergenceCounts.warning}
</span>
<span className="text-[var(--color-grounding-vault)] font-medium">
Info (low): {divergenceCounts.info}
</span>
</span>
</td>
</tr>
</tbody>
</table>

View file

@ -1,3 +1,19 @@
/**
* SURVEY OF THE REPLAY THEATER COMPONENTS (src/app/replay/):
*
* 1. ArtifactList.tsx: Grouped and sorted list of available artifacts (traces, eval results,
* proposals, etc.) generated by the backend. Allows selection via `onSelect` callback.
* 2. ReplayComparisonPanel.tsx: Orchestrates the display of original and replay digests (hashes),
* the status badges, and switches between empty states (intact/equivalent, not replayed, unsupported)
* and the divergence differences viewer.
* 3. ReplayDiffViewer.tsx: Displays side-by-side original vs. replayed value comparisons for
* detected divergences using StableJsonViewer, sorted by severity (failure -> warning -> info).
* 4. ReplayMetadataTable.tsx: Standard property table detailing the selected artifact's metadata
* (artifact ID, kind, path, original hash, replay hash, divergence severity counts, content type).
* 5. ReplayRoute.tsx: The main page route coordinating state from URL search params (artifactId, fromProposal),
* querying artifacts lists/details/comparisons, and displaying appropriate loading, error, and detail panels.
*/
import { useSearchParams } from "react-router-dom";
import { useArtifacts, useArtifactDetail, useReplayComparison } from "../../api/queries";
import { ArtifactList } from "./ArtifactList";
@ -83,9 +99,16 @@ export function ReplayRoute() {
{isLoadingArtifacts ? (
<LoadingState label="Comparing artifacts..." />
) : artifactsQuery.isError ? (
<div className="p-2 text-xs text-[var(--color-state-danger-text)]">
Failed to load artifacts.
</div>
<ErrorState
whatFailed={
artifactsError instanceof WorkbenchApiError
? artifactsError.message
: "Failed to load artifacts."
}
mutationStatus="No corpus mutation occurred."
reproducer="curl -X GET /artifacts"
retrySafety="Retry: safe"
/>
) : (
<ArtifactList
artifacts={artifactsQuery.data || []}
@ -127,7 +150,12 @@ export function ReplayRoute() {
: ReplayStatus.DIVERGED
}
/>
) : null}
) : (
<EmptyState
statement="Selected artifact not found."
nextAction="Select a valid artifact from the list."
/>
)}
</div>
</div>
);

View file

@ -209,11 +209,13 @@ describe("W-031 Replay Theater Tests", () => {
};
render(
<ReplayComparisonPanel
artifact={mockArtifactDetail}
comparison={eqComparison}
status={ReplayStatus.EQUIVALENT}
/>
<MemoryRouter>
<ReplayComparisonPanel
artifact={mockArtifactDetail}
comparison={eqComparison}
status={ReplayStatus.EQUIVALENT}
/>
</MemoryRouter>
);
// Check for calm empty state text
@ -262,6 +264,14 @@ describe("W-031 Replay Theater Tests", () => {
expect(badges[2].textContent).toBe("info");
});
it("renders severity label text ('breaking', 'material', 'low') next to badges", () => {
render(<ReplayDiffViewer divergences={mockReplayComparison.divergences} />);
expect(screen.getByTestId("severity-label-failure").textContent).toBe("breaking");
expect(screen.getByTestId("severity-label-warning").textContent).toBe("material");
expect(screen.getByTestId("severity-label-info").textContent).toBe("low");
});
it("renders nothing (null) with 0 divergences", () => {
const { container } = render(<ReplayDiffViewer divergences={[]} />);
expect(container.firstChild).toBeNull();
@ -298,6 +308,36 @@ describe("W-031 Replay Theater Tests", () => {
expect(pathEl.closest("a")).toBeNull();
expect(pathEl.textContent).toBe(mockArtifactDetail.path);
});
it("renders timestamp, digest, run id, lane, and explicit severity labels", () => {
const detailWithMeta: ArtifactDetail = {
...mockArtifactDetail,
created_at: "2026-05-26T12:00:00Z",
content: {
run_id: "run-999",
lane: "contemplation_quality",
},
};
render(
<ReplayMetadataTable
artifact={detailWithMeta}
comparison={mockReplayComparison}
/>
);
// Verify timestamp
expect(screen.getByTestId("artifact-created-at")).toHaveTextContent("2026");
// Verify run id & lane
expect(screen.getByTestId("artifact-run-id")).toHaveTextContent("run-999");
expect(screen.getByTestId("artifact-lane")).toHaveTextContent("contemplation_quality");
// Verify divergence counts with textual labels
expect(screen.getByText("Failure (breaking): 1")).toBeInTheDocument();
expect(screen.getByText("Warning (material): 1")).toBeInTheDocument();
expect(screen.getByText("Info (low): 1")).toBeInTheDocument();
});
});
describe("ReplayRoute", () => {
@ -393,6 +433,82 @@ describe("W-031 Replay Theater Tests", () => {
expect(await screen.findByText("What failed")).toBeInTheDocument();
expect(screen.getByText("disk read error")).toBeInTheDocument();
});
it("renders Selected artifact not found when selected ID returns null detail data", async () => {
const fetchMock = vi.fn().mockImplementation((url: string) => {
if (url.endsWith("/artifacts")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: { items: mockArtifacts } }),
});
}
if (url.endsWith("/artifacts/missing-id")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: null }),
});
}
if (url.endsWith("/replay/missing-id")) {
return Promise.resolve({
json: () => Promise.resolve({ ok: true, generated_at: "now", data: mockReplayComparison }),
});
}
return Promise.reject(new Error("Unknown route"));
});
vi.stubGlobal("fetch", fetchMock);
renderWithProviders(<ReplayRoute />, ["/replay?artifactId=missing-id"]);
expect(await screen.findByText("Selected artifact not found.")).toBeInTheDocument();
});
it("renders ErrorState in left pane when artifacts loading fails", async () => {
const fetchMock = vi.fn().mockImplementation((url: string) => {
if (url.endsWith("/artifacts")) {
return Promise.resolve({
json: () => Promise.resolve({
ok: false,
generated_at: "now",
error: { code: "read_error", message: "Failed to read artifacts index" },
}),
});
}
return Promise.reject(new Error("Unknown route"));
});
vi.stubGlobal("fetch", fetchMock);
renderWithProviders(<ReplayRoute />, ["/replay"]);
const errorElements = await screen.findAllByText("Failed to read artifacts index");
expect(errorElements.length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("No corpus mutation occurred.").length).toBeGreaterThanOrEqual(1);
});
it("renders Back to proposal link only when fromProposal query parameter is present", () => {
const { unmount } = renderWithProviders(
<ReplayComparisonPanel
artifact={mockArtifactDetail}
comparison={mockReplayComparison}
status={ReplayStatus.DIVERGED}
/>,
["/replay?artifactId=art-trace-1"]
);
expect(screen.queryByTestId("back-to-proposal")).not.toBeInTheDocument();
unmount();
renderWithProviders(
<ReplayComparisonPanel
artifact={mockArtifactDetail}
comparison={mockReplayComparison}
status={ReplayStatus.DIVERGED}
/>,
["/replay?artifactId=art-trace-1&fromProposal=proposal-777"]
);
const link = screen.getByTestId("back-to-proposal");
expect(link).toBeInTheDocument();
expect(link).toHaveTextContent("Back to proposal #proposal-777");
expect(link.getAttribute("href")).toBe("/proposals?proposal_id=proposal-777");
});
});
describe("Anti-motion & Animation Constraints", () => {