Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { FluentProvider, webLightTheme } from '@fluentui/react-components'
import { render, screen, within } from '@testing-library/react'

import type { ScenarioObjectiveScorer, ScenarioObjectiveScorerMetrics } from '@/types'

import ObjectiveScorerDetails from './ObjectiveScorerDetails'

const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<FluentProvider theme={webLightTheme}>{children}</FluentProvider>
)

const PERCENT_METRIC_LABELS = ['Accuracy', 'Precision', 'Recall']

function renderScorer(metrics: ScenarioObjectiveScorerMetrics | null): void {
const scorer: ScenarioObjectiveScorer = {
component_name: 'SelfAskTrueFalseScorer', parameters: {}, children: {}, metrics,
}
render(<TestWrapper><ObjectiveScorerDetails scorer={scorer} /></TestWrapper>)
}

function expectMetric(label: string, value: string): void {
expect(within(screen.getByRole('group', { name: label })).getByText(value)).toBeInTheDocument()
}

describe('ObjectiveScorerDetails', () => {
beforeEach(() => { jest.clearAllMocks() })

it('should show accuracy, precision, and recall as percentages and keep F1 on its 0-to-1 scale', () => {
renderScorer({
accuracy: 0.94, f1_score: 0.91, precision: 0.95, recall: 0.88,
accuracy_standard_error: 0.01, average_score_time_seconds: 0.25,
})
expectMetric('Accuracy', '94.00%')
expectMetric('F1 score', '0.9100')
expectMetric('Precision', '95.00%')
expectMetric('Recall', '88.00%')
expectMetric('Accuracy std error', '0.0100')
expectMetric('Average score time', '0.25s')
})

it.each([
{ value: 0, percent: '0.00%', score: '0.0000' },
{ value: 1, percent: '100.00%', score: '1.0000' },
{ value: 0.95678, percent: '95.68%', score: '0.9568' },
])('should show $value as $percent, and an F1 of $value as $score', (
{ value, percent, score }: { value: number, percent: string, score: string },
) => {
renderScorer({ accuracy: value, f1_score: value, precision: value, recall: value })
for (const label of PERCENT_METRIC_LABELS) expectMetric(label, percent)
expectMetric('F1 score', score)
})

it('should keep the unavailable fallback for missing metrics', () => {
renderScorer({ accuracy: 0.94, f1_score: null })
expectMetric('Accuracy', '94.00%')
expectMetric('F1 score', 'Unavailable')
expectMetric('Precision', 'Unavailable')
expectMetric('Recall', 'Unavailable')
})

it('should explain when no official evaluation exists', () => {
renderScorer(null)
expect(screen.getByText('Official evaluation has not been run for this scorer configuration.')).toBeInTheDocument()
expect(screen.queryByRole('group', { name: 'Accuracy' })).not.toBeInTheDocument()
})
})
2 changes: 1 addition & 1 deletion frontend/src/components/Registry/ScorerRegistry.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('ScorerRegistry', () => {
expect(screen.queryByText('Message * true_false')).not.toBeInTheDocument()
const cells = within(screen.getAllByRole('row')[1]).getAllByRole('cell')
expect(cells.slice(2).map((cell: HTMLElement) => cell.textContent)).toEqual([
'Message', '95.7%', '0.0%', '94.0%', '91.0%', '0.25s',
'Message', '95.7%', '0.0%', '94.0%', '0.9100', '0.25s',
])
await user.hover(within(cells[3]).getByText('95.7%'))
expect(await screen.findByRole('tooltip')).toHaveTextContent('Recorded value: 0.95678')
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/utils/scorerPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ const METRIC_LABELS: Record<string, string> = {
average_score_time_seconds: 'Average score time',
}

const RATIO_METRICS = new Set(['precision', 'recall', 'accuracy', 'f1_score'])
// Shown as percentages. F1 stays on its 0-to-1 scale, like the other scores, and
// standard errors stay raw decimals (as percentages they would need a percentage-points label).
const PERCENT_METRICS = new Set(['precision', 'recall', 'accuracy'])
const METRIC_DESCRIPTIONS: Record<string, string> = {
precision: 'The share of positive predictions that are correct.',
recall: 'The share of actual positives detected by the scorer.',
Expand All @@ -57,7 +59,7 @@ export function formatScorerMetric(name: string, value: unknown, fallback = 'n/a
// Older evaluation records default an unmeasured duration to zero.
return value > 0 ? `${value.toFixed(2)}s` : fallback
}
if (name === 'accuracy') return `${(value * 100).toFixed(2)}%`
if (PERCENT_METRICS.has(name)) return `${(value * 100).toFixed(2)}%`
if (name.startsWith('num_')) return String(value)
return value.toFixed(4)
}
Expand All @@ -66,7 +68,7 @@ export function formatScorerMetric(name: string, value: unknown, fallback = 'n/a
export function formatScorerComparisonMetric(name: string, value: unknown): string {
const numeric = scorerComparisonValue(name, value)
if (numeric === undefined) return 'n/a'
return RATIO_METRICS.has(name) ? `${(numeric * 100).toFixed(1)}%` : formatScorerMetric(name, numeric)
return PERCENT_METRICS.has(name) ? `${(numeric * 100).toFixed(1)}%` : formatScorerMetric(name, numeric)
}

/** Keep display and sorting consistent about missing and unmeasured values. */
Expand Down
Loading