/** * Tests for src/core/skillpack/post-install-advisory.ts (v0.25.1). * * The advisory is meant to be read by the agent (openclaw, claude-code) * from the terminal output of `gbrain init` and `gbrain post-upgrade`. * These tests pin: * - empty/no-workspace path renders a workspace-detection note * - all-installed path returns null (no-op) * - partial-install path lists ONLY the missing skills * - the rendered text contains the explicit "ASK THE USER FIRST" * framing so future changes to the prose can't accidentally * drop the user-sovereignty contract */ import { describe, expect, it, afterEach } from 'bun:test'; import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { buildAdvisory, detectInstalledSlugs, } from '../src/core/skillpack/post-install-advisory.ts'; const cleanup: string[] = []; afterEach(() => { while (cleanup.length) { const d = cleanup.pop(); if (d && existsSync(d)) rmSync(d, { recursive: true, force: true }); } }); function scratchWorkspace(receiptSlugs: string[] | null): { workspace: string; skillsDir: string; } { const workspace = mkdtempSync(join(tmpdir(), 'advisory-test-')); cleanup.push(workspace); const skillsDir = join(workspace, 'skills'); mkdirSync(skillsDir, { recursive: true }); if (receiptSlugs === null) { writeFileSync( join(skillsDir, 'RESOLVER.md'), '# RESOLVER\n\n| Trigger | Skill |\n|---------|-------|\n', ); } else { const slugList = receiptSlugs.sort().join(','); const block = [ '# RESOLVER', '', '| Trigger | Skill |', '|---------|-------|', '', '', ``, ...receiptSlugs.map( (s) => `| "${s}" | \`skills/${s}/SKILL.md\` |`, ), '', '', ].join('\n'); writeFileSync(join(skillsDir, 'RESOLVER.md'), block); } return { workspace, skillsDir }; } describe('detectInstalledSlugs', () => { it('returns empty set when no managed block', () => { const { workspace, skillsDir } = scratchWorkspace(null); expect(detectInstalledSlugs(skillsDir, workspace).size).toBe(0); }); it('reads cumulative-slugs receipt', () => { const { workspace, skillsDir } = scratchWorkspace([ 'brain-ops', 'idea-ingest', ]); const set = detectInstalledSlugs(skillsDir, workspace); expect(set.has('brain-ops')).toBe(true); expect(set.has('idea-ingest')).toBe(true); expect(set.size).toBe(2); }); }); describe('buildAdvisory — partial-install path', () => { it('lists ONLY missing skills when most are already installed', () => { const { workspace, skillsDir } = scratchWorkspace([ 'brain-ops', 'article-enrichment', 'strategic-reading', 'concept-synthesis', 'perplexity-research', 'archive-crawler', 'academic-verify', 'brain-pdf', 'voice-note-ingest', ]); const advisory = buildAdvisory({ version: '0.25.1', context: 'upgrade', targetWorkspace: workspace, targetSkillsDir: skillsDir, }); expect(advisory).not.toBeNull(); expect(advisory).toContain('book-mirror'); expect(advisory).not.toContain('article-enrichment'); expect(advisory).not.toContain('strategic-reading'); expect(advisory).toContain('gbrain skillpack scaffold book-mirror'); }); it('uses --all command when ALL recommended are missing (fresh workspace)', () => { const { workspace, skillsDir } = scratchWorkspace([]); const advisory = buildAdvisory({ version: '0.25.1', context: 'init', targetWorkspace: workspace, targetSkillsDir: skillsDir, }); expect(advisory).not.toBeNull(); expect(advisory).toContain('gbrain skillpack scaffold --all'); expect(advisory).toContain('book-mirror'); expect(advisory).toContain('article-enrichment'); expect(advisory).toContain('strategic-reading'); }); }); describe('buildAdvisory — all-installed → null (no nag)', () => { it('returns null when every recommended skill is already installed', () => { const allRecommended = [ 'book-mirror', 'article-enrichment', 'strategic-reading', 'concept-synthesis', 'perplexity-research', 'archive-crawler', 'academic-verify', 'brain-pdf', 'voice-note-ingest', ]; const { workspace, skillsDir } = scratchWorkspace(allRecommended); const advisory = buildAdvisory({ version: '0.25.1', context: 'upgrade', targetWorkspace: workspace, targetSkillsDir: skillsDir, }); expect(advisory).toBeNull(); }); }); describe('buildAdvisory — agent-readable framing', () => { it('contains the user-sovereignty contract phrasing', () => { const { workspace, skillsDir } = scratchWorkspace([]); const advisory = buildAdvisory({ version: '0.25.1', context: 'init', targetWorkspace: workspace, targetSkillsDir: skillsDir, })!; expect(advisory).toContain('ACTION FOR THE AGENT'); expect(advisory).toContain('Ask the user'); expect(advisory).toContain('Do NOT scaffold without asking'); expect(advisory).toContain('user owns this decision'); }); it('names the version + context (init vs upgrade)', () => { const { workspace, skillsDir } = scratchWorkspace([]); const initAdvisory = buildAdvisory({ version: '0.25.1', context: 'init', targetWorkspace: workspace, targetSkillsDir: skillsDir, })!; const upgradeAdvisory = buildAdvisory({ version: '0.25.1', context: 'upgrade', targetWorkspace: workspace, targetSkillsDir: skillsDir, })!; expect(initAdvisory).toContain('0.25.1'); expect(initAdvisory).toContain('installed'); expect(upgradeAdvisory).toContain('0.25.1'); expect(upgradeAdvisory).toContain('upgraded to'); }); it('describes each skill with a one-line value prop', () => { const { workspace, skillsDir } = scratchWorkspace([]); const advisory = buildAdvisory({ version: '0.25.1', context: 'init', targetWorkspace: workspace, targetSkillsDir: skillsDir, })!; expect(advisory).toContain('FLAGSHIP'); expect(advisory).toContain('two-column'); expect(advisory).toContain('verbatim'); expect(advisory).toContain('Brain-augmented web research'); }); it('shows the exact install command per the missing-set', () => { const { workspace, skillsDir } = scratchWorkspace([]); const advisory = buildAdvisory({ version: '0.25.1', context: 'init', targetWorkspace: workspace, targetSkillsDir: skillsDir, })!; expect(advisory).toContain('gbrain skillpack scaffold --all'); expect(advisory).toContain('ACTION FOR THE AGENT'); }); }); describe('buildAdvisory — no workspace detected', () => { it('still renders an advisory with a workspace-detection note', () => { const advisory = buildAdvisory({ version: '0.25.1', context: 'init', targetWorkspace: '/non/existent/workspace-xyz-' + Math.random().toString(36).slice(2), targetSkillsDir: '/non/existent/skills-xyz-' + Math.random().toString(36).slice(2), }); expect(advisory).not.toBeNull(); // No workspace -> empty installed set -> all recommended treated as missing. expect(advisory).toContain('gbrain skillpack scaffold --all'); }); });