import { describe, test, expect } from 'bun:test'; import { COMMAND_DESCRIPTIONS } from '../browse/src/commands'; import { SNAPSHOT_FLAGS } from '../browse/src/snapshot'; import * as fs from 'fs'; import * as path from 'path'; const ROOT = path.resolve(import.meta.dir, '..'); describe('gen-skill-docs', () => { test('generated SKILL.md contains all command categories', () => { const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); const categories = new Set(Object.values(COMMAND_DESCRIPTIONS).map(d => d.category)); for (const cat of categories) { expect(content).toContain(`### ${cat}`); } }); test('generated SKILL.md contains all commands', () => { const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) { const display = meta.usage || cmd; expect(content).toContain(display); } }); test('command table is sorted alphabetically within categories', () => { const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); // Extract command names from the Navigation section as a test const navSection = content.match(/### Navigation\n\|.*\n\|.*\n([\s\S]*?)(?=\n###|\n## )/); expect(navSection).not.toBeNull(); const rows = navSection![1].trim().split('\n'); const commands = rows.map(r => { const match = r.match(/\| `(\w+)/); return match ? match[1] : ''; }).filter(Boolean); const sorted = [...commands].sort(); expect(commands).toEqual(sorted); }); test('generated header is present in SKILL.md', () => { const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); expect(content).toContain('AUTO-GENERATED from SKILL.md.tmpl'); expect(content).toContain('Regenerate: bun run gen:skill-docs'); }); test('generated header is present in browse/SKILL.md', () => { const content = fs.readFileSync(path.join(ROOT, 'browse', 'SKILL.md'), 'utf-8'); expect(content).toContain('AUTO-GENERATED from SKILL.md.tmpl'); }); test('snapshot flags section contains all flags', () => { const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); for (const flag of SNAPSHOT_FLAGS) { expect(content).toContain(flag.short); expect(content).toContain(flag.description); } }); test('template files exist for generated SKILL.md files', () => { expect(fs.existsSync(path.join(ROOT, 'SKILL.md.tmpl'))).toBe(true); expect(fs.existsSync(path.join(ROOT, 'browse', 'SKILL.md.tmpl'))).toBe(true); }); test('templates contain placeholders', () => { const rootTmpl = fs.readFileSync(path.join(ROOT, 'SKILL.md.tmpl'), 'utf-8'); expect(rootTmpl).toContain('{{COMMAND_REFERENCE}}'); expect(rootTmpl).toContain('{{SNAPSHOT_FLAGS}}'); const browseTmpl = fs.readFileSync(path.join(ROOT, 'browse', 'SKILL.md.tmpl'), 'utf-8'); expect(browseTmpl).toContain('{{COMMAND_REFERENCE}}'); expect(browseTmpl).toContain('{{SNAPSHOT_FLAGS}}'); }); }); /** * Quality evals — catch description regressions. * * These test that generated output is *useful for an AI agent*, * not just structurally valid. Each test targets a specific * regression we actually shipped and caught in review. */ describe('description quality evals', () => { // Regression: snapshot flags lost value hints (-d , -s , -o ) test('snapshot flags with values include value hints in output', () => { const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); for (const flag of SNAPSHOT_FLAGS) { if (flag.takesValue) { expect(flag.valueHint).toBeDefined(); expect(content).toContain(`${flag.short} ${flag.valueHint}`); } } }); // Regression: "is" lost the valid states enum test('is command lists valid state values', () => { const desc = COMMAND_DESCRIPTIONS['is'].description; for (const state of ['visible', 'hidden', 'enabled', 'disabled', 'checked', 'editable', 'focused']) { expect(desc).toContain(state); } }); // Regression: "press" lost common key examples test('press command lists example keys', () => { const desc = COMMAND_DESCRIPTIONS['press'].description; expect(desc).toContain('Enter'); expect(desc).toContain('Tab'); expect(desc).toContain('Escape'); }); // Regression: "console" lost --errors filter note test('console command describes --errors behavior', () => { const desc = COMMAND_DESCRIPTIONS['console'].description; expect(desc).toContain('--errors'); }); // Regression: snapshot -i lost "@e refs" context test('snapshot -i mentions @e refs', () => { const flag = SNAPSHOT_FLAGS.find(f => f.short === '-i')!; expect(flag.description).toContain('@e'); }); // Regression: snapshot -C lost "@c refs" context test('snapshot -C mentions @c refs', () => { const flag = SNAPSHOT_FLAGS.find(f => f.short === '-C')!; expect(flag.description).toContain('@c'); }); // Guard: every description must be at least 8 chars (catches empty or stub descriptions) test('all command descriptions have meaningful length', () => { for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) { expect(meta.description.length).toBeGreaterThanOrEqual(8); } }); // Guard: snapshot flag descriptions must be at least 10 chars test('all snapshot flag descriptions have meaningful length', () => { for (const flag of SNAPSHOT_FLAGS) { expect(flag.description.length).toBeGreaterThanOrEqual(10); } }); // Guard: descriptions must not contain pipe (breaks markdown table cells) // Usage strings are backtick-wrapped in the table so pipes there are safe. test('no command description contains pipe character', () => { for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) { expect(meta.description).not.toContain('|'); } }); // Guard: generated output uses → not -> test('generated SKILL.md uses unicode arrows', () => { const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); // Check the Tips section specifically (where we regressed -> from →) const tipsSection = content.slice(content.indexOf('## Tips')); expect(tipsSection).toContain('→'); expect(tipsSection).not.toContain('->'); }); });