diff --git a/apps/dashboard/src/components/EvalDetail.tsx b/apps/dashboard/src/components/EvalDetail.tsx index 8ce4d45da..9154c7563 100644 --- a/apps/dashboard/src/components/EvalDetail.tsx +++ b/apps/dashboard/src/components/EvalDetail.tsx @@ -66,6 +66,19 @@ function caseTrialPath(trial: EvalCaseTrial, index = 0): string { return trial.attempt_path ?? trial.run_path ?? `attempt-${trial.attempt ?? index + 1}`; } +function trialNumber(trial: EvalCaseTrial, index = 0): number { + if (typeof trial.sample_index === 'number') return trial.sample_index; + if (typeof trial.attempt === 'number') return trial.attempt + 1; + return index + 1; +} + +function trialDisplayLabel(trial: EvalCaseTrial, index = 0): string { + const label = `Attempt ${trialNumber(trial, index)}`; + return typeof trial.retry_index === 'number' && trial.retry_index > 0 + ? `${label} retry ${trial.retry_index}` + : label; +} + function caseTrialTokenTotal(trial: EvalCaseTrial): number | undefined { if (trial.total_tokens != null) return trial.total_tokens; const usage = trial.token_usage; @@ -564,10 +577,16 @@ function TrialActionRow({ onSelectTrial?: (trial: EvalCaseTrial, initialTab?: Tab) => void; }) { const label = caseTrialPath(trial, index); + const displayLabel = trialDisplayLabel(trial, index); return (
-
{label}
+
+ {displayLabel} + + {label} + +
{formatPercent(trial.score)} score {trial.verdict ?? 'unknown'} @@ -608,14 +627,20 @@ function RepeatAggregateChecksTab({ return (
+

+ Aggregate across {group.trialCount} attempts for this test case. +

- + + {group.executionErrorTrials > 0 ? ( + + ) : null}
@@ -806,6 +831,7 @@ function RepeatAggregateTranscriptTab({ {group.trials.map((trial, index) => { const runLabel = caseTrialPath(trial, index); + const displayLabel = trialDisplayLabel(trial, index); const transcriptPath = trial.transcript_path; const transcriptHref = transcriptPath ? artifactFileContentUrl({ @@ -823,7 +849,12 @@ function RepeatAggregateTranscriptTab({ className="grid gap-2 rounded-md border border-gray-800 bg-gray-950/50 p-3 text-sm md:grid-cols-[minmax(8rem,1fr)_auto] md:items-center" >
-
{runLabel}
+
+ {displayLabel} + + {runLabel} + +
{transcriptPath ?? 'No transcript artifact'}
diff --git a/apps/dashboard/src/components/ResultTable.test.tsx b/apps/dashboard/src/components/ResultTable.test.tsx new file mode 100644 index 000000000..fbfa15918 --- /dev/null +++ b/apps/dashboard/src/components/ResultTable.test.tsx @@ -0,0 +1,100 @@ +import { describe, expect, it } from 'bun:test'; +import { renderToStaticMarkup } from 'react-dom/server'; + +import { buildResultTableModel } from '~/lib/result-table'; +import type { EvalResult } from '~/lib/types'; + +import { ResultRowsTable, ResultTable } from './ResultTable'; + +function repeatResult(overrides: Partial = {}): EvalResult { + return { + testId: 'refund-policy-flaky', + target: 'codex', + score: 0.67, + executionStatus: 'quality_failure', + eval_path: 'evals/refund-policy.eval.yaml', + timestamp: '2026-07-04T10:00:00.000Z', + scores: [{ name: 'rubric', type: 'llm-rubric', score: 0.67, verdict: 'fail' }], + attempts: [ + { + attempt: 0, + sample_index: 1, + attempt_path: 'sample-1', + score: 1, + verdict: 'pass', + duration_ms: 4400, + total_tokens: 830, + cost_usd: 0.0021, + }, + { + attempt: 1, + sample_index: 2, + attempt_path: 'sample-2', + score: 0.51, + verdict: 'fail', + duration_ms: 5200, + total_tokens: 910, + cost_usd: 0.0024, + }, + { + attempt: 2, + sample_index: 3, + attempt_path: 'sample-3', + score: 0, + verdict: 'fail', + execution_status: 'execution_error', + error: 'target timed out', + duration_ms: 15000, + }, + ], + ...overrides, + }; +} + +describe('ResultTable repeat-run rendering', () => { + it('renders repeat runs as a collapsed aggregate case by default', () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain('Aggregate case'); + expect(html).toContain('Flaky'); + expect(html).toContain('1/3 attempts passed'); + expect(html).toContain('Attempt success'); + expect(html).toContain('Mean score'); + expect(html).toContain('Expand attempts for refund-policy-flaky'); + expect(html).not.toContain('Under refund-policy-flaky'); + expect(html).not.toContain('sample-1'); + }); + + it('renders expanded attempts as subordinate rows under the aggregate case', () => { + const model = buildResultTableModel({ + results: [repeatResult()], + passThreshold: 0.8, + }); + const row = model.filteredRows[0]; + const html = renderToStaticMarkup( + [group.row.key, group]))} + expandedRepeatRows={new Set([row.key])} + onToggleRepeatGroup={() => undefined} + onOpenDetail={() => undefined} + onOpenTrialDetail={() => undefined} + />, + ); + + expect(html).toContain('Aggregate case'); + expect(html).toContain('Collapse attempts for refund-policy-flaky'); + expect(html).toContain('Attempt 1'); + expect(html).toContain('Attempt 2'); + expect(html).toContain('Attempt 3'); + expect(html).toContain('Under refund-policy-flaky'); + expect(html).toContain('sample-1'); + expect(html).toContain('target timed out'); + }); +}); diff --git a/apps/dashboard/src/components/ResultTable.tsx b/apps/dashboard/src/components/ResultTable.tsx index 9c118a60b..96ac515c0 100644 --- a/apps/dashboard/src/components/ResultTable.tsx +++ b/apps/dashboard/src/components/ResultTable.tsx @@ -191,7 +191,7 @@ export function ResultTable({ const [selectedTrialPath, setSelectedTrialPath] = useState(null); const [selectedDetailFilePath, setSelectedDetailFilePath] = useState(null); const [selectedDetailTab, setSelectedDetailTab] = useState('checks'); - const [collapsedRepeatRows, setCollapsedRepeatRows] = useState>( + const [expandedRepeatRows, setExpandedRepeatRows] = useState>( () => new Set(), ); const model = useMemo( @@ -292,7 +292,7 @@ export function ResultTable({ } function toggleRepeatGroup(rowKey: string) { - setCollapsedRepeatRows((current) => { + setExpandedRepeatRows((current) => { const next = new Set(current); if (next.has(rowKey)) next.delete(rowKey); else next.add(rowKey); @@ -439,7 +439,7 @@ export function ResultTable({ selectedRowKey={selectedRowKey} selectedTrialPath={selectedTrialPath} repeatGroupsByRowKey={repeatGroupsByRowKey} - collapsedRepeatRows={collapsedRepeatRows} + expandedRepeatRows={expandedRepeatRows} onToggleRepeatGroup={toggleRepeatGroup} onOpenDetail={openRowDetail} onOpenTrialDetail={openTrialDetail} @@ -468,14 +468,14 @@ export function ResultTable({ ); } -function ResultRowsTable({ +export function ResultRowsTable({ rows, visibleColumns, passThreshold, selectedRowKey, selectedTrialPath, repeatGroupsByRowKey, - collapsedRepeatRows, + expandedRepeatRows, onToggleRepeatGroup, onOpenDetail, onOpenTrialDetail, @@ -486,7 +486,7 @@ function ResultRowsTable({ selectedRowKey: string | null; selectedTrialPath: string | null; repeatGroupsByRowKey: ReadonlyMap; - collapsedRepeatRows: ReadonlySet; + expandedRepeatRows: ReadonlySet; onToggleRepeatGroup: (rowKey: string) => void; onOpenDetail: (rowKey: string) => void; onOpenTrialDetail: (rowKey: string, trial: EvalCaseTrial) => void; @@ -516,13 +516,13 @@ function ResultRowsTable({ {rows.map((row) => { const repeatGroup = repeatGroupsByRowKey.get(row.key); const isSelected = selectedRowKey === row.key && !selectedTrialPath; - const collapsed = repeatGroup ? collapsedRepeatRows.has(row.key) : true; + const expanded = repeatGroup ? expandedRepeatRows.has(row.key) : false; return ( onOpenDetail(row.key)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { @@ -542,7 +542,7 @@ function ResultRowsTable({ column={column} row={row} repeatGroup={repeatGroup} - repeatCollapsed={collapsed} + repeatExpanded={expanded} passThreshold={passThreshold} isSelected={isSelected} onToggleRepeatGroup={onToggleRepeatGroup} @@ -550,7 +550,7 @@ function ResultRowsTable({ ))} - {repeatGroup && !collapsed + {repeatGroup && expanded ? repeatGroup.trials.map((trial, index) => { const trialPath = caseTrialPath(trial, index); const trialSelected = @@ -558,9 +558,10 @@ function ResultRowsTable({ return ( onOpenTrialDetail(row.key, trial)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { @@ -604,6 +605,19 @@ function caseTrialPassed(trial: EvalCaseTrial, passThreshold: number): boolean { return typeof trial.score === 'number' ? trial.score >= passThreshold : false; } +function trialNumber(trial: EvalCaseTrial, index = 0): number { + if (typeof trial.sample_index === 'number') return trial.sample_index; + if (typeof trial.attempt === 'number') return trial.attempt + 1; + return index + 1; +} + +function trialDisplayLabel(trial: EvalCaseTrial, index = 0): string { + const label = `Attempt ${trialNumber(trial, index)}`; + return typeof trial.retry_index === 'number' && trial.retry_index > 0 + ? `${label} retry ${trial.retry_index}` + : label; +} + function primaryTrialArtifactPath(trial: EvalCaseTrial): string | null { return ( trial.grading_path ?? @@ -652,15 +666,23 @@ function TrialResultCell({ const isExecutionError = trial.execution_status === 'execution_error'; const status = isExecutionError ? 'error' : passed ? 'passing' : 'failing'; const statusLabel = isExecutionError ? 'Error' : passed ? 'Passing' : 'Failing'; - const label = caseTrialPath(trial, index); + const label = trialDisplayLabel(trial, index); + const artifactLabel = caseTrialPath(trial, index); switch (column.id) { case 'status': return ; case 'expander': - return