import { describe, expect, test } from 'claude-code/testing' import type { On } from 'claude-code' import { bar, buildProgress, changedPaths, defaultTab, issueOfBranch, manualTasks, parseDecisions, parseFindings, parseLedger, parsePlan, parseWorktrees, stageOf, statusNote, stepText, worktreeOf, wrapFocus, } from '../hooks/parse.ts' const PLAN = `# OpenAI Transcriber Implementation Plan **Spec:** \`openspec/changes/openai-transcriber/\` — proposal.md. Issue: #5. Ветка: \`feat/5-openai-transcriber\`. ## Global Constraints - [ ] not a step: before any task ### Task 1: HTTP-порт и сборка multipart - [x] **Step 1: Порт** - [x] **Step 2: Фейк** \`\`\`ts // - [ ] a checkbox inside code is not a step \`\`\` ### Task 2: Настройки - [x] **Step 1: Тест** - [ ] **Step 2: Реализация** - [ ] nested sub-step ### Task 3: Ручная проверка - [ ] **Step 1: Mac** ## Self-Review - [ ] not a step either ` const LEDGER = `# SDD ledger — plan: docs/superpowers/plans/2026-10-01-openai.md Task 1: complete (commits a1b2c3d..d4e5f6a, review clean) Task 2: minor (deferred): rename helper Task 2: fix round 1/5 (1 addressed, 1 open — timeout leak; commits d4e5f6a..b7c8d9e) Task 2: Ruling: keep plan text — spec wins - Task 3: parked — flaky check — Ruling: env issue ` describe('plan', () => { test('tasks, steps, spec and issue; code fences and other sections ignored', async () => { const plan = parsePlan('p.md', PLAN) expect(plan.title).toBe('OpenAI Transcriber Implementation Plan') expect(plan.spec).toBe('openspec/changes/openai-transcriber/') expect(plan.issue).toBe(5) expect(plan.tasks.map(t => [t.n, t.title, t.stepsDone, t.stepsTotal])).toEqual([ [1, 'HTTP-порт и сборка multipart', 2, 2], [2, 'Настройки', 1, 3], [3, 'Ручная проверка', 0, 1], ]) }) test('task titles drop backticks', async () => { const plan = parsePlan('p.md', '### Task 3: `RecordingSession` — пороги\n- [ ] s\n') expect(plan.tasks[0]?.title).toBe('RecordingSession — пороги') }) test('step texts are short and readable', async () => { const plan = parsePlan('p.md', PLAN) expect(plan.tasks[1]?.steps).toEqual([ { text: '1. Тест', done: true, depth: 0 }, { text: '2. Реализация', done: false, depth: 0 }, { text: 'nested sub-step', done: false, depth: 1 }, ]) expect(stepText('**Step 1: Добавить порты в конец `src/core/ports.ts`:**')).toBe( '1. Добавить порты в конец src/core/ports.ts', ) expect(stepText('**Step 2: Добавить фейки в `tests/fakes.ts`.** В импорт типов добавить')).toBe( '2. Добавить фейки в tests/fakes.ts', ) expect(stepText('Записать результаты в описание PR')).toBe('Записать результаты в описание PR') }) test('anyTicked is false for an untouched plan', async () => { expect(buildProgress(parsePlan('p.md', PLAN)).anyTicked).toBe(true) expect(buildProgress(parsePlan('p.md', PLAN.replace(/- \[x\]/g, '- [ ]'))).anyTicked).toBe(false) }) test('without a ledger the ticked steps decide', async () => { const p = buildProgress(parsePlan('p.md', PLAN)) expect(p.source).toBe('steps') expect(p.tasks.map(t => t.status)).toEqual(['done', 'active', 'todo']) expect(p.done).toBe(1) expect(p.next?.n).toBe(2) }) }) describe('ledger', () => { test('completion, fix rounds, minors, rulings, parked', async () => { const l = parseLedger(LEDGER) expect(l.plan).toBe('docs/superpowers/plans/2026-10-01-openai.md') expect(l.tasks.get(1)?.complete).toBe(true) expect(l.tasks.get(2)?.complete).toBe(false) expect(l.deferred).toBe(1) expect(l.rulings).toBe(2) expect(l.parked).toBe(1) }) test('with a ledger SDD decides, whatever the steps say', async () => { const plan = parsePlan('p.md', PLAN.replace(/- \[ \]/g, '- [x]')) const p = buildProgress(plan, parseLedger(LEDGER)) expect(p.source).toBe('ledger') expect(p.tasks.map(t => t.status)).toEqual(['done', 'active', 'active']) expect(p.tasks[1]?.note).toBe('fix round 1/5 (1 addressed, 1 open)') expect(p.next?.n).toBe(2) }) test('a task with only a deferred minor has not started', async () => { const plan = parsePlan('p.md', '### Task 1: A\n- [ ] s\n### Task 2: B\n- [ ] s\n') const p = buildProgress(plan, parseLedger('# SDD ledger — plan: p.md\nTask 2: minor (deferred): x\n')) expect(p.tasks.map(t => t.status)).toEqual(['todo', 'todo']) expect(p.next?.n).toBe(1) }) }) describe('focus', () => { test('leaving the list wraps around instead of reaching buttons', async () => { const rows = ['task-1', 'step-1-0', 'step-1-1', 'task-2'] expect(wrapFocus(rows, 'task-1')).toBe('task-2') expect(wrapFocus(rows, 'task-2')).toBe('task-1') expect(wrapFocus(rows, undefined)).toBe('task-1') expect(wrapFocus(rows, 'step-9-9')).toBe('task-1') expect(wrapFocus(['task-1'], 'task-1')).toBe('task-1') expect(wrapFocus([], 'task-1')).toBeUndefined() }) }) describe('git', () => { test('worktrees, issue from branch, changed plans, own worktree', async () => { const wts = parseWorktrees( 'worktree /r\nHEAD 1\nbranch refs/heads/main\n\nworktree /r/.claude/worktrees/x\nHEAD 2\nbranch refs/heads/feat/7-wrapper\n\nworktree /r/d\nHEAD 3\ndetached\n', ) expect(wts).toEqual([ { path: '/r', branch: 'main' }, { path: '/r/.claude/worktrees/x', branch: 'feat/7-wrapper' }, { path: '/r/d' }, ]) expect(issueOfBranch('feat/7-wrapper')).toBe(7) expect(issueOfBranch('main')).toBeUndefined() expect(worktreeOf(wts, '/r/.claude/worktrees/x/src')?.branch).toBe('feat/7-wrapper') expect(worktreeOf(wts, '/r/src')?.branch).toBe('main') expect(changedPaths('docs/a.md\n', '?? docs/b.md\n M docs/a.md\n D docs/c.md\n')).toEqual([ 'docs/a.md', 'docs/b.md', ]) expect(bar(1, 4, 8)).toBe('██░░░░░░') }) }) const ROOT = '/repo' const WT = '/repo/.claude/worktrees/feat-5' const PLAN_REL = 'docs/superpowers/plans/2026-10-01-openai.md' const FILES: Record = { [`${WT}/.superpowers/sdd/2026-10-01-openai/progress.md`]: LEDGER, [`${WT}/.superpowers/sdd/2026-10-01-openai/plan-path`]: PLAN_REL + '\n', [`${WT}/${PLAN_REL}`]: PLAN, } const DIRS: Record = { [`${WT}/.superpowers/sdd`]: ['2026-10-01-openai'], [`${WT}/openspec/changes`]: ['archive', 'openai-transcriber'], [`${ROOT}/openspec/changes`]: ['archive'], [`${ROOT}/.claude/worktrees/feat-7/openspec/changes`]: ['archive'], } const PANE = { plugin: 'feature', component: 'Pane', requestId: 'feature', viewport: { columns: 120, rows: 40 }, props: { title: 'План', isFocused: true, bodyColumns: 80, placement: 'inline', scroll: { offset: 0, bodyRows: 30 }, view: {}, }, } as const const RUN = { origin: { kind: 'composer' }, presentation: { isFullscreen: false, columns: 120 } } as const function mockRepo(on: On) { on('session.start', () => ({ cwd: WT })) on('session.root', () => ({ value: WT })) on('command.register', ($, e) => ({ value: { command: e.name } })) on('process.run', ($, e) => { const args = e.argv.slice(1).join(' ') const cwd = e.init?.cwd const result = (exitCode: number, stdout: string) => ({ value: { exitCode, stdout, stderr: '', isStdoutTruncated: false, isStderrTruncated: false }, }) const out = (stdout: string) => result(0, stdout) if (args === 'worktree list --porcelain') { return out( `worktree ${ROOT}\nbranch refs/heads/main\n\nworktree ${WT}\nbranch refs/heads/feat/5-openai\n\nworktree ${ROOT}/.claude/worktrees/feat-7\nbranch refs/heads/feat/7-wrapper\n`, ) } if (args.startsWith('rev-parse')) return args.endsWith('refs/heads/main') ? out('abc\n') : result(1, '') if (args.startsWith('log') && cwd?.endsWith('feat-7')) return out('b15ae97 fix: crlf\n543af13 fix: settings\n') return out('') }) on('fs.list', ($, e) => ({ value: (DIRS[e.path] ?? []).map(name => ({ name, kind: 'dir' as const, size: 0, mtimeMs: 0, isLink: false })), })) on('fs.exists', ($, e) => ({ value: e.path in FILES || e.path in DIRS || e.path.endsWith('feat-7/.bounded') })) on('fs.read', ($, e) => (e.path in FILES ? { value: FILES[e.path] as string } : { deny: 'ENOENT' })) on('fs.stat', () => ({ value: { kind: 'file' as const, size: 1, mtimeMs: 5, isLink: false } })) on('ui.open', () => ({ value: { isPlaced: true } })) } test('/feature opens the SDD plan of the session worktree, then a bounded branch', async ($, on) => { mockRepo(on) await $.session.start({ surface: 'terminal', isInteractive: true, cwd: WT }) expect(await $.command.run({ command: 'feature', args: '', ...RUN })).toEqual({}) for (const surface of ['terminal', 'desktop'] as const) { const ui = await $.ui.mount({ ...PANE, surface }) expect(await ui.find({ type: 'Text', text: /#5 · openspec: openai-transcriber · по SDD-ledger/ })).toBeDefined() expect(await ui.find({ type: 'Text', text: '1/3' })).toBeDefined() expect(await ui.find({ type: 'Text', text: /Дальше: Task 2 — Настройки/ })).toBeDefined() expect(await ui.find({ type: 'Text', text: 'Этап: исполнение: Task 2' })).toBeDefined() expect(await ui.find({ type: 'Text', text: 'работает агент' })).toBeDefined() expect(await ui.find({ type: 'Text', text: /fix round 1\/5/ })).toBeDefined() // The next task is open by default; a task row toggles its steps expect(await ui.find({ type: 'Text', text: '1. Тест' })).toBeDefined() expect(await ui.find({ type: 'Text', text: /✓ $/ })).toBeDefined() expect(await ui.find({ type: 'Text', text: '2. Реализация' })).toBeDefined() await ui.press({ key: 'task-2' }) expect(await ui.find({ type: 'Text', text: '2. Реализация' })).toBeUndefined() await ui.press({ key: 'task-1' }) // Task 1 is complete in the ledger, so its steps read as done though the plan has no ticks there expect(await ui.find({ type: 'Text', text: '2. Фейк' })).toBeDefined() await ui.press({ key: 'task-1' }) await ui.press({ key: 'task-2' }) // h hides finished tasks await ui.press({ key: 'toggle-done' }) expect(await ui.find({ type: 'Text', text: /HTTP-порт/ })).toBeUndefined() expect(await ui.find({ type: 'Text', text: /скрыто готовых: 1/ })).toBeDefined() await ui.press({ key: 'toggle-done' }) // The bounded branch: no plan, its commits instead // The top line is a carousel; 9 / 0 at the bottom step through every branch, wrapping around expect((await ui.findAll({ type: 'Text', text: /^[●○]$/ })).map(d => d.text).join(' ')).toBe('● ○') expect((await ui.find({ key: 'prev-branch' }))?.props.hotkey).toBe('9') expect((await ui.find({ key: 'next-branch' }))?.props.hotkey).toBe('0') await ui.press({ key: 'next-branch' }) expect((await ui.findAll({ type: 'Text', text: /^[●○]$/ })).map(d => d.text).join(' ')).toBe('○ ●') expect(await ui.find({ type: 'Text', text: /Этап: bounded/ })).toBeDefined() expect(await ui.find({ type: 'Text', text: /b15ae97 fix: crlf/ })).toBeDefined() expect(await ui.find({ type: 'Text', text: /#7 · bounded/ })).toBeDefined() await ui.press({ key: 'next-branch' }) expect(await ui.find({ type: 'Text', text: /HTTP-порт/ })).toBeDefined() await ui.press({ key: 'prev-branch' }) expect(await ui.find({ type: 'Text', text: /Этап: bounded/ })).toBeDefined() expect(await ui.find({ type: 'Text', text: /↑↓ Enter/ })).toBeDefined() await ui.press({ key: 'prev-branch' }) expect((await ui.findAll({ type: 'Text', text: /^[●○]$/ })).map(d => d.text).join(' ')).toBe('● ○') await ui.unmount() } }) test('/feature 7 picks the branch by issue number', async ($, on) => { mockRepo(on) await $.session.start({ surface: 'terminal', isInteractive: true, cwd: WT }) await $.command.run({ command: 'feature', args: '#7', ...RUN }) const ui = await $.ui.mount({ ...PANE, surface: 'terminal' }) expect(await ui.find({ type: 'Text', text: /Этап: bounded/ })).toBeDefined() await ui.unmount() }) test('falls back to /progress when /feature is taken', async ($, on) => { const registered: string[] = [] on('session.start', () => ({ cwd: ROOT })) on('command.register', ($, e) => { if (e.name === 'feature') return { deny: '"/feature" refused: taken' } registered.push(e.name) return { value: { command: e.name } } }) on('command.run', () => ({ text: 'someone else' })) await $.session.start({ surface: 'terminal', isInteractive: true, cwd: ROOT }) expect(registered).toEqual(['progress']) expect(await $.command.run({ command: 'feature', args: '', ...RUN })).toEqual({ text: 'someone else' }) }) test('a plan SDD has not started shows no step counters and opens steps on demand', async ($, on) => { const WT2 = '/repo/.claude/worktrees/feat-11' const REL = 'docs/superpowers/plans/2026-10-15-wrapper.md' const fresh = PLAN.replace(/- \[x\]/g, '- [ ]') on('session.start', () => ({ cwd: WT2 })) on('session.root', () => ({ value: WT2 })) on('command.register', ($, e) => ({ value: { command: e.name } })) on('process.run', ($, e) => { const args = e.argv.slice(1).join(' ') const out = (stdout: string) => ({ value: { exitCode: 0, stdout, stderr: '', isStdoutTruncated: false, isStderrTruncated: false }, }) if (args === 'worktree list --porcelain') return out(`worktree ${WT2}\nbranch refs/heads/feat/11-wrapper\n`) if (args.startsWith('rev-parse')) return out('abc\n') if (args.startsWith('diff')) return out(REL + '\n') return out('') }) on('fs.list', () => ({ value: [] })) on('fs.exists', () => ({ value: false })) on('fs.read', ($, e) => (e.path === `${WT2}/${REL}` ? { value: fresh } : { deny: 'ENOENT' })) on('fs.stat', () => ({ value: { kind: 'file' as const, size: 1, mtimeMs: 5, isLink: false } })) on('ui.open', () => ({ value: { isPlaced: true } })) await $.session.start({ surface: 'terminal', isInteractive: true, cwd: WT2 }) await $.command.run({ command: 'feature', args: '', ...RUN }) const ui = await $.ui.mount({ ...PANE, surface: 'terminal' }) expect(await ui.find({ type: 'Text', text: /#5 · SDD не запущен/ })).toBeDefined() // До старта исполнения открыта вкладка документов, план в ней; задачи — на вкладке работы expect(await ui.find({ type: 'Text', text: 'Этап: ревью плана и выбор режима исполнения' })).toBeDefined() expect(await ui.find({ type: 'Text', text: 'План — OpenAI Transcriber Implementation Plan' })).toBeDefined() // Путей в списке документов нет expect(await ui.find({ type: 'Text', text: REL })).toBeUndefined() await ui.press({ key: 'tab-tasks' }) expect(await ui.find({ type: 'Text', text: /0\/2/ })).toBeUndefined() // Prefix and title are separate columns: the title wraps under itself, not under the prefix expect(await ui.find({ type: 'Text', text: '+ ○ 3. ' })).toBeDefined() // The next task is marked ●, open (−) and holds the focus ring first expect(await ui.find({ type: 'Text', text: '− ● 1. ' })).toBeDefined() expect(await ui.find({ type: 'Text', text: 'Ручная проверка' })).toBeDefined() // Task 1 is next, so it is open expect(await ui.find({ type: 'Text', text: '1. Порт' })).toBeDefined() // A step is a focus stop too; Enter on it folds its task await ui.press({ key: 'step-1-0' }) expect(await ui.find({ type: 'Text', text: '1. Порт' })).toBeUndefined() expect(await ui.find({ type: 'Text', text: '+ ● 1. ' })).toBeDefined() // e opens every task await ui.press({ key: 'expand-all' }) expect(await ui.find({ type: 'Text', text: '1. Mac' })).toBeDefined() await ui.press({ key: 'expand-all' }) expect(await ui.find({ type: 'Text', text: '1. Порт' })).toBeUndefined() await ui.unmount() }) // Строки из настоящего ledger фичи #10, сокращены const LEDGER_10 = `# SDD ledger — plan: docs/superpowers/plans/2026-10-15-obsidian-wrapper.md Scope: Tasks 1-6; Task 7 is manual (human). Base: 60fed47. O-1: Task 1 implementer used a Haiku trailer; amended HEAD (fixed, now a1317e3). Task 1: minor (deferred): T1.1 no test for start() after stop() (restart) Task 1: complete (commits 60fed47..a1317e3, review clean) Task 5: review: spec compliant; 2 Important: T5.I-1 'error' event drops tail chunk; T5.I-2 mic leak on start failure Task 5: Ruling: R-1 (design #15) — fix both plan-mandated defects Task 5: minor (deferred): T5.2 handlers.end() throw; T5.3 AudioContext suspended on iOS; T5.4 track mute unhandled Task 5: fix round 1/5 dispatched (T5.I-1, T5.I-2, T5.1), resume implementer Task 5: fix round 1/5 (3 addressed, 0 open — T5.I-1, T5.I-2, T5.1; commits f8b0df3..7f03ea5) Final review (opus): no Critical. I-1 Task 7 checklist lacks level bar. I-2 stale metadataCache Final: Ruling: R-3 (design #17) — fix I-2: poke on metadataCache 'changed' Final: fix wave fixed I-1, I-2, T5.3 (06aa7bb); scoped re-review: all addressed Final: M-1 deferred; M-2 deferred; T2.1 deferred (follow-up) ` const DESIGN = `# Design ## Decisions ### Порты 1. not a decision — another section ## Decisions made without discussion Формат: дата — решение. *Обоснование.* **Цена ошибки.** 1. 2026-10-15 — Окно записи с таймером и \`Стоп\`. (Принято человеком.) 2. 2026-10-15 — Очередь раз в 30 секунд. *Повторы стоят 30 с.* **Батарея.** 17. 2026-10-15 (исполнение) — проход очереди по \`metadataCache.on('changed')\`. *list() читает кэш.* ## Risks 3. 2026-10-15 — not here either ` describe('feature', () => { test('ledger: rulings by design number, manual tasks, final review', async () => { const l = parseLedger(LEDGER_10) expect([...l.rulingOf]).toEqual([[15, 'R-1'], [17, 'R-3']]) expect(l.manual).toEqual([7]) expect(l.finalReview).toBe(true) expect(parseLedger(LEDGER).finalReview).toBe(false) }) test('provisional findings: last explicit status per marker wins', async () => { const f = parseFindings(LEDGER_10) expect(f.final).toBe(false) const status = Object.fromEntries(f.items.map(i => [i.marker, i.status])) expect(status).toEqual({ 'O-1': 'fixed', 'T1.1': 'deferred', 'T5.I-1': 'fixed', 'T5.I-2': 'fixed', 'T5.2': 'deferred', 'T5.3': 'fixed', 'T5.4': 'deferred', 'T5.1': 'fixed', 'I-1': 'fixed', 'I-2': 'fixed', 'M-1': 'deferred', 'M-2': 'deferred', 'T2.1': 'deferred', }) expect(f.items.find(i => i.marker === 'T1.1')?.text).toBe('no test for start() after stop() (restart)') }) test('the controller table, when present, is the final word', async () => { const md = LEDGER_10 + ` ## Findings | Маркер | Суть | Статус | |---|---|---| | T1.1 | нет теста на \`start()\` после stop() | deferred | | T1.2 | FakeClock.jump не используется | rejected: нужен тестам Task 2 | | T5.3 | AudioContext suspended | fixed 06aa7bb (resume()) | ` const f = parseFindings(md) expect(f.final).toBe(true) expect(f.items.map(i => [i.marker, i.status])).toEqual([ ['T1.1', 'deferred'], ['T1.2', 'rejected'], ['T5.3', 'fixed'], ]) expect(f.items[0]?.text).toBe('нет теста на start() после stop()') expect(f.items[1]?.detail).toBe('rejected: нужен тестам Task 2') expect(statusNote('deferred (решит Task 7, шаг 3)')).toBe('решит Task 7, шаг 3') expect(statusNote('rejected: нужен тестам Task 2')).toBe('нужен тестам Task 2') expect(statusNote('deferred')).toBe('') expect(statusNote('')).toBe('') // Колонки по заголовкам, в любом порядке const swapped = parseFindings('## Findings\n| Status | Marker | Text |\n|-|-|-|\n| deferred | M-1 | jitter |\n') expect(swapped.items).toEqual([{ marker: 'M-1', text: 'jitter', status: 'deferred', detail: 'deferred' }]) }) test('decisions: only the section, text without rationale, human-accepted flagged', async () => { const d = parseDecisions(DESIGN) expect(d.map(x => x.n)).toEqual([1, 2, 17]) expect(d[0]).toEqual({ n: 1, date: '2026-10-15', stage: undefined, text: 'Окно записи с таймером и Стоп.', why: '', cost: '', human: true, }) expect([d[1]?.why, d[1]?.cost]).toEqual(['Повторы стоят 30 с.', 'Батарея.']) // Второй вид записи: подписи «Обоснование:» и «Цена ошибки:» внутри разметки; пометка о человеке отрезана const labelled = parseDecisions( '## Decisions made without discussion\n15. 2026-10-15 (план) — Ждать stop. *Обоснование:* спека (T5.I-1). **Цена ошибки:** запоздает. (Подтверждено человеком.)\n', ) expect(labelled[0]).toMatchObject({ text: 'Ждать stop.', why: 'спека (T5.I-1).', cost: 'запоздает.', human: true }) expect(d[1]?.text).toBe('Очередь раз в 30 секунд.') expect(d[1]?.human).toBe(false) expect(d[2]?.stage).toBe('исполнение') expect(d[2]?.text).toBe("проход очереди по metadataCache.on('changed').") }) test('stage and whose move it is', async () => { const plan = parsePlan('p.md', '### Task 1: A\n- [ ] s\n### Task 2: Ручная проверка\n- [ ] s\n') const base = { change: true, bounded: false, manual: [2], pushed: false, commits: 3 } expect(stageOf({ ...base, change: false })).toEqual({ label: 'brainstorming', who: 'you' }) expect(stageOf(base)).toEqual({ label: 'ревью спеки', who: 'you' }) expect(stageOf({ ...base, planPath: 'p.md', progress: buildProgress(plan) }).label).toBe( 'ревью плана и выбор режима исполнения', ) const running = parseLedger('# SDD ledger — plan: p.md\nTask 1: fix round 1/5\n') expect(stageOf({ ...base, planPath: 'p.md', ledger: running, progress: buildProgress(plan, running) })).toEqual({ label: 'исполнение: Task 1', who: 'agent', }) const reviewing = parseLedger('# SDD ledger — plan: p.md\nTask 1: complete\n') expect(stageOf({ ...base, planPath: 'p.md', ledger: reviewing, progress: buildProgress(plan, reviewing) }).label).toBe( 'финальное ревью ветки', ) const done = parseLedger('# SDD ledger — plan: p.md\nTask 1: complete\nFinal: clean\n') const finished = { ...base, planPath: 'p.md', ledger: done, progress: buildProgress(plan, done) } expect(stageOf(finished)).toEqual({ label: 'ручная проверка (Task 2)', who: 'you' }) expect(stageOf({ ...finished, pushed: true }).who).toBe('you') expect(stageOf({ ...finished, pushed: true }).label).toMatch(/PR/) expect(manualTasks(plan, undefined)).toEqual([2]) expect(stageOf({ ...base, bounded: true }).who).toBe('agent') }) test('default tab follows the stage', async () => { const plan = parsePlan('p.md', '### Task 1: A\n- [ ] s\n') const ledger = parseLedger('# SDD ledger — plan: p.md\nTask 1: fix round 1/5\n') const base = { docs: 3, rulings: 2, boundedNoPlan: false, pushed: false } // Ревью спеки и плана — документы expect(defaultTab(base)).toBe('docs') expect(defaultTab({ ...base, progress: buildProgress(plan) })).toBe('docs') // Исполнение и ручная проверка — задачи; bounded без плана и без документов — тоже expect(defaultTab({ ...base, progress: buildProgress(plan, ledger), ledger })).toBe('tasks') expect(defaultTab({ ...base, boundedNoPlan: true })).toBe('tasks') expect(defaultTab({ ...base, docs: 0 })).toBe('tasks') // Ветка на сервере — рулинги, если они есть expect(defaultTab({ ...base, progress: buildProgress(plan, ledger), ledger, pushed: true })).toBe('rulings') expect(defaultTab({ ...base, progress: buildProgress(plan, ledger), ledger, pushed: true, rulings: 0 })).toBe('tasks') }) }) test('after the run: stage is yours, rulings and deferred unfold, Enter opens the document beside the session', async ($, on) => { const W = '/repo/.claude/worktrees/feat-10' const REL = 'docs/superpowers/plans/2026-10-15-wrapper.md' const SDD = `${W}/.superpowers/sdd/2026-10-15-wrapper` const DESIGN_PATH = `${W}/openspec/changes/obsidian-wrapper/design.md` const ledger = `# SDD ledger — plan: ${REL} Task 1: minor (deferred): T1.1 no restart test Task 1: complete Task 2: Ruling: R-1 (design #2) — keep the interval Task 2: review: T2.I-1 tail chunk lost Task 2: complete Final: no Critical; M-1 deferred ` const files: Record = { [`${SDD}/progress.md`]: ledger, [`${W}/${REL}`]: PLAN, [DESIGN_PATH]: DESIGN, [`${W}/openspec/changes/obsidian-wrapper/proposal.md`]: '# P', [`${W}/openspec/changes/obsidian-wrapper/specs/recording-lifecycle/spec.md`]: '# S', } const dirsOf: Record = { [`${W}/.superpowers/sdd`]: ['2026-10-15-wrapper'], [`${W}/openspec/changes`]: ['obsidian-wrapper'], [`${W}/openspec/changes/obsidian-wrapper/specs`]: ['recording-lifecycle'], } const runs: string[][] = [] on('session.start', () => ({ cwd: W })) on('session.root', () => ({ value: W })) on('command.register', ($, e) => ({ value: { command: e.name } })) on('env.get', ($, e) => ({ value: e.name === 'TERM_PROGRAM' ? 'iTerm.app' : undefined })) on('process.run', ($, e) => { runs.push([...e.argv]) const args = e.argv.slice(1).join(' ') const out = (stdout: string, exitCode = 0) => ({ value: { exitCode, stdout, stderr: '', isStdoutTruncated: false, isStderrTruncated: false }, }) if (args === 'worktree list --porcelain') return out(`worktree ${W}\nbranch refs/heads/feat/10-wrapper\n`) if (args.startsWith('rev-parse')) return out('abc\n') // Ещё не запушена if (args.startsWith('rev-list')) return out('', 128) if (e.argv[0] === '/usr/bin/which') return out('/opt/homebrew/bin/glow\n') return out('') }) on('fs.list', ($, e) => ({ value: (dirsOf[e.path] ?? []).map(name => ({ name, kind: 'dir' as const, size: 0, mtimeMs: 0, isLink: false })), })) on('fs.exists', ($, e) => ({ value: e.path in files || e.path in dirsOf })) on('fs.read', ($, e) => (e.path in files ? { value: files[e.path] as string } : { deny: 'ENOENT' })) on('fs.stat', () => ({ value: { kind: 'file' as const, size: 1, mtimeMs: 5, isLink: false } })) on('ui.open', () => ({ value: { isPlaced: true } })) await $.session.start({ surface: 'terminal', isInteractive: true, cwd: W }) await $.command.run({ command: 'feature', args: '', ...RUN }) const ui = await $.ui.mount({ ...PANE, surface: 'terminal' }) // Task 3 «Ручная проверка» — человеку; остальное готово, финальное ревью было expect(await ui.find({ type: 'Text', text: 'Этап: ручная проверка (Task 3)' })).toBeDefined() expect(await ui.find({ type: 'Text', text: 'ход за тобой' })).toBeDefined() // Вкладки по порядку этапов, открыта вкладка задач; на вкладках счётчики. Решение, принятое человеком (#1), не в счёт expect(await ui.find({ type: 'Text', text: 'Все задачи выполнены' })).toBeUndefined() expect(await ui.find({ type: 'Text', text: /Дальше: Task 3/ })).toBeDefined() expect(await ui.find({ type: 'Button', key: 'tab-rulings', text: /^○ Рулинги 2$/ })).toBeDefined() expect(await ui.find({ type: 'Button', key: 'tab-rulings', text: /^○ Рулинги 3$/ })).toBeUndefined() expect(await ui.find({ type: 'Button', key: 'tab-deferred', text: /^○ Отложено 2 !1$/ })).toBeDefined() expect(await ui.find({ type: 'Button', key: 'tab-tasks', text: /^● Задачи 2\/3$/ })).toBeDefined() expect(await ui.find({ type: 'Text', text: 'Очередь раз в 30 секунд.' })).toBeUndefined() await ui.press({ key: 'tab-rulings' }) expect(await ui.find({ type: 'Text', text: 'Очередь раз в 30 секунд.' })).toBeDefined() expect(await ui.find({ type: 'Text', text: /^\+ R-1 +#2/ })).toBeDefined() // Enter раскрывает обоснование и цену ошибки прямо в списке, документ не открывается expect(await ui.find({ type: 'Text', text: 'Почему: Повторы стоят 30 с.' })).toBeUndefined() await ui.press({ key: 'rul-2' }) expect(await ui.find({ type: 'Text', text: 'Почему: Повторы стоят 30 с.' })).toBeDefined() expect(await ui.find({ type: 'Text', text: 'Цена ошибки: Батарея.' })).toBeDefined() expect(await ui.find({ type: 'Text', text: /^− R-1 +#2/ })).toBeDefined() await ui.press({ key: 'rul-2' }) expect(await ui.find({ type: 'Text', text: 'Цена ошибки: Батарея.' })).toBeUndefined() expect(runs.some(r => r[0] === 'osascript')).toBe(false) expect(await ui.find({ type: 'Text', text: /Окно записи/ })).toBeUndefined() // Отложенное по маркерам, открытые находки — отдельно и первыми await ui.press({ key: 'tab-deferred' }) expect(await ui.find({ type: 'Text', text: /предварительно, по строкам ledger/ })).toBeDefined() expect(await ui.find({ type: 'Text', text: 'Очередь раз в 30 секунд.' })).toBeUndefined() expect(await ui.find({ type: 'Text', text: 'no restart test' })).toBeDefined() expect(await ui.find({ type: 'Text', text: 'tail chunk lost' })).toBeDefined() expect(await ui.find({ type: 'Text', text: /M-1/ })).toBeDefined() // Документы — отдельная вкладка: на этапе после исполнения она не открыта и не шумит expect(await ui.find({ type: 'Text', text: /Спека recording-lifecycle/ })).toBeUndefined() // Вкладка «Документы»: Enter открывает glow в соседней панели iTerm await ui.press({ key: 'tab-docs' }) expect(await ui.find({ type: 'Text', text: 'no restart test' })).toBeUndefined() expect(await ui.find({ type: 'Text', text: 'Спека recording-lifecycle — требования и сценарии' })).toBeDefined() expect(await ui.find({ type: 'Text', text: /^Proposal — / })).toBeDefined() expect(await ui.find({ type: 'Text', text: /openspec\/changes/ })).toBeUndefined() await ui.press({ key: 'doc-' + DESIGN_PATH }) const osa = runs.find(r => r[0] === 'osascript') expect(osa?.slice(-2)).toEqual(['/opt/homebrew/bin/glow -t', DESIGN_PATH]) expect(osa?.join(' ')).toMatch(/iTerm2.*split vertically/) // Выбор вкладки запоминается await ui.press({ key: 'tab-deferred' }) expect(await ui.find({ type: 'Text', text: 'no restart test' })).toBeDefined() await ui.unmount() }) test('before the run the documents tab is the default: spec review', async ($, on) => { const W = '/repo/.claude/worktrees/feat-12' const CH = `${W}/openspec/changes/new-thing` const files: Record = { [`${CH}/proposal.md`]: '# P', [`${CH}/design.md`]: '# D' } on('session.start', () => ({ cwd: W })) on('session.root', () => ({ value: W })) on('command.register', ($, e) => ({ value: { command: e.name } })) on('process.run', ($, e) => { const args = e.argv.slice(1).join(' ') const out = (stdout: string, exitCode = 0) => ({ value: { exitCode, stdout, stderr: '', isStdoutTruncated: false, isStderrTruncated: false }, }) if (args === 'worktree list --porcelain') return out(`worktree ${W}\nbranch refs/heads/feat/12-new-thing\n`) if (args.startsWith('rev-parse')) return out('abc\n') if (args.startsWith('rev-list')) return out('', 128) return out('') }) on('fs.list', ($, e) => ({ value: (e.path === `${W}/openspec/changes` ? ['new-thing'] : []).map(name => ({ name, kind: 'dir' as const, size: 0, mtimeMs: 0, isLink: false, })), })) on('fs.exists', ($, e) => ({ value: e.path in files })) on('fs.read', ($, e) => (e.path in files ? { value: files[e.path] as string } : { deny: 'ENOENT' })) on('fs.stat', () => ({ value: { kind: 'file' as const, size: 1, mtimeMs: 5, isLink: false } })) on('ui.open', () => ({ value: { isPlaced: true } })) await $.session.start({ surface: 'terminal', isInteractive: true, cwd: W }) await $.command.run({ command: 'feature', args: '', ...RUN }) const ui = await $.ui.mount({ ...PANE, surface: 'terminal' }) expect(await ui.find({ type: 'Text', text: 'Этап: ревью спеки' })).toBeDefined() expect(await ui.find({ type: 'Text', text: /^Proposal — / })).toBeDefined() expect(await ui.find({ type: 'Text', text: /Коммитов поверх main нет/ })).toBeUndefined() await ui.press({ key: 'tab-tasks' }) expect(await ui.find({ type: 'Text', text: /^Proposal — / })).toBeUndefined() expect(await ui.find({ type: 'Text', text: /Коммитов поверх main нет/ })).toBeDefined() await ui.unmount() })