diff --git a/test/cso-preserved.test.ts b/test/cso-preserved.test.ts new file mode 100644 index 000000000..83fe6bbc8 --- /dev/null +++ b/test/cso-preserved.test.ts @@ -0,0 +1,86 @@ +/** + * cso security-guidance preservation test (v1.45.0.0 T6). + * + * The cso skill carries load-bearing security prose: OWASP Top 10 mappings, + * STRIDE threat-model phrasing, "do not auto-fix without user approval" + * gates. Codex 2nd-pass critique #9: "cso exemption too broad ... should + * still get resolver dedup, catalog trim, sectioning if safe, and targeted + * evals around must-not-miss checks." + * + * This test pins the must-not-miss checks. cso gets the same resolver gate + * (T2), jargon dedup (T3), and catalog trim (T4) as every other skill — but + * its security-guidance body content stays intact. Future compression work + * that would strip this content fails CI here. + */ + +import { describe, test, expect } from 'bun:test'; +import * as fs from 'fs'; +import * as path from 'path'; + +const REPO_ROOT = path.resolve(import.meta.dir, '..'); +const CSO_SKILL = path.join(REPO_ROOT, 'cso', 'SKILL.md'); + +const MUST_PRESERVE_PHRASES = [ + // OWASP / STRIDE positioning + 'OWASP', + 'STRIDE', + // Mode discipline + 'daily', + 'comprehensive', + // Severity language + 'confidence', + // Active verification requirement (codex critique: "active verification") + 'verif', // covers "verify", "verification", "verified" +]; + +const MUST_PRESERVE_HEADINGS = [ + '## Preamble', // from PREAMBLE resolver +]; + +describe('cso skill preserves load-bearing security guidance', () => { + test('cso/SKILL.md exists and is non-trivial', () => { + expect(fs.existsSync(CSO_SKILL)).toBe(true); + const content = fs.readFileSync(CSO_SKILL, 'utf-8'); + // cso is a content-heavy security skill; under 30 KB suggests stripping went too far. + expect(content.length).toBeGreaterThan(30_000); + }); + + test('cso preserves required security phrases (case-insensitive)', () => { + const content = fs.readFileSync(CSO_SKILL, 'utf-8').toLowerCase(); + const missing: string[] = []; + for (const phrase of MUST_PRESERVE_PHRASES) { + if (!content.includes(phrase.toLowerCase())) missing.push(phrase); + } + if (missing.length > 0) { + throw new Error( + `cso/SKILL.md is missing required security phrases: ${missing.join(', ')}. ` + + `These are load-bearing for the skill's audit posture. If you intentionally ` + + `removed them, update this test with the new phrasing.`, + ); + } + }); + + test('cso preserves required headings', () => { + const content = fs.readFileSync(CSO_SKILL, 'utf-8'); + for (const heading of MUST_PRESERVE_HEADINGS) { + expect(content).toContain(heading); + } + }); + + test('cso catalog trim landed (frontmatter description ≤ 200 chars)', () => { + const content = fs.readFileSync(CSO_SKILL, 'utf-8'); + const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); + expect(fmMatch).not.toBeNull(); + const fm = fmMatch![1]; + const descMatch = fm.match(/^description:\s+(.+)$/m); + expect(descMatch).not.toBeNull(); + const desc = descMatch![1].trim(); + expect(desc.length).toBeLessThanOrEqual(200); + expect(desc).toContain('(gstack)'); + }); + + test('cso routing prose moved to "## When to invoke" body section', () => { + const content = fs.readFileSync(CSO_SKILL, 'utf-8'); + expect(content).toContain('## When to invoke this skill'); + }); +});