mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
With a real codex login now available, record the one shape the fake-key lab could never produce: a genuine model reply streaming above the pinned composer, pushing lines into history (baseY grows) while keystrokes land mid-stream. The recorder gains an opt-in CODEX_RECORD_REAL=1 scenario using the user's own ~/.codex (fixture secret-scanned for key/JWT material before writing; scanned clean). The replay test pins: baseY > 0, mid-stream predictions painted, exact convergence to the typed text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
189 lines
7.3 KiB
TypeScript
189 lines
7.3 KiB
TypeScript
/**
|
||
* @vitest-environment jsdom
|
||
*
|
||
* Layer 2 (the load-bearing suite): the REAL algorithm against the REAL xterm
|
||
* parser, fed by fixtures recorded from real codex 0.147 through the
|
||
* production pipeline (tmux + the codex full strip). See
|
||
* scripts/dev/record-codex-frames.mjs in the consuming repo.
|
||
*
|
||
* Every replay ends with the convergence invariant: predictions never outlive
|
||
* their run (outstanding 0, span container empty).
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { PredictiveEchoAddon } from '../src/predictive-echo-addon.js';
|
||
import {
|
||
CELL_H,
|
||
CELL_W,
|
||
classifyPredictInput,
|
||
codexComposerGate,
|
||
createReplayTerminal,
|
||
loadFixture,
|
||
type ReplayTerminal,
|
||
} from './replay-helpers.js';
|
||
|
||
async function flushMicrotasks() {
|
||
await Promise.resolve();
|
||
await Promise.resolve();
|
||
}
|
||
|
||
function sleep(ms: number) {
|
||
return new Promise((r) => setTimeout(r, ms));
|
||
}
|
||
|
||
interface KeyEvent {
|
||
key: string;
|
||
kind: ReturnType<typeof classifyPredictInput>;
|
||
painted: boolean;
|
||
spansAfter: number;
|
||
}
|
||
|
||
function assertSpansInGrid(rt: ReplayTerminal) {
|
||
for (const s of rt.spans()) {
|
||
const left = parseFloat(s.style.left);
|
||
const width = parseFloat(s.style.width);
|
||
const top = parseFloat(s.style.top);
|
||
expect(left + width).toBeLessThanOrEqual(rt.hybrid.cols * CELL_W);
|
||
expect(top).toBeLessThanOrEqual((rt.hybrid.rows - 1) * CELL_H);
|
||
expect(left).toBeGreaterThanOrEqual(0);
|
||
expect(top).toBeGreaterThanOrEqual(0);
|
||
}
|
||
}
|
||
|
||
async function replay(name: string) {
|
||
const { meta, lines } = loadFixture(name);
|
||
const rt = createReplayTerminal(meta.cols, meta.rows);
|
||
const addon = new PredictiveEchoAddon({ predictWhen: codexComposerGate });
|
||
addon.activate(rt.hybrid);
|
||
|
||
const events: KeyEvent[] = [];
|
||
for (const line of lines) {
|
||
if (line.keyAt) {
|
||
const kind = classifyPredictInput(line.data);
|
||
let painted = false;
|
||
if (kind === 'char') painted = addon.predictChar(line.data);
|
||
else if (kind === 'backspace') addon.predictBackspace();
|
||
else addon.clearPredictions(); // 'clear' AND 'text', like the terminal-ui hook
|
||
// Span/record parity and grid bounds hold at every step
|
||
expect(rt.spanCount()).toBe(addon.state.outstanding);
|
||
assertSpansInGrid(rt);
|
||
events.push({ key: line.data, kind, painted, spansAfter: rt.spanCount() });
|
||
} else {
|
||
await rt.write(line.data);
|
||
await flushMicrotasks();
|
||
}
|
||
}
|
||
return { rt, addon, events, meta };
|
||
}
|
||
|
||
/** Convergence invariant: after the last chunk + reconcile (+ TTL if needed),
|
||
* nothing outlives the run. */
|
||
async function converge(rt: ReplayTerminal, addon: PredictiveEchoAddon) {
|
||
addon.reconcile();
|
||
if (addon.state.outstanding > 0) {
|
||
await sleep(1100); // ttlMs default
|
||
addon.reconcile();
|
||
}
|
||
expect(addon.state.outstanding).toBe(0);
|
||
expect(rt.spanCount()).toBe(0);
|
||
}
|
||
|
||
describe('codex replay', () => {
|
||
it('type-hello: all 5 predictions confirm, zero drops, composer converges', async () => {
|
||
const { rt, addon, events } = await replay('type-hello');
|
||
const chars = events.filter((e) => e.kind === 'char');
|
||
expect(chars).toHaveLength(5);
|
||
expect(chars.every((e) => e.painted)).toBe(true);
|
||
await converge(rt, addon);
|
||
expect(addon.state.confirmedTotal).toBe(5);
|
||
expect(addon.state.droppedTotal).toBe(0);
|
||
expect(rt.cursorRowText()).toBe('› hello');
|
||
addon.dispose();
|
||
rt.cleanup();
|
||
}, 15000);
|
||
|
||
it('slash-picker: "/" and filter chars confirm; no ghosts while picker rows redraw', async () => {
|
||
const { rt, addon, events } = await replay('slash-picker');
|
||
const chars = events.filter((e) => e.kind === 'char');
|
||
expect(chars.map((e) => e.key)).toEqual(['/', 'm', 'o']);
|
||
expect(chars.every((e) => e.painted)).toBe(true);
|
||
await converge(rt, addon);
|
||
expect(addon.state.confirmedTotal).toBe(3);
|
||
expect(addon.state.droppedTotal).toBe(0);
|
||
addon.dispose();
|
||
rt.cleanup();
|
||
}, 15000);
|
||
|
||
it('wrap: predictions stay inside the grid, continuation rows fall back to real echo, buffer converges', async () => {
|
||
const { rt, addon, events } = await replay('wrap');
|
||
// The gate goes false once the cursor is on a wrapped continuation row
|
||
// (2-space indent, no "› "): a tail of keystrokes must be suppressed.
|
||
const chars = events.filter((e) => e.kind === 'char');
|
||
expect(chars.some((e) => !e.painted)).toBe(true);
|
||
expect(chars.some((e) => e.painted)).toBe(true);
|
||
await converge(rt, addon);
|
||
// The composer content is exactly what was typed (word-wrapped)
|
||
const b = rt.term.buffer.active;
|
||
const cursorRow = b.cursorY;
|
||
expect(rt.rowText(cursorRow).trim()).toBe('this line twice over');
|
||
expect(rt.rowText(cursorRow - 1)).toMatch(/^› the quick brown fox/);
|
||
addon.dispose();
|
||
rt.cleanup();
|
||
}, 15000);
|
||
|
||
it('streaming-burst: typed predictions confirm; the re-rendered composer keeps its signature', async () => {
|
||
const { rt, addon, events } = await replay('streaming-burst');
|
||
const chars = events.filter((e) => e.kind === 'char');
|
||
expect(chars).toHaveLength(5); // "hello" (the \r is kind 'clear')
|
||
await converge(rt, addon);
|
||
expect(addon.state.confirmedTotal).toBe(5);
|
||
expect(addon.state.droppedTotal).toBe(0);
|
||
// After the 401 burst codex re-renders a fresh composer at the cursor
|
||
expect(rt.cursorRowText()).toMatch(/^› /);
|
||
addon.dispose();
|
||
rt.cleanup();
|
||
}, 15000);
|
||
|
||
it('streaming-real: mid-stream typing survives real baseY growth (recorded with real auth)', async () => {
|
||
// The one shape the fake-key lab cannot produce: a genuine model reply
|
||
// streaming above the pinned composer pushes lines into history, so
|
||
// baseY GROWS while predictions are outstanding: the no-drop-on-baseY
|
||
// rule against reality instead of a synthetic scroll.
|
||
const { rt, addon, events } = await replay('streaming-real');
|
||
expect(rt.term.buffer.active.baseY).toBeGreaterThan(0); // history really grew
|
||
const midStream = events.filter((e) => e.kind === 'char' && ['a', 'b', 'c'].includes(e.key));
|
||
expect(midStream.length).toBe(3);
|
||
expect(midStream.some((e) => e.painted)).toBe(true); // predictions ran mid-stream
|
||
await converge(rt, addon);
|
||
expect(rt.cursorRowText()).toBe('› abc'); // the mid-stream chars landed intact
|
||
addon.dispose();
|
||
rt.cleanup();
|
||
}, 15000);
|
||
|
||
it('paste-bracketed: typed chars confirm, the paste clears predictions, content intact', async () => {
|
||
const { rt, addon, events } = await replay('paste-bracketed');
|
||
const paste = events.find((e) => e.key.startsWith('\x1b[200~'))!;
|
||
expect(paste.kind).toBe('clear');
|
||
expect(paste.spansAfter).toBe(0);
|
||
await converge(rt, addon);
|
||
expect(addon.state.confirmedTotal).toBe(2); // 'a', 'b'
|
||
expect(rt.cursorRowText()).toContain('abXYZpasted');
|
||
addon.dispose();
|
||
rt.cleanup();
|
||
}, 15000);
|
||
|
||
it('trust-modal: the predictWhen gate paints ZERO spans on the modal (ghost eliminator)', async () => {
|
||
const { rt, addon, events } = await replay('trust-modal');
|
||
const x = events.find((e) => e.key === 'x')!;
|
||
expect(x.painted).toBe(false);
|
||
expect(x.spansAfter).toBe(0);
|
||
expect(events.every((e) => e.spansAfter === 0)).toBe(true);
|
||
await converge(rt, addon);
|
||
expect(addon.state.confirmedTotal).toBe(0);
|
||
expect(addon.state.droppedTotal).toBe(0);
|
||
// The transition landed on the real composer afterwards
|
||
expect(rt.cursorRowText()).toMatch(/^› /);
|
||
addon.dispose();
|
||
rt.cleanup();
|
||
}, 15000);
|
||
});
|