070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
130 lines
5.6 KiB
TypeScript
130 lines
5.6 KiB
TypeScript
/**
|
|
* Spawn-input glue coverage for the rollout resolver (Phase 15 wireup).
|
|
*
|
|
* Pins the boundary-narrowing behavior of `normalizeCritiquePolicy`
|
|
* (skill-frontmatter input) and `narrowProjectCritiqueOverride`
|
|
* (project-metadata input) in isolation from the spawn handler. The
|
|
* resolver itself is covered by `critique-rollout.test.ts` against
|
|
* direct inputs; this file covers the glue that constructs those
|
|
* inputs from runtime state.
|
|
*
|
|
* Added for PerishCode P3 on PR #1338: the boolean guard on the
|
|
* metadata blob and the trim/lowercase/typo-reject on the skill
|
|
* frontmatter token were previously only exercised indirectly via
|
|
* the spawn-handler's branches, so a refactor that loosens either
|
|
* guard could silently let a string `'true'` or a misspelled
|
|
* `'OPT_IN'` accidentally activate the feature.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { normalizeCritiquePolicy } from '../src/skills.js';
|
|
import { narrowProjectCritiqueOverride } from '../src/critique/spawn-inputs.js';
|
|
|
|
describe('normalizeCritiquePolicy (skill frontmatter input)', () => {
|
|
it('passes through the three canonical tokens', () => {
|
|
expect(normalizeCritiquePolicy('required')).toBe('required');
|
|
expect(normalizeCritiquePolicy('opt-in')).toBe('opt-in');
|
|
expect(normalizeCritiquePolicy('opt-out')).toBe('opt-out');
|
|
});
|
|
|
|
it('lowercases mixed-case tokens (YAML allows REQUIRED / Required)', () => {
|
|
expect(normalizeCritiquePolicy('REQUIRED')).toBe('required');
|
|
expect(normalizeCritiquePolicy('Required')).toBe('required');
|
|
expect(normalizeCritiquePolicy('OPT-IN')).toBe('opt-in');
|
|
expect(normalizeCritiquePolicy('Opt-Out')).toBe('opt-out');
|
|
});
|
|
|
|
it('trims surrounding whitespace before matching', () => {
|
|
expect(normalizeCritiquePolicy(' required ')).toBe('required');
|
|
expect(normalizeCritiquePolicy('\trequired\n')).toBe('required');
|
|
});
|
|
|
|
it('rejects typos to null (cannot accidentally force panel on/off)', () => {
|
|
// Underscore instead of hyphen is the most likely YAML-author typo.
|
|
expect(normalizeCritiquePolicy('opt_in')).toBeNull();
|
|
expect(normalizeCritiquePolicy('opt_out')).toBeNull();
|
|
expect(normalizeCritiquePolicy('require')).toBeNull();
|
|
expect(normalizeCritiquePolicy('optional')).toBeNull();
|
|
expect(normalizeCritiquePolicy('off')).toBeNull();
|
|
expect(normalizeCritiquePolicy('on')).toBeNull();
|
|
});
|
|
|
|
it('falls through to null for non-string inputs', () => {
|
|
expect(normalizeCritiquePolicy(undefined)).toBeNull();
|
|
expect(normalizeCritiquePolicy(null)).toBeNull();
|
|
expect(normalizeCritiquePolicy(true)).toBeNull();
|
|
expect(normalizeCritiquePolicy(1)).toBeNull();
|
|
expect(normalizeCritiquePolicy({})).toBeNull();
|
|
expect(normalizeCritiquePolicy([])).toBeNull();
|
|
});
|
|
|
|
it('returns null on empty / whitespace-only strings', () => {
|
|
expect(normalizeCritiquePolicy('')).toBeNull();
|
|
expect(normalizeCritiquePolicy(' ')).toBeNull();
|
|
expect(normalizeCritiquePolicy('\t\n')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('narrowProjectCritiqueOverride (project metadata input)', () => {
|
|
it('returns the boolean when metadata.critiqueTheaterEnabled is a real boolean', () => {
|
|
expect(narrowProjectCritiqueOverride({ critiqueTheaterEnabled: true }))
|
|
.toBe(true);
|
|
expect(narrowProjectCritiqueOverride({ critiqueTheaterEnabled: false }))
|
|
.toBe(false);
|
|
});
|
|
|
|
it('returns null when the metadata object has no critiqueTheaterEnabled key', () => {
|
|
expect(narrowProjectCritiqueOverride({}))
|
|
.toBeNull();
|
|
expect(narrowProjectCritiqueOverride({ kind: 'design', templateId: 't' }))
|
|
.toBeNull();
|
|
});
|
|
|
|
it('rejects string "true" / "false" to null (cannot accidentally activate via stringified value)', () => {
|
|
// This is the load-bearing case: a metadata blob with a stringified
|
|
// toggle (from a malformed PATCH or a future serializer drift) must
|
|
// NOT activate the feature. The resolver falls through to env / phase.
|
|
expect(narrowProjectCritiqueOverride({ critiqueTheaterEnabled: 'true' }))
|
|
.toBeNull();
|
|
expect(narrowProjectCritiqueOverride({ critiqueTheaterEnabled: 'false' }))
|
|
.toBeNull();
|
|
});
|
|
|
|
it('rejects number 1 / 0 to null (cannot accidentally activate via numeric coercion)', () => {
|
|
expect(narrowProjectCritiqueOverride({ critiqueTheaterEnabled: 1 }))
|
|
.toBeNull();
|
|
expect(narrowProjectCritiqueOverride({ critiqueTheaterEnabled: 0 }))
|
|
.toBeNull();
|
|
});
|
|
|
|
it('rejects nested objects / arrays to null', () => {
|
|
expect(narrowProjectCritiqueOverride({ critiqueTheaterEnabled: { v: true } }))
|
|
.toBeNull();
|
|
expect(narrowProjectCritiqueOverride({ critiqueTheaterEnabled: [true] }))
|
|
.toBeNull();
|
|
});
|
|
|
|
it('returns null when the entire metadata blob is missing or malformed', () => {
|
|
expect(narrowProjectCritiqueOverride(null)).toBeNull();
|
|
expect(narrowProjectCritiqueOverride(undefined)).toBeNull();
|
|
expect(narrowProjectCritiqueOverride('not an object')).toBeNull();
|
|
expect(narrowProjectCritiqueOverride(42)).toBeNull();
|
|
expect(narrowProjectCritiqueOverride(true)).toBeNull();
|
|
});
|
|
|
|
it('preserves other metadata fields by ignoring them entirely', () => {
|
|
// Sanity check that the narrower does not throw on or care about
|
|
// other fields the metadata blob carries (kind, templateId, linkedDirs,
|
|
// importedFrom, baseDir, etc.). It is a pure read of one key.
|
|
const metadata = {
|
|
kind: 'design',
|
|
templateId: 'landing-page',
|
|
linkedDirs: ['/a', '/b'],
|
|
importedFrom: '/some/path',
|
|
critiqueTheaterEnabled: true,
|
|
};
|
|
expect(narrowProjectCritiqueOverride(metadata)).toBe(true);
|
|
});
|
|
});
|