Skip to content

Commit 58fda3e

Browse files
authored
fix(core): allow templated use_target validation
Bead: av-njl
1 parent adcb6cf commit 58fda3e

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ function isObject(value: unknown): value is JsonObject {
1919
return typeof value === 'object' && value !== null && !Array.isArray(value);
2020
}
2121

22+
function isNonEmptyString(value: unknown): value is string {
23+
return typeof value === 'string' && value.trim().length > 0;
24+
}
25+
2226
// Cross-provider settings derived from the schema source of truth in targets.ts.
2327
// Adding a field to COMMON_TARGET_SETTINGS automatically makes it valid here.
2428
const COMMON_SETTINGS = new Set<string>(COMMON_TARGET_SETTINGS);
@@ -320,9 +324,11 @@ export async function validateTargetsFile(filePath: string): Promise<ValidationR
320324
const absolutePath = path.resolve(filePath);
321325

322326
let parsed: unknown;
327+
let rawParsed: unknown;
323328
try {
324329
const content = await readFile(absolutePath, 'utf8');
325-
parsed = interpolateEnv(parseYamlValue(content), process.env);
330+
rawParsed = parseYamlValue(content);
331+
parsed = interpolateEnv(rawParsed, process.env);
326332
} catch (error) {
327333
errors.push({
328334
severity: 'error',
@@ -484,6 +490,8 @@ export async function validateTargetsFile(filePath: string): Promise<ValidationR
484490

485491
// Validate targets array
486492
const targets = parsed.targets;
493+
const rawTargets =
494+
isObject(rawParsed) && Array.isArray(rawParsed.targets) ? rawParsed.targets : [];
487495
if (!Array.isArray(targets)) {
488496
errors.push({
489497
severity: 'error',
@@ -542,8 +550,9 @@ export async function validateTargetsFile(filePath: string): Promise<ValidationR
542550

543551
// Required field: provider
544552
const provider = target.provider;
545-
const hasUseTarget =
546-
typeof target.use_target === 'string' && target.use_target.trim().length > 0;
553+
const rawTarget = rawTargets[i];
554+
const rawUseTarget = isObject(rawTarget) ? rawTarget.use_target : undefined;
555+
const hasUseTarget = isNonEmptyString(target.use_target) || isNonEmptyString(rawUseTarget);
547556
const providerValue = typeof provider === 'string' ? provider.trim().toLowerCase() : undefined;
548557
const isTemplated = typeof provider === 'string' && /^\$\{\{.+\}\}$/.test(provider.trim());
549558
if (!hasUseTarget && (typeof provider !== 'string' || provider.trim().length === 0)) {

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,51 @@ describe('validateTargetsFile', () => {
122122
expect(result.valid).toBe(true);
123123
});
124124

125+
it('accepts env-templated use_target values without resolving the env during validation', async () => {
126+
const filePath = path.join(tempDir, 'templated-use-target.yaml');
127+
await writeFile(
128+
filePath,
129+
`targets:
130+
- name: default
131+
use_target: \${{ AGENT_TARGET }}
132+
- name: grader
133+
use_target: \${{ GRADER_TARGET }}
134+
- name: codex-agent
135+
provider: codex
136+
grader_target: grader
137+
`,
138+
);
139+
140+
const originalAgentTarget = process.env.AGENT_TARGET;
141+
const originalGraderTarget = process.env.GRADER_TARGET;
142+
Reflect.deleteProperty(process.env, 'AGENT_TARGET');
143+
Reflect.deleteProperty(process.env, 'GRADER_TARGET');
144+
145+
try {
146+
const result = await validateTargetsFile(filePath);
147+
148+
expect(result.valid).toBe(true);
149+
expect(
150+
result.errors.some(
151+
(error) =>
152+
error.severity === 'error' &&
153+
error.message.includes("Missing or invalid 'provider' field"),
154+
),
155+
).toBe(false);
156+
} finally {
157+
if (originalAgentTarget === undefined) {
158+
Reflect.deleteProperty(process.env, 'AGENT_TARGET');
159+
} else {
160+
process.env.AGENT_TARGET = originalAgentTarget;
161+
}
162+
if (originalGraderTarget === undefined) {
163+
Reflect.deleteProperty(process.env, 'GRADER_TARGET');
164+
} else {
165+
process.env.GRADER_TARGET = originalGraderTarget;
166+
}
167+
}
168+
});
169+
125170
it('rejects azure api_format with a migration error', async () => {
126171
const filePath = path.join(tempDir, 'azure-api-format.yaml');
127172
await writeFile(

0 commit comments

Comments
 (0)