diff --git a/plugins/voyage/tests/synthetic/plan-determinism.test.mjs b/plugins/voyage/tests/synthetic/plan-determinism.test.mjs index 009d579..15938e5 100644 --- a/plugins/voyage/tests/synthetic/plan-determinism.test.mjs +++ b/plugins/voyage/tests/synthetic/plan-determinism.test.mjs @@ -61,3 +61,65 @@ test('plan determinism — no duplicate step titles within run', () => { ); } }); + +// --- v4.1 forward-compat block (SC #10) --- +// +// Adding the optional frontmatter key `profile_used` (Step 3 OPTIONAL_STRING_KEYS) +// must not break parsing of EITHER: +// - Existing plans WITHOUT profile_used (plan-run-A.md, plan-run-B.md) +// - New plans WITH profile_used (profile-plan-run-{economy,premium}-*.md) +// +// This is the forward-compat assertion required by Step 19. Extend-in-place +// keeps the determinism + forward-compat checks colocated. + +test('plan determinism — forward-compat: legacy fixtures (no profile_used) parse cleanly', () => { + for (const rel of ['tests/synthetic/plan-run-A.md', 'tests/synthetic/plan-run-B.md']) { + const text = readFileSync(join(ROOT, rel), 'utf-8'); + const doc = parseDocument(text); + assert.ok(doc.valid, `${rel}: frontmatter parse failed: ${(doc.errors || []).map((e) => e.message).join(', ')}`); + assert.equal( + doc.parsed.frontmatter.profile_used, + undefined, + `${rel}: legacy fixture must NOT have profile_used set`, + ); + assert.ok( + Array.isArray(doc.parsed.frontmatter.steps), + `${rel}: steps array still loads after parser extension`, + ); + } +}); + +test('plan determinism — forward-compat: new fixtures with profile_used parse cleanly', () => { + const cases = [ + { rel: 'tests/synthetic/profile-plan-run-economy-1.md', profile: 'economy' }, + { rel: 'tests/synthetic/profile-plan-run-economy-2.md', profile: 'economy' }, + { rel: 'tests/synthetic/profile-plan-run-premium-1.md', profile: 'premium' }, + { rel: 'tests/synthetic/profile-plan-run-premium-2.md', profile: 'premium' }, + ]; + for (const { rel, profile } of cases) { + const text = readFileSync(join(ROOT, rel), 'utf-8'); + const doc = parseDocument(text); + assert.ok(doc.valid, `${rel}: frontmatter parse failed: ${(doc.errors || []).map((e) => e.message).join(', ')}`); + assert.equal( + doc.parsed.frontmatter.profile_used, + profile, + `${rel}: profile_used must be ${profile}`, + ); + assert.ok( + Array.isArray(doc.parsed.frontmatter.steps) && doc.parsed.frontmatter.steps.length >= 10, + `${rel}: steps array must be non-empty`, + ); + } +}); + +test('plan determinism — forward-compat: real v4.1 plan validates with --strict (no PLAN_VERSION_MISMATCH)', async () => { + // Sanity check that adding profile_used to manifest-yaml schema doesn't + // regress full plan-validator strict-mode behaviour on a real plan that + // ships profile_used in step manifests. + const realPlan = '.claude/projects/2026-05-08-voyage-v4.1-modellprofiler/plan.md'; + const { validatePlan } = await import('../../lib/validators/plan-validator.mjs'); + const result = await validatePlan(join(ROOT, realPlan), { strict: true }); + assert.equal(result.valid, true, `real plan must validate strict: ${JSON.stringify(result.errors)}`); + const versionMismatch = (result.warnings || []).find((w) => w.code === 'PLAN_VERSION_MISMATCH'); + assert.equal(versionMismatch, undefined, 'real plan must NOT emit PLAN_VERSION_MISMATCH warning'); +});