import { afterEach, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as fsp from 'node:fs/promises'; import * as os from 'node:os'; import * as path from 'node:path'; import { pathToFileURL } from 'node:url'; import { SOURCE_ASSIGNMENTS } from '../scripts/gstack2/assignments'; import { ROOT, legacySections, renderLegacyBody, renderPortedLegacyBody, renderPortedLegacySection, retiredInvocationPattern, } from '../scripts/gstack2/render-legacy'; import { TREE_NAMES } from '../scripts/gstack2/types'; const temporary: string[] = []; afterEach(async () => { await Promise.all(temporary.splice(0).map((entry) => fsp.rm(entry, { recursive: true, force: true }))); }); function ownerModule(source: string): string { const assignment = SOURCE_ASSIGNMENTS.find((entry) => entry.source === source); if (!assignment) throw new Error(`Unknown source ${source}`); return path.join(ROOT, 'skills', assignment.tree, 'references', 'legacy', `${source}.md`); } describe('GStack 2 canonical skill UX', () => { test('keeps exactly six public skills while excluding the retired shared onboarding machine', () => { const publicSkills = fs.readdirSync(path.join(ROOT, 'skills'), { withFileTypes: true }) .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(ROOT, 'skills', entry.name, 'SKILL.md'))) .map((entry) => entry.name) .sort(); expect(publicSkills).toEqual([...TREE_NAMES].sort()); for (const assignment of SOURCE_ASSIGNMENTS) { const body = fs.readFileSync(ownerModule(assignment.source), 'utf8'); expect(body, assignment.source).not.toContain('## Preamble (run first)'); expect(body, assignment.source).not.toMatch( /MODEL_OVERLAY: claude|CLAUDE_PLAN_FILE|Add routing rules to CLAUDE\.md|Boil the Ocean principle|TEL_PROMPTED|PROACTIVE_PROMPTED/, ); expect(body, assignment.source).not.toContain('cd && ./setup'); } }); test('resolves retired user-facing recommendations without rewriting package paths', () => { for (const assignment of SOURCE_ASSIGNMENTS) { const body = fs.readFileSync(ownerModule(assignment.source), 'utf8'); expect(body, assignment.source).not.toMatch(retiredInvocationPattern()); } expect(fs.readFileSync(ownerModule('plan-ceo-review'), 'utf8')) .toContain('$plan --mode Discovery --module office-hours'); expect(fs.readFileSync(ownerModule('open-gstack-browser'), 'utf8')) .toContain('$GSTACK_BIN/gstack runtime path extension'); }); test('packages one consent-first host-neutral bootstrap with every selected skill', () => { const source = fs.readFileSync(path.join(ROOT, 'runtime', 'runtime-bootstrap.mjs')); for (const tree of TREE_NAMES) { const runtime = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'RUNTIME.md'), 'utf8'); const bootstrap = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs')); const contract = JSON.parse(fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-contract.json'), 'utf8')); const resultContract = JSON.parse(fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'execution-result-contract.json'), 'utf8')); expect(bootstrap, tree).toEqual(source); expect(contract, tree).toEqual({ schemaVersion: 1, runtimeVersion: '2.0.0', skillApi: '2.0' }); expect(resultContract.properties.status.enum, tree).toEqual(['success', 'degraded', 'unsupported', 'failed']); expect(resultContract.allOf[0].then.properties.evidence.minItems, tree).toBe(1); expect(runtime, tree).toContain('preview --capability '); expect(runtime, tree).toContain('It never downloads components or mutates runtime state.'); expect(runtime, tree).toContain('install --capability --yes'); expect(runtime, tree).toContain('Never run `./setup` inside a standard-installed skill directory'); expect(runtime, tree).toContain('Deferring installation records no consent'); expect(runtime, tree).toContain('Logical `browser` expands to `browser-code + browser-headless`'); expect(runtime, tree).toContain('`browser-visible` expands to `browser-code + browser-visible` and does not require headless'); expect(runtime, tree).toContain('`pdf` depends on `diagram`'); expect(runtime, tree).toContain('`all` means those five and intentionally excludes visible Chromium'); expect(runtime, tree).toContain('summed compressed bytes'); expect(runtime, tree).toContain('B=$GSTACK_BIN/browse'); expect(runtime, tree).toContain('BUN_CMD=$GSTACK_BIN/bun'); expect(runtime, tree).toContain('discovers Git for Windows Bash'); expect(runtime, tree).toContain('Python is not a global GStack prerequisite'); } }); test('defers visible Chromium until a workflow reaches a headed point of use', () => { for (const source of ['open-gstack-browser', 'pair-agent', 'setup-browser-cookies']) { const body = fs.readFileSync(ownerModule(source), 'utf8'); expect(body, source).toContain('## Visible-browser point-of-use gate'); expect(body, source).toContain('preview --capability browser-visible'); expect(body, source).toContain('install --capability browser-visible --yes'); expect(body, source).toContain('never requires `browser-headless`'); } expect(fs.readFileSync(ownerModule('browse'), 'utf8')).not.toContain('browser-visible'); expect(fs.readFileSync(ownerModule('qa-only'), 'utf8')).not.toContain('browser-visible'); }); test('binds retained runtime helper variables to stable host-neutral paths', () => { for (const assignment of SOURCE_ASSIGNMENTS) { const body = fs.readFileSync(ownerModule(assignment.source), 'utf8'); expect(body, assignment.source).not.toMatch(/bun\.sh\/install|bun run \$GSTACK_BIN|command -v bun/); if (!/\$(?:GSTACK_BIN|GSTACK_ROOT|GSTACK_STATE_ROOT)\b|\$(?:B|D|P)\b/.test(body)) continue; expect(body, assignment.source).toContain('## Host-neutral runtime bindings'); expect(body, assignment.source).toContain('GSTACK_BIN="$GSTACK_HOME/bin"'); expect(body, assignment.source).toContain('BUN_CMD="$GSTACK_BIN/bun"'); expect(body, assignment.source).toContain('B="$GSTACK_BIN/browse"'); } }); test('the packaged post-approval bootstrap reaches the managed installer without enrolling a host', async () => { const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'gstack-skill-bootstrap-')); temporary.push(root); const source = path.join(root, 'reviewed checkout'); const home = path.join(root, 'runtime home'); await fsp.mkdir(path.join(source, 'runtime'), { recursive: true }); await fsp.writeFile(path.join(source, 'runtime', 'install.js'), [ 'import fs from "node:fs";', 'import path from "node:path";', 'const home = process.argv[process.argv.indexOf("--home") + 1];', 'fs.mkdirSync(home, { recursive: true });', 'fs.writeFileSync(path.join(home, "argv.json"), JSON.stringify(process.argv.slice(2)));', ].join('\n')); const packaged = path.join(ROOT, 'skills', 'qa', 'references', 'support', 'runtime-bootstrap.mjs'); const module = await import(`${pathToFileURL(packaged).href}?test=${Date.now()}`); let stdout = ''; let stderr = ''; const code = await module.main([ 'install', '--source', source, '--capability', 'browser', '--home', home, '--yes', ], { stdout: { write: (chunk: string) => { stdout += chunk; } }, stderr: { write: (chunk: string) => { stderr += chunk; } }, }); expect(code).toBe(0); expect(stderr).toContain('Developer-only source install'); expect(stdout).toContain('No coding host was enrolled'); expect(JSON.parse(await fsp.readFile(path.join(home, 'argv.json'), 'utf8'))).toContain('--install-now'); }); test('cuts canonical active prompt bytes by at least half without dropping carved specialist phases', () => { const baselineBytes = SOURCE_ASSIGNMENTS.reduce( (total, assignment) => total + Buffer.byteLength(renderLegacyBody(assignment.source)), 0, ); const canonicalBytes = SOURCE_ASSIGNMENTS.reduce( (total, assignment) => total + Buffer.byteLength(renderPortedLegacyBody(assignment.source)), 0, ); expect(canonicalBytes).toBeLessThan(baselineBytes * 0.5); for (const section of legacySections()) { const assignment = SOURCE_ASSIGNMENTS.find((entry) => entry.source === section.source)!; const filename = path.basename(section.relativePath).replace(/\.tmpl$/, ''); const reference = `references/sections/${section.source}/${filename}`; const module = fs.readFileSync(ownerModule(section.source), 'utf8'); const packaged = fs.readFileSync(path.join(ROOT, 'skills', assignment.tree, reference), 'utf8'); expect(module, section.relativePath).toContain(reference); expect(packaged.trim(), section.relativePath).toBe(renderPortedLegacySection(section).trim()); expect(packaged, section.relativePath).not.toMatch(retiredInvocationPattern()); } for (const tree of TREE_NAMES) { const artifactRoot = path.join(ROOT, 'skills', tree, 'references', 'artifacts'); if (!fs.existsSync(artifactRoot)) continue; const pending = [artifactRoot]; while (pending.length) { const current = pending.pop()!; for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const target = path.join(current, entry.name); if (entry.isDirectory()) pending.push(target); else if (entry.name.endsWith('.md')) { expect(fs.readFileSync(target, 'utf8'), path.relative(ROOT, target)).not.toMatch(retiredInvocationPattern()); } } } } }, 15_000); });