Replay Evidence
diff --git a/workbench-ui/src/app/replay/ReplayDiffViewer.tsx b/workbench-ui/src/app/replay/ReplayDiffViewer.tsx
index df0e2927..2ad4f32c 100644
--- a/workbench-ui/src/app/replay/ReplayDiffViewer.tsx
+++ b/workbench-ui/src/app/replay/ReplayDiffViewer.tsx
@@ -54,6 +54,12 @@ export function ReplayDiffViewer({ divergences }: ReplayDiffViewerProps) {
+
+ {div.severity === "failure" ? "breaking" : div.severity === "warning" ? "material" : "low"}
+
{div.path}
diff --git a/workbench-ui/src/app/replay/ReplayMetadataTable.tsx b/workbench-ui/src/app/replay/ReplayMetadataTable.tsx
index 4a3b0093..1d3b21ee 100644
--- a/workbench-ui/src/app/replay/ReplayMetadataTable.tsx
+++ b/workbench-ui/src/app/replay/ReplayMetadataTable.tsx
@@ -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 (
Metadata Audit Details
@@ -22,8 +47,8 @@ export function ReplayMetadataTable({ artifact, comparison }: ReplayMetadataTabl
- | Property |
- Value |
+ Property |
+ Value |
@@ -35,12 +60,32 @@ export function ReplayMetadataTable({ artifact, comparison }: ReplayMetadataTabl
Kind |
{artifact.kind} |
+
+ | Content Type |
+ {artifact.content_type} |
+
| Path |
{artifact.path}
|
+
+ | Created At |
+
+ {timestamp || None}
+ |
+
+
+ | Artifact Digest / Trace Hash |
+
+ {artifact.digest ? (
+
+ ) : (
+ None
+ )}
+ |
+
| Original Hash |
@@ -62,24 +107,32 @@ export function ReplayMetadataTable({ artifact, comparison }: ReplayMetadataTabl
|
- | Divergences |
-
-
-
- Failure: {divergenceCounts.failure}
-
-
- Warning: {divergenceCounts.warning}
-
-
- Info: {divergenceCounts.info}
-
-
+ | Run ID |
+
+ {runId || None}
|
- | Content Type |
- {artifact.content_type} |
+ Lane |
+
+ {lane || None}
+ |
+
+
+ | Divergences |
+
+
+
+ Failure (breaking): {divergenceCounts.failure}
+
+
+ Warning (material): {divergenceCounts.warning}
+
+
+ Info (low): {divergenceCounts.info}
+
+
+ |
diff --git a/workbench-ui/src/app/replay/ReplayRoute.tsx b/workbench-ui/src/app/replay/ReplayRoute.tsx
index 9144c805..44c31017 100644
--- a/workbench-ui/src/app/replay/ReplayRoute.tsx
+++ b/workbench-ui/src/app/replay/ReplayRoute.tsx
@@ -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 ? (
) : artifactsQuery.isError ? (
-
- Failed to load artifacts.
-
+
) : (
- ) : null}
+ ) : (
+
+ )}
);
diff --git a/workbench-ui/src/app/replay/replay.test.tsx b/workbench-ui/src/app/replay/replay.test.tsx
index 95ee316c..b00fe538 100644
--- a/workbench-ui/src/app/replay/replay.test.tsx
+++ b/workbench-ui/src/app/replay/replay.test.tsx
@@ -209,11 +209,13 @@ describe("W-031 Replay Theater Tests", () => {
};
render(
-
+
+
+
);
// 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(
);
+
+ 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(
);
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(
+
+ );
+
+ // 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(
, ["/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(
, ["/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(
+
,
+ ["/replay?artifactId=art-trace-1"]
+ );
+
+ expect(screen.queryByTestId("back-to-proposal")).not.toBeInTheDocument();
+ unmount();
+
+ renderWithProviders(
+
,
+ ["/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", () => {