feat(validators): add fable to BASE_ALLOWED_MODELS with accept/reject coverage
This commit is contained in:
parent
937482067d
commit
357e17b176
2 changed files with 74 additions and 2 deletions
|
|
@ -21,7 +21,7 @@
|
||||||
// PROFILE_READ_ERROR — file unreadable or parse-error
|
// PROFILE_READ_ERROR — file unreadable or parse-error
|
||||||
// PROFILE_NOT_FOUND — file does not exist
|
// PROFILE_NOT_FOUND — file does not exist
|
||||||
//
|
//
|
||||||
// Allowed model values: ['sonnet', 'opus']. Haiku is allowed only when
|
// Allowed model values: ['sonnet', 'opus', 'fable']. Haiku is allowed only when
|
||||||
// VOYAGE_ALLOW_HAIKU=1 (per global CLAUDE.md modellvalg-prinsipp: Haiku skal
|
// VOYAGE_ALLOW_HAIKU=1 (per global CLAUDE.md modellvalg-prinsipp: Haiku skal
|
||||||
// ikke brukes som default; eksplisitt opt-in for spesielle bruksmønstre).
|
// ikke brukes som default; eksplisitt opt-in for spesielle bruksmønstre).
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ export const PROFILE_REQUIRED_PHASES = Object.freeze([
|
||||||
'brief', 'research', 'plan', 'execute', 'review', 'continue',
|
'brief', 'research', 'plan', 'execute', 'review', 'continue',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const BASE_ALLOWED_MODELS = Object.freeze(['sonnet', 'opus']);
|
export const BASE_ALLOWED_MODELS = Object.freeze(['sonnet', 'opus', 'fable']);
|
||||||
|
|
||||||
function getAllowedModels(env = process.env) {
|
function getAllowedModels(env = process.env) {
|
||||||
if (env.VOYAGE_ALLOW_HAIKU === '1') {
|
if (env.VOYAGE_ALLOW_HAIKU === '1') {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
validateProfileContent,
|
validateProfileContent,
|
||||||
PROFILE_REQUIRED_FIELDS,
|
PROFILE_REQUIRED_FIELDS,
|
||||||
PROFILE_REQUIRED_PHASES,
|
PROFILE_REQUIRED_PHASES,
|
||||||
|
BASE_ALLOWED_MODELS,
|
||||||
} from '../../lib/validators/profile-validator.mjs';
|
} from '../../lib/validators/profile-validator.mjs';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
@ -93,6 +94,77 @@ brief_reviewer_iter_cap: 1
|
||||||
`expected valid with VOYAGE_ALLOW_HAIKU=1, got: ${JSON.stringify(allowed.errors)}`);
|
`expected valid with VOYAGE_ALLOW_HAIKU=1, got: ${JSON.stringify(allowed.errors)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fable tier (v5.9): fable accepted under DEFAULT env (no opt-in flag),
|
||||||
|
// unknown models still rejected — the allowlist gate must demonstrably fire.
|
||||||
|
|
||||||
|
test('fable accepted in phase_models under default env (no env flag)', () => {
|
||||||
|
const fableProfile = `---
|
||||||
|
profile_version: "1.0"
|
||||||
|
name: fable-inline
|
||||||
|
phase_models:
|
||||||
|
- phase: brief
|
||||||
|
model: fable
|
||||||
|
- phase: research
|
||||||
|
model: fable
|
||||||
|
- phase: plan
|
||||||
|
model: fable
|
||||||
|
- phase: execute
|
||||||
|
model: fable
|
||||||
|
- phase: review
|
||||||
|
model: fable
|
||||||
|
- phase: continue
|
||||||
|
model: fable
|
||||||
|
parallel_agents_min: 6
|
||||||
|
parallel_agents_max: 8
|
||||||
|
external_research_enabled: true
|
||||||
|
brief_reviewer_iter_cap: 3
|
||||||
|
---
|
||||||
|
`;
|
||||||
|
const r = validateProfileContent(fableProfile, { env: { /* default: no flags */ } });
|
||||||
|
assert.equal(r.valid, true,
|
||||||
|
`expected fable accepted under default env, got: ${JSON.stringify(r.errors)}`);
|
||||||
|
assert.equal(r.errors.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown model gpt5 rejected with PROFILE_INVALID_MODEL under default env', () => {
|
||||||
|
const gpt5Profile = `---
|
||||||
|
profile_version: "1.0"
|
||||||
|
name: gpt5-inline
|
||||||
|
phase_models:
|
||||||
|
- phase: brief
|
||||||
|
model: gpt5
|
||||||
|
- phase: research
|
||||||
|
model: sonnet
|
||||||
|
- phase: plan
|
||||||
|
model: opus
|
||||||
|
- phase: execute
|
||||||
|
model: sonnet
|
||||||
|
- phase: review
|
||||||
|
model: opus
|
||||||
|
- phase: continue
|
||||||
|
model: sonnet
|
||||||
|
parallel_agents_min: 2
|
||||||
|
parallel_agents_max: 4
|
||||||
|
external_research_enabled: false
|
||||||
|
brief_reviewer_iter_cap: 1
|
||||||
|
---
|
||||||
|
`;
|
||||||
|
const r = validateProfileContent(gpt5Profile, { env: { /* default: no flags */ } });
|
||||||
|
assert.equal(r.valid, false);
|
||||||
|
const found = r.errors.find(e => e.code === 'PROFILE_INVALID_MODEL' && /gpt5/.test(e.message));
|
||||||
|
assert.ok(found, `expected PROFILE_INVALID_MODEL for gpt5, got: ${JSON.stringify(r.errors)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// BASE_ALLOWED_MODELS allowlist drift-pin (mirrors the PROFILE_REQUIRED_FIELDS pin)
|
||||||
|
|
||||||
|
test('BASE_ALLOWED_MODELS export drift-pin', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
[...BASE_ALLOWED_MODELS],
|
||||||
|
['sonnet', 'opus', 'fable'],
|
||||||
|
'BASE_ALLOWED_MODELS contract drift — pin contract',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Required fields presence
|
// Required fields presence
|
||||||
|
|
||||||
test('PROFILE_MISSING_FIELD when name absent', () => {
|
test('PROFILE_MISSING_FIELD when name absent', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue