// tests/validators/plan-validator-profile-drift.test.mjs // SC #20 — MANIFEST_PROFILE_DRIFT warning per brief Assumptions block 7. // In strict mode, plan-validator must emit a warning (NOT an error) when a // step manifest's profile_used differs from the plan's frontmatter profile. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import { validatePlanContent } from '../../lib/validators/plan-validator.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, '..', '..'); function loadFixture(name) { return readFileSync(resolve(ROOT, 'tests/fixtures', name), 'utf-8'); } const DRIFT_FIXTURE = loadFixture('plan-profile-drift.md'); test('drift detected in strict mode — emits MANIFEST_PROFILE_DRIFT warning, not error', () => { const r = validatePlanContent(DRIFT_FIXTURE, { strict: true }); assert.equal(r.valid, true, `plan must remain valid; errors: ${JSON.stringify(r.errors)}`); const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT'); assert.equal(drift.length, 1, `expected 1 drift warning, got ${drift.length}: ${JSON.stringify(drift)}`); assert.match(drift[0].message, /step 2/, `warning message must reference step 2: ${drift[0].message}`); assert.match(drift[0].message, /premium/, 'warning must include offending profile_used value'); assert.match(drift[0].message, /economy/, 'warning must include plan-level profile value'); }); test('drift NOT detected in soft mode — strict gate honored', () => { const r = validatePlanContent(DRIFT_FIXTURE, { strict: false }); const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT'); assert.equal( drift.length, 0, 'MANIFEST_PROFILE_DRIFT must only emit in strict mode (per brief assumption 7)', ); }); test('matching profile — no drift warning emitted', () => { // Same fixture body, but rewrite step 2 profile_used to match plan profile. // Use /g to catch both the doc-comment mention and the actual manifest entry. const matching = DRIFT_FIXTURE.replace(/profile_used: premium/g, 'profile_used: economy'); const r = validatePlanContent(matching, { strict: true }); assert.equal(r.valid, true); const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT'); assert.equal( drift.length, 0, `no drift expected when all step profile_used match plan profile; got ${JSON.stringify(drift)}`, ); }); test('plan without frontmatter profile — no drift warnings emitted', () => { // If plan-level profile is absent, step-level profile_used can be anything // without triggering drift (drift is only meaningful relative to a baseline). const noProfile = DRIFT_FIXTURE.replace(/profile: economy\n/, ''); const r = validatePlanContent(noProfile, { strict: true }); const drift = r.warnings.filter((w) => w.code === 'MANIFEST_PROFILE_DRIFT'); assert.equal( drift.length, 0, 'no plan-level profile means no baseline; drift detection must be silent', ); });