Skip to content

Commit bcafc06

Browse files
committed
fix(eval): reject removed eval_cases aliases
1 parent c5f9d41 commit bcafc06

9 files changed

Lines changed: 123 additions & 1907 deletions

File tree

packages/core/src/evaluation/validation/eval-file.schema.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -903,8 +903,19 @@ export const EvalFileSchemaInput: z.ZodType = z.object({
903903
// Shared composable config graph fields
904904
graders: z.union([z.array(ConfigGraderSchema), z.string().min(1)]).optional(),
905905
defaults: z.union([ConfigDefaultsSchema, z.string().min(1)]).optional(),
906-
// Deprecated aliases
907-
eval_cases: TestsSchema.optional(),
906+
// Removed legacy aliases
907+
eval_cases: z
908+
.never({
909+
invalid_type_error:
910+
"Top-level 'eval_cases' has been removed from authored eval YAML. Use 'tests' instead.",
911+
})
912+
.optional(),
913+
evalcases: z
914+
.never({
915+
invalid_type_error:
916+
"Top-level 'evalcases' has been removed from authored eval YAML. Use 'tests' instead.",
917+
})
918+
.optional(),
908919
// Target
909920
target: z.union([z.string().min(1), EvalLocalTargetSchema]).optional(),
910921
targets: EvalTargetsSchema.optional(),
@@ -936,7 +947,6 @@ export const EvalFileSchemaInput: z.ZodType = z.object({
936947
});
937948

938949
export const EvalFileSchema: z.ZodType = EvalFileSchemaInput.refine(
939-
(value) =>
940-
value.tests !== undefined || value.eval_cases !== undefined || value.scenarios !== undefined,
950+
(value) => value.tests !== undefined || value.scenarios !== undefined,
941951
{ message: "Eval files must define 'tests' or 'scenarios'." },
942952
);

packages/core/src/evaluation/validation/eval-validator.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ const REMOVED_TOP_LEVEL_FIELDS = new Map<string, string>([
312312
'providers',
313313
"Top-level 'providers' is not a runtime alias in AgentV eval YAML. Use 'targets' for systems under test; provider names backend kind inside each target.",
314314
],
315+
[
316+
'eval_cases',
317+
"Top-level 'eval_cases' has been removed from authored eval YAML. Use 'tests' instead.",
318+
],
319+
[
320+
'evalcases',
321+
"Top-level 'evalcases' has been removed from authored eval YAML. Use 'tests' instead.",
322+
],
315323
['repeat', "Top-level 'repeat' has been removed. Use evaluate_options.repeat instead."],
316324
['runs', "Top-level 'runs' has been removed. Use evaluate_options.repeat.count instead."],
317325
[
@@ -331,8 +339,6 @@ const REMOVED_TOP_LEVEL_FIELDS = new Map<string, string>([
331339

332340
/** Deprecated top-level fields with migration hints. */
333341
const DEPRECATED_TOP_LEVEL_FIELDS = new Map<string, string>([
334-
['eval_cases', "'eval_cases' is deprecated. Use 'tests' instead."],
335-
['evalcases', "'evalcases' is deprecated. Use 'tests' instead."],
336342
['evaluator', "'evaluator' is deprecated. Use 'assert' instead."],
337343
]);
338344

packages/core/src/evaluation/validation/file-reference-validator.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,7 @@ export async function validateFileReferences(
5050
return errors;
5151
}
5252

53-
let cases: JsonValue | undefined = parsed.tests;
54-
if (cases === undefined && 'eval_cases' in parsed) {
55-
cases = parsed.eval_cases;
56-
}
57-
if (cases === undefined && 'evalcases' in parsed) {
58-
cases = parsed.evalcases;
59-
}
53+
const cases: JsonValue | undefined = parsed.tests;
6054
if (!Array.isArray(cases)) {
6155
return errors;
6256
}

packages/core/src/evaluation/yaml-parser.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,6 @@ function formatCircularImportChain(
192192

193193
type RawTestSuite = JsonObject & {
194194
readonly tests?: JsonValue;
195-
/** @deprecated Use `tests` instead */
196-
readonly eval_cases?: JsonValue;
197-
/** @deprecated Use `tests` instead */
198-
readonly evalcases?: JsonValue;
199195
readonly target?: JsonValue;
200196
readonly providers?: JsonValue;
201197
readonly model?: JsonValue;
@@ -271,16 +267,26 @@ type PromptExpansionResult = {
271267
readonly sourceTestIdById: ReadonlyMap<string, string>;
272268
};
273269

274-
function resolveTests(suite: RawTestSuite): JsonValue | undefined {
275-
if (suite.tests !== undefined) return suite.tests;
276-
if (suite.eval_cases !== undefined) {
277-
logWarning("'eval_cases' is deprecated. Use 'tests' instead.");
278-
return suite.eval_cases;
270+
function removedEvalCasesAliasMessage(alias: 'eval_cases' | 'evalcases'): string {
271+
return `Top-level '${alias}' has been removed from authored eval YAML. Use 'tests' instead.`;
272+
}
273+
274+
function rejectRemovedEvalCasesAliases(suite: RawTestSuite, evalFilePath: string): void {
275+
if ('eval_cases' in suite) {
276+
throw new Error(
277+
`Invalid eval file ${evalFilePath}: ${removedEvalCasesAliasMessage('eval_cases')}`,
278+
);
279279
}
280-
if (suite.evalcases !== undefined) {
281-
logWarning("'evalcases' is deprecated. Use 'tests' instead.");
282-
return suite.evalcases;
280+
if ('evalcases' in suite) {
281+
throw new Error(
282+
`Invalid eval file ${evalFilePath}: ${removedEvalCasesAliasMessage('evalcases')}`,
283+
);
283284
}
285+
}
286+
287+
function resolveTests(suite: RawTestSuite, evalFilePath: string): JsonValue | undefined {
288+
rejectRemovedEvalCasesAliases(suite, evalFilePath);
289+
if (suite.tests !== undefined) return suite.tests;
284290
return undefined;
285291
}
286292

@@ -1283,7 +1289,7 @@ async function loadTestsFromParsedYamlValue(
12831289
const suiteName =
12841290
suiteNameFromFile && suiteNameFromFile.length > 0 ? suiteNameFromFile : fallbackSuiteName;
12851291

1286-
const rawTestCases = resolveTests(suite);
1292+
const rawTestCases = resolveTests(suite, evalFilePath);
12871293
const suiteExperimentConfig = normalizeSuiteExperimentConfig(suite);
12881294
// Top-level `metadata:` is inherited by cases. Suite identity tags are parsed
12891295
// separately by parseMetadata() and are not case tags.
@@ -2275,7 +2281,7 @@ async function loadRawCasesForInclude(includePath: string): Promise<readonly Jso
22752281
if (!isJsonObject(raw)) {
22762282
throw new Error(`Imported eval suite must be a YAML object: ${includePath}`);
22772283
}
2278-
const tests = resolveTests(raw as RawTestSuite);
2284+
const tests = resolveTests(raw as RawTestSuite, includePath);
22792285
if (typeof tests === 'string') {
22802286
const externalPath = path.resolve(path.dirname(includePath), tests);
22812287
const pathStat = await stat(externalPath).catch(() => undefined);
@@ -2507,8 +2513,7 @@ function buildRawInlineTestSnapshots(rawParsed: unknown): Map<string, string> {
25072513
return snapshots;
25082514
}
25092515

2510-
const rawTests =
2511-
rawParsed.tests ?? rawParsed.eval_cases ?? (rawParsed as Record<string, unknown>).evalcases;
2516+
const rawTests = rawParsed.tests;
25122517
if (!Array.isArray(rawTests)) {
25132518
return snapshots;
25142519
}

packages/core/test/evaluation/loaders/jsonl-parser.test.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -906,8 +906,8 @@ describe('Backward-compat aliases', () => {
906906
await rm(tempDir, { recursive: true, force: true });
907907
});
908908

909-
describe('eval_cases → tests alias (YAML)', () => {
910-
it('supports eval_cases as deprecated alias for tests', async () => {
909+
describe('removed eval_cases/evalcases aliases (YAML)', () => {
910+
it('rejects eval_cases as a removed top-level alias for tests', async () => {
911911
const yamlPath = path.join(tempDir, 'eval-cases-alias.yaml');
912912
await writeFile(
913913
yamlPath,
@@ -923,14 +923,12 @@ prompts:
923923
`,
924924
);
925925

926-
const cases = await loadTests(yamlPath, tempDir);
927-
928-
expect(cases).toHaveLength(1);
929-
expect(cases[0].id).toBe('test-1');
930-
expect(cases[0].criteria).toBe('Goal');
926+
await expect(loadTests(yamlPath, tempDir)).rejects.toThrow(
927+
"Top-level 'eval_cases' has been removed from authored eval YAML. Use 'tests' instead.",
928+
);
931929
});
932930

933-
it('supports evalcases as deprecated alias for tests', async () => {
931+
it('rejects evalcases as a removed top-level alias for tests', async () => {
934932
const yamlPath = path.join(tempDir, 'evalcases-alias.yaml');
935933
await writeFile(
936934
yamlPath,
@@ -943,13 +941,12 @@ prompts:
943941
`,
944942
);
945943

946-
const cases = await loadTests(yamlPath, tempDir);
947-
948-
expect(cases).toHaveLength(1);
949-
expect(cases[0].id).toBe('test-1');
944+
await expect(loadTests(yamlPath, tempDir)).rejects.toThrow(
945+
"Top-level 'evalcases' has been removed from authored eval YAML. Use 'tests' instead.",
946+
);
950947
});
951948

952-
it('tests takes precedence over eval_cases', async () => {
949+
it('rejects eval_cases even when canonical tests is present', async () => {
953950
const yamlPath = path.join(tempDir, 'cases-precedence.yaml');
954951
await writeFile(
955952
yamlPath,
@@ -971,10 +968,9 @@ eval_cases:
971968
`,
972969
);
973970

974-
const cases = await loadTests(yamlPath, tempDir);
975-
976-
expect(cases).toHaveLength(1);
977-
expect(cases[0].id).toBe('canonical');
971+
await expect(loadTests(yamlPath, tempDir)).rejects.toThrow(
972+
"Top-level 'eval_cases' has been removed from authored eval YAML. Use 'tests' instead.",
973+
);
978974
});
979975
});
980976

packages/core/test/evaluation/validation/eval-file-schema.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,23 @@ describe('EvalFileSchema input shorthand', () => {
5858
expect(result.success).toBe(true);
5959
});
6060

61+
it('rejects removed eval_cases and evalcases aliases as test collections', () => {
62+
const result = EvalFileSchema.safeParse({
63+
eval_cases: [baseTest],
64+
evalcases: [baseTest],
65+
});
66+
67+
expect(result.success).toBe(false);
68+
if (result.success) throw new Error('Expected removed aliases to be rejected');
69+
const messages = collectIssueMessages(result.error.issues);
70+
expect(
71+
messages.some((message) => message.includes("Top-level 'eval_cases' has been removed")),
72+
).toBe(true);
73+
expect(
74+
messages.some((message) => message.includes("Top-level 'evalcases' has been removed")),
75+
).toBe(true);
76+
});
77+
6178
it('rejects eval-level execution.max_concurrency', () => {
6279
const result = EvalFileSchema.safeParse({
6380
execution: {

packages/core/test/evaluation/validation/eval-validator.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,47 @@ tests:
531531
).toBe(true);
532532
});
533533

534+
it('rejects removed eval_cases and evalcases aliases for tests', async () => {
535+
const filePath = path.join(tempDir, 'removed-eval-cases-aliases.yaml');
536+
await writeFile(
537+
filePath,
538+
`prompts:
539+
- "{{ prompt }}"
540+
tests:
541+
- vars:
542+
prompt: Canonical
543+
assert:
544+
- type: contains
545+
value: Canonical
546+
eval_cases:
547+
- vars:
548+
prompt: Legacy snake case
549+
evalcases:
550+
- vars:
551+
prompt: Legacy collapsed case
552+
`,
553+
);
554+
555+
const result = await validateEvalFile(filePath);
556+
557+
expect(result.valid).toBe(false);
558+
expect(result.errors).toContainEqual(
559+
expect.objectContaining({
560+
severity: 'error',
561+
location: 'eval_cases',
562+
message: expect.stringContaining("Top-level 'eval_cases' has been removed"),
563+
}),
564+
);
565+
expect(result.errors).toContainEqual(
566+
expect.objectContaining({
567+
severity: 'error',
568+
location: 'evalcases',
569+
message: expect.stringContaining("Top-level 'evalcases' has been removed"),
570+
}),
571+
);
572+
expect(result.errors.some((error) => error.severity === 'warning')).toBe(false);
573+
});
574+
534575
it('rejects removed top-level repeat controls with migration guidance', async () => {
535576
const filePath = path.join(tempDir, 'removed-repeat-fields.yaml');
536577
await writeFile(

skills-data/agentv-eval-migrations/references/breaking-changes.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,9 +1082,10 @@ rg -n "include:|tests:" path/to/evals
10821082

10831083
### Compatibility Notes
10841084

1085-
`eval_cases` remains a deprecated alias in the current schema, but migrated
1086-
YAML should use `tests`. The current convention is that runnable suites use
1087-
`*.eval.yaml`; reusable raw case files commonly use `*.cases.yaml` or JSONL.
1085+
`eval_cases` and `evalcases` have been removed from authored eval YAML. Migrate
1086+
them to `tests` before validating or running the suite. The current convention is
1087+
that runnable suites use `*.eval.yaml`; reusable raw case files commonly use
1088+
`*.cases.yaml` or JSONL.
10881089

10891090
## Result Artifact Path Changes Are Not Eval YAML Migrations
10901091

0 commit comments

Comments
 (0)