Skip to content

Commit 5bf651c

Browse files
committed
feat(v3-4): RunResultModal surfaces extras with data-testid'd cells
Closes V3-4 / cppmega-mlx-xzp. Fixes B3: pipeline.run extras (losses, lr_trajectory, weight_delta_norm, optimizer_kind, model_summary) were returned by the backend but invisible in the UI — only error.detail ever rendered. Playwright tests could not assert against training math. Now: every stage with extras OR error gets an expand button. Expand reveals both. Extras render as a data-testid'd dl: run-result-extras-{stage}-{key} primitive run-result-extras-{stage}-{key}-{i} array item run-result-extras-{stage}-{key}-{subkey} nested object value run-result-extras-row-{stage} extras container row Required test surface for V3-5..V3-12 deep e2e assertions: run-result-extras-train-optimizer_kind run-result-extras-train-losses-{i} run-result-extras-train-lr_trajectory-{i} run-result-extras-train-model_summary-mlp_activation run-result-extras-train-model_summary-attention_pre_norm run-result-extras-train-weight_delta_norm run-result-extras-train-schedule_kind 7 new vitest in RunResultModal.test.tsx (was 8 → 14 total). Full vbgui regression 166/166 green.
1 parent f443aaf commit 5bf651c

2 files changed

Lines changed: 203 additions & 6 deletions

File tree

vbgui/src/components/RunResultModal.tsx

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Modal that displays per-stage results from a pipeline.run response.
22

3-
import { useState } from "react";
3+
import { Fragment, useState } from "react";
44

