/** * B9: one gate list, one reply grammar. Pins the grammar on the owner replies * quoted in the feedback (`d10 yes`, `1a 2a`, a 33-decision approve-all), * stable ids across pages, auto vs approval, gate_rev binding, and the bin's * exit codes. Prose in the rendering is not pinned. */ import { describe, expect, test, beforeEach, afterEach } from 'bun:test'; import { spawnSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { applyReply, decisionRows, parseReply, recHash, renderGate, validateGateList, type GateList } from '../lib/gate-list'; import { EXIT, validateFile } from '../lib/headless-artifacts'; const ROOT = path.resolve(import.meta.dir, '..'); const BIN = path.join(ROOT, 'bin', 'gstack-gate'); const ab = [{ key: 'a', text: 'option A' }, { key: 'b', text: 'option B' }]; const abc = [...ab, { key: 'c', text: 'option C' }]; const list: GateList = { schema_version: 1, run: 'maw-20261010', gate_rev: 1, items: [ { id: 'uc1', title: 'Scope of the committed wave', options: abc, recommended: 'a', kind: 'approval', phase: 'ceo' }, { id: 'd1', title: 'Delivery shape', options: abc, recommended: 'a', kind: 'approval' }, { id: 'd2', title: 'Unattended as a session kind', options: ab, recommended: 'a', kind: 'auto' }, { id: 'd3', title: 'Installer in the repo', options: ab, recommended: 'a', kind: 'auto' }, ], }; const numbered: GateList = { schema_version: 1, run: 'r', gate_rev: 2, items: [ { id: '1', title: 'first', options: ab, recommended: 'b', kind: 'approval' }, { id: '2', title: 'second', options: ab, recommended: 'a', kind: 'approval' }, ], }; const big: GateList = { schema_version: 1, run: 'r', gate_rev: 1, items: Array.from({ length: 33 }, (_, i) => ({ id: `d${i + 1}`, title: `decision ${i + 1}`, options: ab, recommended: 'a', kind: (i % 3 === 0 ? 'approval' : 'auto') as 'auto' | 'approval' })), }; let tmp: string; beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-gate-')); }); afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); }); function write(name: string, g: GateList): string { const f = path.join(tmp, name); fs.writeFileSync(f, JSON.stringify(g)); return f; } function run(args: string[]) { const r = spawnSync(BIN, args, { encoding: 'utf8', timeout: 30_000 }); return { code: r.status, out: r.stdout, err: r.stderr }; } const chosen = (p: ReturnType) => Object.fromEntries(p.choices.map(c => [c.id, c.chosen])); describe('reply grammar', () => { test('`all` takes every recommendation; approval items included', () => { const p = parseReply('all', list, 1); expect(p.valid).toBe(true); expect(chosen(p)).toEqual({ uc1: 'a', d1: 'a', d2: 'a', d3: 'a' }); expect(p.pending).toEqual([]); expect(p.choices.every(c => c.how === 'recommended')).toBe(true); }); test('`