/** @fileoverview Guarded staging one-shot and content-verification safety. */ import { beforeEach, expect, it } from 'vitest'; import { createHash } from 'node:crypto'; import { CODEX_REASONING_EFFORTS } from '../src/types/session.js'; import { GuardedStageStore, type GuardedInputTarget } from '../src/agent-staging.js'; const sha = (s: string) => createHash('sha256').update(s).digest('hex'); let store: GuardedStageStore; let time: number; let generation: string; let revision: number; let composer: string | null; let writes: string[]; let fail: boolean; let target: GuardedInputTarget; beforeEach(() => { time = 0; generation = 'pane-1'; revision = 0; composer = ''; writes = []; fail = false; store = new GuardedStageStore(() => time, 2, 1000); target = { id: 's', snapshot: () => composer === null ? null : { generation, revision, composerSha: sha(composer), availableColumns: 100 }, submit: async () => target.write('\r'), write: async (text) => { writes.push(text); revision++; if (fail) return false; composer = text === '\r' ? '' : composer + text; return true; }, }; }); it('stages without Enter, stores only a hash, and submits exactly once', async () => { const receipt = await store.stage(target, 'alice', 'hello'); expect(writes).toEqual(['hello']); expect(receipt.sha256).toBe(sha('hello')); expect(JSON.stringify(receipt)).not.toContain('hello'); await store.submit(target, 'alice', receipt.stageId, receipt.sha256); expect(writes).toEqual(['hello', '\r']); await expect(store.submit(target, 'alice', receipt.stageId, receipt.sha256)).rejects.toThrow(); }); it('refuses multiline, controls, oversized prompts and nonempty or unprovable composers before writing', async () => { for (const text of ['', 'one\ntwo', 'one\rtwo', '\u001b', '\u0085', 'x'.repeat(20000), '\ud800']) await expect(store.stage(target, 'alice', text)).rejects.toThrow(); composer = 'existing'; await expect(store.stage(target, 'alice', 'hello')).rejects.toThrow(); composer = null; await expect(store.stage(target, 'alice', 'hello')).rejects.toThrow(); expect(writes).toEqual([]); }); it('refuses altered composer including whitespace-only differences', async () => { const receipt = await store.stage(target, 'alice', 'hello world'); composer = 'helloworld'; await expect(store.submit(target, 'alice', receipt.stageId, receipt.sha256)).rejects.toThrow(); expect(writes).toEqual(['hello world']); }); it('refuses changed generation, intervening input, wrong owner/session/hash and expiry', async () => { for (const change of [ () => { generation = 'pane-2'; }, () => { revision++; }, () => { time += 1001; }, ]) { composer = ''; const receipt = await store.stage(target, 'alice', 'hello'); change(); await expect(store.submit(target, 'alice', receipt.stageId, receipt.sha256)).rejects.toThrow(); } composer = ''; const receipt = await store.stage(target, 'alice', 'hello'); await expect(store.submit(target, 'bob', receipt.stageId, receipt.sha256)).rejects.toThrow(); await expect(store.submit({ ...target, id: 'other' }, 'alice', receipt.stageId, receipt.sha256)).rejects.toThrow(); await expect(store.submit(target, 'alice', receipt.stageId, sha('other'))).rejects.toThrow(); expect(writes.filter((s) => s === '\r')).toHaveLength(0); }); it('consumes before write so concurrent and failed deliveries never replay Enter', async () => { const receipt = await store.stage(target, 'alice', 'hello'); fail = true; const results = await Promise.allSettled([ store.submit(target, 'alice', receipt.stageId, receipt.sha256), store.submit(target, 'alice', receipt.stageId, receipt.sha256), ]); expect(results.every((r) => r.status === 'rejected')).toBe(true); expect(writes).toEqual(['hello', '\r']); await expect(store.submit(target, 'alice', receipt.stageId, receipt.sha256)).rejects.toThrow(); }); it('does not issue a receipt on failed staging or intervening input during its write', async () => { fail = true; await expect(store.stage(target, 'alice', 'hello')).rejects.toThrow(); fail = false; composer = ''; const altered = { ...target, write: async (s: string) => { await target.write(s); revision++; return true; }, }; await expect(store.stage(altered, 'alice', 'hello')).rejects.toThrow(); }); it('reads exact composer spaces and refuses wrapped or modal frames', async () => { const { readGuardedCodexComposer } = await import('../src/agent-staging.js'); expect(readGuardedCodexComposer('› hello world\n GPT-6-Luna default · ~/case\n')).toBe('hello world'); expect( readGuardedCodexComposer( '› \x1b[2mAsk Codex to do anything\x1b[0m\n GPT-6-Luna default · ~/case\n ← for agents · ? for shortcuts' ) ).toBe(''); expect(readGuardedCodexComposer('› wrapped\n more text\n GPT-6-Luna default · ~/case')).toBeNull(); expect(readGuardedCodexComposer('› 1. Yes\n Press enter to confirm')).toBeNull(); expect(readGuardedCodexComposer('› hello\n')).toBeNull(); }); it('requires capture cursor geometry, rejecting invisible trailing edits and moved cursors', async () => { const { readGuardedCodexSnapshot } = await import('../src/agent-staging.js'); const { formatPaneSnapshot } = await import('../src/tmux-manager.js'); const frame = (text: string, x: number, y = 0) => formatPaneSnapshot([`› ${text}`, ' GPT-6-Luna default · ~/case'], { cols: 100, rows: 20, cursorX: x, cursorY: y }); expect(readGuardedCodexSnapshot(frame('hello', 7))).toBe('hello'); expect(readGuardedCodexSnapshot(frame('hello', 8))).toBeNull(); expect(readGuardedCodexSnapshot(frame('hello', 4))).toBeNull(); expect(readGuardedCodexSnapshot(frame('hello', 7, 1))).toBeNull(); expect(readGuardedCodexSnapshot('› hello\n GPT-6-Luna default · ~/case')).toBeNull(); expect(readGuardedCodexSnapshot(frame('你好', 6))).toBe('你好'); expect(readGuardedCodexSnapshot(frame('\x1b[38;2;2;100;100mhello\x1b[0m', 7))).toBe('hello'); }); it('reserves receipt capacity before concurrent stages type', async () => { store = new GuardedStageStore(() => time, 1, 1000); let release: (() => void) | undefined; const firstTarget = { ...target, write: async (text: string) => { await new Promise((resolve) => { release = resolve; }); return target.write(text); }, }; const first = store.stage(firstTarget, 'alice', 'hello'); const otherTarget = { ...target, id: 'other' }; await expect(store.stage(otherTarget, 'alice', 'other')).rejects.toThrow(/capacity/i); expect(writes).toEqual([]); release!(); await first; }); it.each([...CODEX_REASONING_EFFORTS, 'default'])( 'accepts the shared Codex effort word %s in the footer', async (word) => { const { readGuardedCodexComposer } = await import('../src/agent-staging.js'); expect(readGuardedCodexComposer(`› hello\n GPT-6 ${word} · ~/case\n`)).toBe('hello'); } ); it('refuses messages that reach the available pane columns before typing', async () => { target.snapshot = () => ({ generation, revision, composerSha: sha(composer ?? ''), availableColumns: 4 }); for (const text of ['four', '界界']) await expect(store.stage(target, 'alice', text)).rejects.toThrow(); expect(writes).toEqual([]); await expect(store.stage(target, 'alice', 'abc')).resolves.toMatchObject({ submitted: false }); });