import { afterEach, describe, expect, test } from 'bun:test'; import { execFileSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { between } from './helpers/prompt-structure'; const ROOT = path.resolve(import.meta.dir, '..'); const BIN = path.join(ROOT, 'bin/gstack-ceo-plan-find'); const temps: string[] = []; afterEach(() => { for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); }); function fixture() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ceo-plan-find-')); temps.push(dir); const home = path.join(dir, 'home'); fs.mkdirSync(home, { recursive: true }); const plans = path.join(home, '.gstack/projects/proj/ceo-plans'); const baseEnv = { PATH: process.env.PATH!, HOME: home }; // The header /plan-ceo-review writes (plan-ceo-review/SKILL.md.tmpl, 0H). const write = (file: string, branch: string | null, ageSeconds: number) => { fs.mkdirSync(path.dirname(file), { recursive: true }); const header = branch === null ? '' : `Branch: ${branch} | Mode: SELECTIVE EXPANSION\n`; fs.writeFileSync(file, `---\nstatus: ACTIVE\n---\n# CEO Plan: Feature\nGenerated by /plan-ceo-review on 2026-10-12\n${header}Repo: acme/widgets\n`); const t = Date.now() / 1000 - ageSeconds; fs.utimesSync(file, t, t); return file; }; const find = (env: Record = {}, args = ['proj', 'feat-x']) => execFileSync(BIN, args, { cwd: dir, env: { ...baseEnv, ...env }, encoding: 'utf8', timeout: 10_000 }).trim(); return { dir, home, plans, write, find }; } describe('gstack-ceo-plan-find', () => { test('default root is ~/.gstack and prints nothing when no plan exists', () => { const f = fixture(); expect(f.find()).toBe(''); const plan = f.write(path.join(f.plans, '2026-10-01-feature.md'), 'feat/x', 100); expect(f.find()).toBe(plan); }); test('GSTACK_HOME moves the state root', () => { const f = fixture(); f.write(path.join(f.plans, '2026-10-01-a.md'), 'feat/x', 10); const custom = path.join(f.dir, 'custom-state'); const plan = f.write(path.join(custom, 'projects/proj/ceo-plans/2026-10-02-b.md'), 'feat/x', 100); expect(f.find({ GSTACK_HOME: custom })).toBe(plan); }); test('the plan for this branch wins over a newer plan for another branch', () => { const f = fixture(); // gstack-slug reports feat/x as feat-x; the plan header keeps the slash. const mine = f.write(path.join(f.plans, '2026-10-01-mine.md'), 'feat/x', 500); const other = f.write(path.join(f.plans, '2026-10-03-other.md'), 'main', 10); expect(f.find()).toBe(mine); expect(f.find({}, ['proj', 'main'])).toBe(other); expect(f.find({}, ['proj', 'unrelated'])).toBe(other); }); test('the newest plan for the branch wins, and a plan without a Branch line is still a fallback', () => { const f = fixture(); f.write(path.join(f.plans, '2026-10-01-old.md'), 'feat-x', 500); const newer = f.write(path.join(f.plans, '2026-10-02-new.md'), 'feat-x', 100); const headerless = f.write(path.join(f.plans, '2026-10-03-headerless.md'), null, 10); expect(f.find()).toBe(newer); expect(f.find({}, ['proj', 'other'])).toBe(headerless); }); test('only top-level plans count', () => { const f = fixture(); const plan = f.write(path.join(f.plans, '2026-10-01-live.md'), 'feat/x', 500); f.write(path.join(f.plans, 'archive/2026-10-02-old.md'), 'feat/x', 10); expect(f.find()).toBe(plan); }); test('usage errors exit 2', () => { const f = fixture(); for (const args of [[], ['proj'], ['', 'feat-x']]) { let status: number | null = null; try { f.find({}, args); } catch (err) { status = (err as { status: number | null }).status; } expect(status).toBe(2); } }); }); describe('rendered plan-eng-review Design Doc Check (#280)', () => { const rendered = fs.readFileSync(path.join(ROOT, 'plan-eng-review/SKILL.md'), 'utf8'); const check = between(rendered, '### Design Doc Check', '### Step 0'); const at = check.indexOf('CEO_PLAN=$(~/.claude/skills/gstack/bin/gstack-ceo-plan-find'); const block = check.slice(Math.max(at, 0)).split('\n').slice(0, 2).join('\n'); test('the check renders one CEO-plan helper invocation before Step 0', () => { expect(at).toBeGreaterThanOrEqual(0); expect(block).toContain('gstack-ceo-plan-find "$SLUG" "$BRANCH"'); expect(block).toContain('echo "CEO plan found: $CEO_PLAN"'); expect(block).toContain('echo "No CEO plan found"'); }); test('finds the plan for this branch under a custom GSTACK_HOME and reports none elsewhere', () => { expect(at).toBeGreaterThanOrEqual(0); const f = fixture(); const installed = path.join(f.home, '.claude/skills/gstack/bin'); fs.mkdirSync(installed, { recursive: true }); for (const bin of ['gstack-ceo-plan-find', 'gstack-paths', 'gstack-state-root.sh']) { fs.copyFileSync(path.join(ROOT, 'bin', bin), path.join(installed, bin)); fs.chmodSync(path.join(installed, bin), 0o755); } const run = (env: Record) => execFileSync('bash', ['-c', `SLUG=proj; BRANCH=feat-x; ${block}`], { cwd: f.dir, env: { PATH: process.env.PATH!, HOME: f.home, ...env }, encoding: 'utf8', timeout: 10_000 }).trim(); const custom = path.join(f.dir, 'custom-state'); const mine = f.write(path.join(custom, 'projects/proj/ceo-plans/2026-10-01-mine.md'), 'feat/x', 500); f.write(path.join(custom, 'projects/proj/ceo-plans/2026-10-03-other.md'), 'main', 10); expect(run({ GSTACK_HOME: custom })).toBe(`CEO plan found: ${mine}`); expect(run({})).toBe('No CEO plan found'); }); });