gstack/test/cso-preserved.test.ts

87 lines
3.2 KiB
TypeScript

/**
* 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');
});
});