gstack/test/terse-build.test.ts

152 lines
6.1 KiB
TypeScript

/**
* Unit tests for the terse-build flag (v1.46.0.0 T3).
*
* `--explain-level=terse` makes the gen-skill-docs pipeline drop 4 preamble
* sections at gen time. Default builds keep them. Without these tests, a
* refactor that breaks the explainLevel threading silently regresses one
* of the opt-in compression paths — the runtime EXPLAIN_LEVEL: terse runtime
* gate still works, so users wouldn't notice immediately.
*
* Pure-function tests against the resolvers — fast, free, no subprocess.
*/
import { describe, test, expect } from 'bun:test';
import type { TemplateContext } from '../scripts/resolvers/types';
import { generateWritingStyle } from '../scripts/resolvers/preamble/generate-writing-style';
import { generateCompletenessSection } from '../scripts/resolvers/preamble/generate-completeness-section';
import { generateConfusionProtocol } from '../scripts/resolvers/preamble/generate-confusion-protocol';
import { generateContextHealth } from '../scripts/resolvers/preamble/generate-context-health';
import { generatePreamble } from '../scripts/resolvers/preamble';
function makeCtx(explainLevel?: 'default' | 'terse', tier: number = 4): TemplateContext {
return {
skillName: 'test-skill',
tmplPath: '/tmp/test/SKILL.md.tmpl',
host: 'claude',
paths: {
skillRoot: '~/.claude/skills/gstack',
localSkillRoot: '.claude/skills',
binDir: '~/.claude/skills/gstack/bin',
browseDir: '~/.claude/skills/gstack/browse/dist',
designDir: '~/.claude/skills/gstack/design/dist',
makePdfDir: '~/.claude/skills/gstack/make-pdf/dist',
},
preambleTier: tier,
explainLevel,
};
}
describe('terse build — per-resolver behavior', () => {
describe('generateWritingStyle', () => {
test('default: emits full section with jargon-list pointer', () => {
const out = generateWritingStyle(makeCtx('default'));
expect(out).toContain('## Writing Style');
expect(out).toContain('jargon-list.json');
expect(out).toContain('Curated jargon list');
expect(out).toContain('outcome');
});
test('terse: emits one-line terse directive only', () => {
const out = generateWritingStyle(makeCtx('terse'));
expect(out).toContain('## Writing Style');
expect(out).toContain('Terse mode (build-time)');
// Negative: NONE of the default-mode prose
expect(out).not.toContain('jargon-list.json');
expect(out).not.toContain('Curated jargon list');
expect(out).not.toContain('Frame questions in outcome terms');
});
test('terse is meaningfully shorter than default', () => {
const fullLen = generateWritingStyle(makeCtx('default')).length;
const terseLen = generateWritingStyle(makeCtx('terse')).length;
expect(terseLen).toBeLessThan(fullLen / 3);
});
});
describe('generateCompletenessSection', () => {
test('default: emits full section with Boil-the-Lake prose', () => {
const out = generateCompletenessSection(makeCtx('default'));
expect(out).toContain('## Completeness Principle');
expect(out).toContain('Boil the Lake');
});
test('terse: returns empty string', () => {
expect(generateCompletenessSection(makeCtx('terse'))).toBe('');
});
test('no ctx arg: defaults to non-terse (back-compat with old callers)', () => {
const out = generateCompletenessSection();
expect(out).toContain('## Completeness Principle');
});
});
describe('generateConfusionProtocol', () => {
test('default: emits full section', () => {
const out = generateConfusionProtocol(makeCtx('default'));
expect(out).toContain('## Confusion Protocol');
expect(out).toContain('high-stakes ambiguity');
});
test('terse: returns empty string', () => {
expect(generateConfusionProtocol(makeCtx('terse'))).toBe('');
});
test('no ctx arg: defaults to non-terse', () => {
expect(generateConfusionProtocol()).toContain('## Confusion Protocol');
});
});
describe('generateContextHealth', () => {
test('default: emits full section', () => {
const out = generateContextHealth(makeCtx('default'));
expect(out).toContain('## Context Health');
expect(out).toContain('PROGRESS');
});
test('terse: returns empty string', () => {
expect(generateContextHealth(makeCtx('terse'))).toBe('');
});
});
});
describe('terse build — generatePreamble integration', () => {
test('default tier-2 preamble includes all 4 terse-gated sections', () => {
const out = generatePreamble(makeCtx('default', 2));
expect(out).toContain('## Writing Style');
expect(out).toContain('## Completeness Principle');
expect(out).toContain('## Confusion Protocol');
expect(out).toContain('## Context Health');
});
test('terse tier-2 preamble drops 3 of 4 sections + collapses Writing Style', () => {
const out = generatePreamble(makeCtx('terse', 2));
// Writing Style heading still present (collapsed to one line)
expect(out).toContain('## Writing Style');
expect(out).toContain('Terse mode (build-time)');
// Three sections dropped entirely
expect(out).not.toContain('## Completeness Principle');
expect(out).not.toContain('## Confusion Protocol');
expect(out).not.toContain('## Context Health');
});
test('terse preamble is measurably smaller', () => {
const defaultLen = generatePreamble(makeCtx('default', 2)).length;
const terseLen = generatePreamble(makeCtx('terse', 2)).length;
// Saving roughly 2-4 KB across the 4 sections; assert at least 1 KB saved.
expect(defaultLen - terseLen).toBeGreaterThan(1024);
});
test('terse preamble at tier 1 is identical to default (terse only affects tier-2+ sections)', () => {
// Tier 1 doesn't include the 4 terse-gated sections in the first place.
const defaultT1 = generatePreamble(makeCtx('default', 1));
const terseT1 = generatePreamble(makeCtx('terse', 1));
expect(terseT1).toBe(defaultT1);
});
test('explainLevel undefined behaves as default', () => {
const undefinedOut = generatePreamble(makeCtx(undefined, 2));
const defaultOut = generatePreamble(makeCtx('default', 2));
expect(undefinedOut).toBe(defaultOut);
});
});