0
+ $: passed = has_score && score_values.every((v) => v === 1.0)
const count_type_labels: Record = {
tool_calls: "Tool calls",
diff --git a/app/web_ui/src/lib/components/eval_types/step_count_check_result.test.ts b/app/web_ui/src/lib/components/eval_types/step_count_check_result.test.ts
index 8c2472bfb1..b092eb6557 100644
--- a/app/web_ui/src/lib/components/eval_types/step_count_check_result.test.ts
+++ b/app/web_ui/src/lib/components/eval_types/step_count_check_result.test.ts
@@ -26,17 +26,17 @@ describe("StepCountCheckResult", () => {
expect(container).toBeTruthy()
})
- it("shows Pass badge when match score is 1.0", () => {
+ it("shows Pass badge when the score is 1.0", () => {
const { container } = render(StepCountCheckResult, {
- props: { scores: { match: 1.0 }, eval_config: makeConfig() },
+ props: { scores: { within_bounds: 1.0 }, eval_config: makeConfig() },
})
expect(container.textContent).toContain("Pass")
expect(container.querySelector(".badge-success")).toBeTruthy()
})
- it("shows Fail badge when match score is 0.0", () => {
+ it("shows Fail badge when the score is 0.0", () => {
const { container } = render(StepCountCheckResult, {
- props: { scores: { match: 0.0 }, eval_config: makeConfig() },
+ props: { scores: { within_bounds: 0.0 }, eval_config: makeConfig() },
})
expect(container.textContent).toContain("Fail")
expect(container.querySelector(".badge-error")).toBeTruthy()
@@ -56,7 +56,7 @@ describe("StepCountCheckResult", () => {
it("shows tool_calls count type label", () => {
const { container } = render(StepCountCheckResult, {
props: {
- scores: { match: 1.0 },
+ scores: { within_bounds: 1.0 },
eval_config: makeConfig({ count_type: "tool_calls" }),
},
})
@@ -66,7 +66,7 @@ describe("StepCountCheckResult", () => {
it("shows model_responses count type label", () => {
const { container } = render(StepCountCheckResult, {
props: {
- scores: { match: 1.0 },
+ scores: { within_bounds: 1.0 },
eval_config: makeConfig({ count_type: "model_responses" }),
},
})
@@ -76,7 +76,7 @@ describe("StepCountCheckResult", () => {
it("shows turns count type label", () => {
const { container } = render(StepCountCheckResult, {
props: {
- scores: { match: 1.0 },
+ scores: { within_bounds: 1.0 },
eval_config: makeConfig({ count_type: "turns" }),
},
})
@@ -86,7 +86,7 @@ describe("StepCountCheckResult", () => {
it("shows range when both min and max are set", () => {
const { container } = render(StepCountCheckResult, {
props: {
- scores: { match: 1.0 },
+ scores: { within_bounds: 1.0 },
eval_config: makeConfig({ min_count: 2, max_count: 10 }),
},
})
@@ -96,7 +96,7 @@ describe("StepCountCheckResult", () => {
it("shows 'at least N' when only min is set", () => {
const { container } = render(StepCountCheckResult, {
props: {
- scores: { match: 1.0 },
+ scores: { within_bounds: 1.0 },
eval_config: makeConfig({ min_count: 3, max_count: null }),
},
})
@@ -106,7 +106,7 @@ describe("StepCountCheckResult", () => {
it("shows 'at most N' when only max is set", () => {
const { container } = render(StepCountCheckResult, {
props: {
- scores: { match: 1.0 },
+ scores: { within_bounds: 1.0 },
eval_config: makeConfig({ min_count: null, max_count: 7 }),
},
})
@@ -116,7 +116,7 @@ describe("StepCountCheckResult", () => {
it("shows 'any' when neither min nor max is set", () => {
const { container } = render(StepCountCheckResult, {
props: {
- scores: { match: 1.0 },
+ scores: { within_bounds: 1.0 },
eval_config: makeConfig({ min_count: null, max_count: null }),
},
})
@@ -125,17 +125,17 @@ describe("StepCountCheckResult", () => {
it("shows scores via EvalResultScores", () => {
const { container } = render(StepCountCheckResult, {
- props: { scores: { match: 0.0 } },
+ props: { scores: { within_bounds: 0.0 } },
})
- expect(container.textContent).toContain("match:")
+ expect(container.textContent).toContain("within_bounds:")
expect(container.textContent).toContain("0.00")
})
it("does not show config details when eval_config is null", () => {
const { container } = render(StepCountCheckResult, {
- props: { scores: { match: 1.0 } },
+ props: { scores: { within_bounds: 1.0 } },
})
- expect(container.textContent).toContain("match:")
+ expect(container.textContent).toContain("within_bounds:")
expect(container.textContent).not.toContain("Counting:")
expect(container.textContent).not.toContain("Allowed range:")
})
diff --git a/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.reference_data.test.ts b/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.reference_data.test.ts
index cb4085d744..6b124de5c6 100644
--- a/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.reference_data.test.ts
+++ b/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.reference_data.test.ts
@@ -97,7 +97,7 @@ describe("EvalTestRunPane reference data visibility by eval type", () => {
cleanup()
})
- it("hides reference data field for pattern_match (none mode) in ready state", () => {
+ it("shows reference data field for pattern_match (optional mode) in ready state", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: {
@@ -110,7 +110,7 @@ describe("EvalTestRunPane reference data visibility by eval type", () => {
const refField = container.querySelector(
'[data-testid="reference-data-field"]',
)
- expect(refField).toBeNull()
+ expect(refField).not.toBeNull()
})
it("hides reference data field for tool_call_check (none mode) in ready state", () => {
@@ -194,7 +194,7 @@ describe("EvalTestRunPane reference data visibility by eval type", () => {
expect(refField).not.toBeNull()
})
- it("hides reference data field for pattern_match in results state", () => {
+ it("shows reference data field for pattern_match in results state", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: {
@@ -212,7 +212,7 @@ describe("EvalTestRunPane reference data visibility by eval type", () => {
const refField = container.querySelector(
'[data-testid="reference-data-field"]',
)
- expect(refField).toBeNull()
+ expect(refField).not.toBeNull()
})
it("shows reference data field for llm_judge in results state", () => {
diff --git a/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.svelte b/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.svelte
index e7213cd5b4..89894c8780 100644
--- a/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.svelte
+++ b/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.svelte
@@ -104,7 +104,7 @@
@@ -184,7 +184,7 @@
@@ -231,7 +231,7 @@
@@ -242,7 +242,7 @@
@@ -283,7 +283,7 @@
diff --git a/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.test.ts b/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.test.ts
index bc14405505..b16befaf11 100644
--- a/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.test.ts
+++ b/app/web_ui/src/lib/components/eval_types/test_run/eval_test_run_pane.test.ts
@@ -131,7 +131,7 @@ describe("EvalTestRunPane", () => {
expect(goToRunLink?.textContent?.trim()).toContain("Go to Run")
})
- it("does NOT show Save Without Testing button (D10)", () => {
+ it("does NOT show Save Without Testing button", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: { available_runs: [], runs_loading: false },
@@ -145,7 +145,7 @@ describe("EvalTestRunPane", () => {
})
describe("State 2: Ready (pick input)", () => {
- it("renders selected run card without quick-picks (D15)", () => {
+ it("renders selected run card without quick-picks", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: {
@@ -167,7 +167,7 @@ describe("EvalTestRunPane", () => {
expect(quickPicks.length).toBe(0)
})
- it("does NOT show Browse all dataset inputs link (D15)", () => {
+ it("does NOT show Browse all dataset inputs link", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: {
@@ -183,7 +183,7 @@ describe("EvalTestRunPane", () => {
expect(browseLink).toBeNull()
})
- it("shows Run button with btn-primary btn-outline style (D11)", () => {
+ it("shows Run button with btn-primary btn-outline style", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: {
@@ -202,7 +202,7 @@ describe("EvalTestRunPane", () => {
expect(runBtn?.classList.contains("btn-outline")).toBe(true)
})
- it("does NOT show results placeholder (D12)", () => {
+ it("does NOT show results placeholder", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: {
@@ -387,7 +387,7 @@ describe("EvalTestRunPane", () => {
).toContain("tone")
})
- it("selected card shows Change button that opens browse dialog (D15)", () => {
+ it("selected card shows Change button that opens browse dialog", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: {
@@ -646,7 +646,7 @@ describe("EvalTestRunPane", () => {
expect(container.textContent).toContain("Missing expected scores")
})
- it("shows Run again button with btn-primary btn-outline style (D11) and no Save button (D10)", () => {
+ it("shows Run again button with btn-primary btn-outline style and no Save button", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: {
@@ -772,7 +772,7 @@ describe("EvalTestRunPane", () => {
})
})
- describe("Test Run heading and subtitle (D13)", () => {
+ describe("Test Run heading and subtitle", () => {
it("renders Test Run heading", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
@@ -825,7 +825,7 @@ describe("TestRunInputCard", () => {
cleanup()
})
- it("renders selected variant with 'Selected Test Run' label in non-grey (D14)", () => {
+ it("renders selected variant with 'Selected Test Run' label in non-grey", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(TestRunInputCard as any, {
props: {
@@ -1914,7 +1914,7 @@ describe("Auto-select integration", () => {
expect(container.textContent).toContain("Select a run to get started")
})
- it("does not show quick-picks when only 1 run (D15)", () => {
+ it("does not show quick-picks when only 1 run", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(EvalTestRunPane as any, {
props: {
@@ -2137,6 +2137,18 @@ describe("ReferenceDataField callout per usage mode", () => {
expect(callout?.textContent).toContain(".get(")
})
+ it("renders optional callout pointing at the Output to Check expression", () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const { container } = render(ReferenceDataField as any, {
+ props: { reference_data: "", usage_mode: "optional" },
+ })
+ const callout = container.querySelector('[data-testid="ref-data-callout"]')
+ expect(callout).not.toBeNull()
+ expect(callout?.textContent).toContain("expected values (ground truth)")
+ expect(callout?.textContent).toContain("Output to Check")
+ expect(callout?.textContent).toContain("{{ reference_data.expected_type }}")
+ })
+
it("uses the shared CalloutCard component (blue style)", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { container } = render(ReferenceDataField as any, {
diff --git a/app/web_ui/src/lib/components/eval_types/test_run/reference_data_field.svelte b/app/web_ui/src/lib/components/eval_types/test_run/reference_data_field.svelte
index 1e47cd4ccb..8b988f4745 100644
--- a/app/web_ui/src/lib/components/eval_types/test_run/reference_data_field.svelte
+++ b/app/web_ui/src/lib/components/eval_types/test_run/reference_data_field.svelte
@@ -252,6 +252,21 @@
>, then select the field to compare against.
+ {:else if usage_mode === "optional"}
+
+
+
+ Reference data is the expected values (ground truth) for this test
+ case. It's optional here, but if your Output to Check expression reads it via Jinja, provide it so the test can run:
+
+
+ {"{{ reference_data.expected_type }}"}
+
+
{:else if usage_mode === "code"}
diff --git a/app/web_ui/src/lib/components/eval_types/tool_call_check_form.svelte b/app/web_ui/src/lib/components/eval_types/tool_call_check_form.svelte
index ea0c138fcd..a9ffec4afc 100644
--- a/app/web_ui/src/lib/components/eval_types/tool_call_check_form.svelte
+++ b/app/web_ui/src/lib/components/eval_types/tool_call_check_form.svelte
@@ -136,6 +136,34 @@
arg_rows = synced
}
+ function is_valid_json(raw: string): boolean {
+ try {
+ JSON.parse(raw)
+ return true
+ } catch {
+ return false
+ }
+ }
+
+ // Authoring-time check for one argument row. A value with no name, or a
+ // non-empty value that isn't valid JSON, is an error the author must fix --
+ // rather than being silently dropped or coerced to a raw string at save time.
+ function validate_arg_row(row: ArgRow): {
+ name: string | null
+ value: string | null
+ } {
+ const has_name = row.name.trim().length > 0
+ const has_value = row.value.trim().length > 0
+ return {
+ name: has_value && !has_name ? "Add a name, or clear the value." : null,
+ value:
+ has_value && !is_valid_json(row.value) ? "Must be valid JSON." : null,
+ }
+ }
+
+ // Inline errors, one entry per row, mirroring the arg_rows shape.
+ $: arg_errors = arg_rows.map((rows) => rows.map(validate_arg_row))
+
function sync_args_to_properties() {
for (let i = 0; i < properties.expected_tools.length; i++) {
const rows = arg_rows[i]
@@ -145,15 +173,13 @@
}
const args: Record = {}
for (const row of rows) {
- if (!row.name.trim()) continue
- let parsed: unknown
- try {
- parsed = JSON.parse(row.value)
- } catch {
- parsed = row.value
- }
- args[row.name.trim()] = {
- value: parsed as ArgMatch["value"],
+ const name = row.name.trim()
+ if (!name) continue
+ // validate() gates save/test, so a non-empty value is guaranteed valid
+ // JSON here; an empty value means "no value constraint" (stored as "").
+ const raw = row.value.trim()
+ args[name] = {
+ value: (raw ? JSON.parse(raw) : "") as ArgMatch["value"],
match_mode: row.match_mode as ArgMatch["match_mode"],
}
}
@@ -175,6 +201,16 @@
if (!properties.expected_tools[i].tool_name.trim()) {
return `Expected Tool #${i + 1} is missing a name.`
}
+ const rows = arg_rows[i] ?? []
+ for (let j = 0; j < rows.length; j++) {
+ const err = validate_arg_row(rows[j])
+ if (err.name) {
+ return `Expected Tool #${i + 1}, argument #${j + 1}: ${err.name}`
+ }
+ if (err.value) {
+ return `Expected Tool #${i + 1}, argument #${j + 1}: ${err.value}`
+ }
+ }
}
return null
}
@@ -250,6 +286,7 @@
inputType="input"
placeholder="e.g. query"
bind:value={arg_row.name}
+ error_message={arg_errors[item_index]?.[arg_index]?.name}
/>
diff --git a/app/web_ui/src/lib/eval/default_judge.ts b/app/web_ui/src/lib/eval/default_judge.ts
new file mode 100644
index 0000000000..01ad680902
--- /dev/null
+++ b/app/web_ui/src/lib/eval/default_judge.ts
@@ -0,0 +1,38 @@
+// The judge-config shapes shared across the eval builder. The judge MODEL
+// is always chosen by the user (the builder's Drive Settings pickers,
+// pre-populated from the task's last saved eval or the registry's
+// suggested-for-evals models) — nothing here hardcodes a model or provider,
+// so the builder carries no dependency on any particular provider being
+// connected.
+
+import type { components } from "$lib/api_schema"
+
+// The ONE judge shape across the builder: the review step runs this judge
+// and the save path persists it, so the calibrated judge is the shipped one.
+export type JudgeConfig = components["schemas"]["JudgeConfig"]
+
+// The provider registry's enum — a lane's provider is always one of these
+// (the picks come from the models registry), and JudgeConfig now validates
+// it, so ModelChoice carries the same enum end-to-end rather than a bare
+// string.
+type ModelProviderName = components["schemas"]["ModelProviderName"]
+
+// A bare model choice for one of the builder's lanes (synthetic-user driver
+// or judge), as the wire carries it.
+export type ModelChoice = {
+ model_name: string
+ model_provider: ModelProviderName
+}
+
+// Construct a lane choice from registry-sourced ids (a dropdown pick, a
+// suggested model, or a persisted eval config). The provider always
+// originates from the models registry — a real ModelProviderName — so this is
+// the single honest wire→domain boundary where the loose string is asserted,
+// keeping every downstream lane (and the JudgeConfig built from it) enum-typed
+// without scattering casts at each construction.
+export function model_choice(
+ model_name: string,
+ model_provider: string,
+): ModelChoice {
+ return { model_name, model_provider: model_provider as ModelProviderName }
+}
diff --git a/app/web_ui/src/lib/git_sync/git_sync_status.svelte b/app/web_ui/src/lib/git_sync/git_sync_status.svelte
index 4f782750fd..ccb79d77ed 100644
--- a/app/web_ui/src/lib/git_sync/git_sync_status.svelte
+++ b/app/web_ui/src/lib/git_sync/git_sync_status.svelte
@@ -209,10 +209,12 @@
{#if show_auth_form}