55
export interface StageResult {
66
name: string;
@@ -29,6 +29,95 @@ const COLORS = {
2929
ok: "#10b981", fail: "#dc2626", skipped: "#9ca3af",
3030
} as const;
3131

32+
// V3-4: keys excluded from the visible extras dl because they're
33+
// redundant with the row's status / error rendering.
34+
const EXTRAS_RESERVED = new Set<string>([
35+
"name", "status", "elapsed_ms", "warnings", "errors", "error",
36+
]);
37+
38+
function extrasOf(s: StageResult): Record<string, unknown> {
39+
const out: Record<string, unknown> = {};
40+
for (const [k, v] of Object.entries(s)) {
41+
if (!EXTRAS_RESERVED.has(k)) out[k] = v;
42+
}
43+
return out;
44+
}
45+
46+
function hasContent(s: StageResult): boolean {
47+
return s.error != null || Object.keys(extrasOf(s)).length > 0;
48+
}
49+
50+
function StageExtras({
51+
stage, extras,
52+
}: { stage: string; extras: Record<string, unknown> }): JSX.Element {
53+
return (
54+
<dl data-testid={`run-result-extras-${stage}`}
55+
style={{ margin: 0, fontSize: 11, fontFamily: "monospace" }}>
56+
{Object.entries(extras).map(([k, v]) => (
57+
<ExtrasEntry key={k} stage={stage} k={k} v={v} />
58+
))}
59+
</dl>
60+
);
61+
}
62+
63+
function ExtrasEntry({
64+
stage, k, v,
65+
}: { stage: string; k: string; v: unknown }): JSX.Element {
66+
const base = `run-result-extras-${stage}-${k}`;
67+
if (Array.isArray(v)) {
68+
return (
69+
<div style={{ display: "flex", gap: 8 }}>
70+
<dt style={{ color: "#6b7280", minWidth: 140 }}>{k}</dt>
71+
<dd style={{ margin: 0 }}>
72+
<ol data-testid={base}
73+
style={{ margin: 0, padding: "0 0 0 16px",
74+
display: "flex", gap: 6, flexWrap: "wrap",
75+
listStyle: "none" }}>
76+
{v.map((item, i) => (
77+
<li key={i} data-testid={`${base}-${i}`}>
78+
{typeof item === "object" && item !== null
79+
? JSON.stringify(item)
80+
: String(item)}
81+
</li>
82+
))}
83+
</ol>
84+
</dd>
85+
</div>
86+
);
87+
}
88+
if (v !== null && typeof v === "object") {
89+
return (
90+
<div style={{ display: "flex", gap: 8 }}>
91+
<dt style={{ color: "#6b7280", minWidth: 140 }}>{k}</dt>
92+
<dd style={{ margin: 0 }}>
93+
<dl data-testid={base}
94+
style={{ margin: 0, paddingLeft: 8 }}>
95+
{Object.entries(v as Record<string, unknown>).map(([sk, sv]) => (
96+
<div key={sk} style={{ display: "flex", gap: 6 }}>
97+
<dt style={{ color: "#9ca3af", minWidth: 120 }}>{sk}</dt>
98+
<dd data-testid={`${base}-${sk}`}
99+
style={{ margin: 0 }}>
100+
{typeof sv === "object" && sv !== null
101+
? JSON.stringify(sv)
102+
: String(sv)}
103+
</dd>
104+
</div>
105+
))}
106+
</dl>
107+
</dd>
108+
</div>
109+
);
110+
}
111+
return (
112+
<div style={{ display: "flex", gap: 8 }}>
113+
<dt style={{ color: "#6b7280", minWidth: 140 }}>{k}</dt>
114+
<dd data-testid={base} style={{ margin: 0 }}>
115+
{v === null || v === undefined ? "null" : String(v)}
116+
</dd>
117+
</div>
118+
);
119+
}
120+
32121
export function RunResultModal({
33122
report, error, onClose,
34123
}: RunResultModalProps): JSX.Element | null {
@@ -91,10 +180,11 @@ export function RunResultModal({
91180
<tbody>
92181
{report.stages.map((s) => {
93182
const open = expanded.has(s.name);
183+
const extras = extrasOf(s);
184+
const hasExtras = Object.keys(extras).length > 0;
94185
return (
95-
<>
96-
<tr key={s.name}
97-
data-testid={`run-result-stage-${s.name}`}
186+
<Fragment key={s.name}>
187+
<tr data-testid={`run-result-stage-${s.name}`}
98188
style={{ borderBottom: "1px solid #f3f4f6" }}>
99189
<td style={td}>
100190
<span style={{ color: COLORS[s.status],
@@ -110,7 +200,7 @@ export function RunResultModal({
110200
{s.elapsed_ms.toFixed(1)}
111201
</td>
112202
<td style={td}>
113-
{s.error && (
203+
{hasContent(s) && (
114204
<button data-testid={`run-result-expand-${s.name}`}
115205
onClick={() => toggle(expanded,
116206
setExpanded, s.name)}>
@@ -129,7 +219,15 @@ export function RunResultModal({
129219
</td>
130220
</tr>
131221
)}
132-
</>
222+
{open && hasExtras && (
223+
<tr data-testid={`run-result-extras-row-${s.name}`}>
224+
<td colSpan={5} style={{ ...td, background: "#f9fafb",
225+
padding: "6px 12px" }}>
226+
<StageExtras stage={s.name} extras={extras} />
227+
</td>
228+
</tr>
229+
)}
230+
</Fragment>
133231
);
134232
})}
135233
</tbody>

vbgui/tests/RunResultModal.test.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,103 @@ describe("RunResultModal", () => {
8282
expect(screen.getByTestId("run-result-error").textContent)
8383
.toContain("backend down");
8484
});
85+
86+
// ---------------- V3-4: extras surfaced under each stage row -----------
87+
88+
const REPORT_WITH_EXTRAS: RunReport = {
89+
overall_status: "ok",
90+
total_elapsed_ms: 500.0,
91+
stages: [
92+
{
93+
name: "train",
94+
status: "ok",
95+
elapsed_ms: 480.0,
96+
losses: [3.4, 3.1, 2.9],
97+
lr_trajectory: [0.001, 0.001, 0.001],
98+
weight_delta_norm: 0.0123,
99+
num_steps: 3,
100+
schedule_kind: "constant",
101+
optimizer_kind: "lion",
102+
model_summary: {
103+
mlp_activation: "swiglu",
104+
attention_pre_norm: "layernorm",
105+
attention_post_norm: "rmsnorm",
106+
mlp_pre_norm: "none",
107+
mlp_post_norm: "none",
108+
optimizer_kind: "lion",
109+
schedule_kind: "constant",
110+
num_brick_kinds: 2,
111+
},
112+
},
113+
],
114+
};
115+
116+
it("expand reveals extras when stage has only extras (no error)", () => {
117+
render(<RunResultModal report={REPORT_WITH_EXTRAS} onClose={() => {}} />);
118+
fireEvent.click(screen.getByTestId("run-result-expand-train"));
119+
expect(screen.getByTestId("run-result-extras-row-train")).toBeTruthy();
120+
expect(screen.getByTestId("run-result-extras-train")).toBeTruthy();
121+
});
122+
123+
it("primitives surface with run-result-extras-{stage}-{key} testids", () => {
124+
render(<RunResultModal report={REPORT_WITH_EXTRAS} onClose={() => {}} />);
125+
fireEvent.click(screen.getByTestId("run-result-expand-train"));
126+
expect(screen.getByTestId("run-result-extras-train-optimizer_kind")
127+
.textContent).toBe("lion");
128+
expect(screen.getByTestId("run-result-extras-train-schedule_kind")
129+
.textContent).toBe("constant");
130+
expect(screen.getByTestId("run-result-extras-train-weight_delta_norm")
131+
.textContent).toBe("0.0123");
132+
expect(screen.getByTestId("run-result-extras-train-num_steps")
133+
.textContent).toBe("3");
134+
});
135+
136+
it("arrays render with run-result-extras-{stage}-{key}-{i} per item", () => {
137+
render(<RunResultModal report={REPORT_WITH_EXTRAS} onClose={() => {}} />);
138+
fireEvent.click(screen.getByTestId("run-result-expand-train"));
139+
expect(screen.getByTestId("run-result-extras-train-losses-0")
140+
.textContent).toBe("3.4");
141+
expect(screen.getByTestId("run-result-extras-train-losses-2")
142+
.textContent).toBe("2.9");
143+
expect(screen.getByTestId("run-result-extras-train-lr_trajectory-1")
144+
.textContent).toBe("0.001");
145+
});
146+
147+
it("nested objects render with run-result-extras-{stage}-{key}-{sub}", () => {
148+
render(<RunResultModal report={REPORT_WITH_EXTRAS} onClose={() => {}} />);
149+
fireEvent.click(screen.getByTestId("run-result-expand-train"));
150+
expect(screen.getByTestId(
151+
"run-result-extras-train-model_summary-mlp_activation")
152+
.textContent).toBe("swiglu");
153+
expect(screen.getByTestId(
154+
"run-result-extras-train-model_summary-attention_pre_norm")
155+
.textContent).toBe("layernorm");
156+
expect(screen.getByTestId(
157+
"run-result-extras-train-model_summary-optimizer_kind")
158+
.textContent).toBe("lion");
159+
});
160+
161+
it("expand button hidden when stage has no content (no error+no extras)", () => {
162+
render(<RunResultModal report={REPORT_OK} onClose={() => {}} />);
163+
// REPORT_OK stages have no error and no extras → no expand button.
164+
expect(screen.queryByTestId("run-result-expand-parse")).toBeNull();
165+
expect(screen.queryByTestId("run-result-expand-build_model")).toBeNull();
166+
});
167+
168+
it("expand reveals BOTH error and extras when both present", () => {
169+
const mixed: RunReport = {
170+
overall_status: "fail", total_elapsed_ms: 1.0,
171+
stages: [{
172+
name: "train", status: "fail", elapsed_ms: 1.0,
173+
error: { type: "Boom", detail: "kaboom" },
174+
losses: [1, 2],
175+
}],
176+
};
177+
render(<RunResultModal report={mixed} onClose={() => {}} />);
178+
fireEvent.click(screen.getByTestId("run-result-expand-train"));
179+
expect(screen.getByTestId("run-result-detail-train").textContent)
180+
.toContain("Boom");
181+
expect(screen.getByTestId("run-result-extras-train-losses-0")
182+
.textContent).toBe("1");
183+
});
85184
});

0 commit comments

Comments
 (0)