diff --git a/frontend/src/components/Chat/ObjectiveHeader.test.tsx b/frontend/src/components/Chat/ObjectiveHeader.test.tsx index 4b2609accc..37d77ad9e6 100644 --- a/frontend/src/components/Chat/ObjectiveHeader.test.tsx +++ b/frontend/src/components/Chat/ObjectiveHeader.test.tsx @@ -96,6 +96,27 @@ describe('ObjectiveHeader', () => { expect(screen.getByRole('group', { name: 'Automated score', exact: true })).toHaveTextContent(label) }) + it.each([ + { scoreType: 'float_scale', value: '0' }, + { scoreType: 'float_scale', value: '0.9' }, + { scoreType: 'unknown', value: 'unknown' }, + { scoreType: 'unknown', value: 'true' }, + ])('shows a $scoreType score of $value as its raw value, not a verdict', async ({ scoreType, value }) => { + const user = userEvent.setup() + const score: BackendScore = { + id: 'score', message_piece_id: 'response', scorer_type: 'Judge', score_type: scoreType, + score_value: value, status: 'complete', timestamp: '', + } + render() + await user.click(screen.getByRole('button', { name: /^objective achieved outcome:/i })) + const scoreRow = screen.getByRole('group', { name: 'Automated score', exact: true }) + expect(scoreRow).toHaveTextContent(value) + expect(scoreRow).not.toHaveTextContent(/Success|Failure|Undetermined/) + expect(screen.getByRole('button', { name: new RegExp(`^${value} automated score`) })).toBeInTheDocument() + // A score without a verdict must not preselect one for the human reviewer either. + expect(screen.getByRole('radio', { name: 'Success' })).toBeChecked() + }) + it('retains the API explanation when scorer selection fails', async () => { const user = userEvent.setup() const explanation = 'The selected scorer changed. Reload the registry before selecting it again.' diff --git a/frontend/src/components/Chat/ObjectiveHeader.tsx b/frontend/src/components/Chat/ObjectiveHeader.tsx index 126483ff18..04a7c1e97c 100644 --- a/frontend/src/components/Chat/ObjectiveHeader.tsx +++ b/frontend/src/components/Chat/ObjectiveHeader.tsx @@ -82,8 +82,12 @@ interface ObjectiveHeaderProps { applyStatus?: ApplyScorerStatus | null } +/** The only score type whose value answers "was the objective met?". */ +const BOOLEAN_SCORE_TYPE = 'true_false' + function scoreVerdict(score?: BackendScore | null): 'success' | 'failure' | 'undetermined' { - if (!score?.score_value) return 'undetermined' + // A float_scale or unknown value is a measurement, not a verdict, so it gets none. + if (!score?.score_value || score.score_type !== BOOLEAN_SCORE_TYPE) return 'undetermined' return score.score_value.toLowerCase() === 'true' ? 'success' : 'failure' } @@ -97,9 +101,12 @@ const RESULT_POPOVER_POSITIONING = { } as const function scoreLabel(score?: BackendScore | null): string { - const verdict = scoreVerdict(score) if (!score) return 'Not set' - return verdict === 'undetermined' ? 'Undetermined' : verdict === 'success' ? 'Success' : 'Failure' + if (!score.score_value) return 'Undetermined' + const verdict = scoreVerdict(score) + // Nonboolean scores carry no verdict: show what the scorer recorded, as the message score does. + if (verdict === 'undetermined') return score.score_value + return verdict === 'success' ? 'Success' : 'Failure' } export default function ObjectiveHeader({