2026-06-10 13:18:45 -04:00
import { describe , expect , it } from 'vitest' ;
import { Session } from '../src/session.js' ;
type SessionInternals = {
_handleTerminalOutput ( data : string ) : void ;
_ptyRows : number ;
};
function handleOutput ( session : Session , data : string ) : void {
( session as unknown as SessionInternals ). _handleTerminalOutput ( data );
}
describe ( 'Codex terminal output filtering' , () => {
it ( 'keeps browser scrollback guards but skips Codeman row repair in hybrid render mode' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' , codexConfig : { renderMode : 'hybrid' } });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
const hybridRedraw = '\x1b[?1049h\x1b[55;1H\x1b[2m• Working (21s)\x1b[3J\x1b[?1006h\x1b[?1049l' ;
handleOutput ( session , hybridRedraw );
expect ( emitted [ 0 ]). toBe ( '\x1b[55;1H\x1b[2m• Working (21s)' );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[55;1H\x1b[2K' );
expect ( session . terminalBuffer ). toBe ( emitted [ 0 ]);
});
it ( 'preserves Codex erase-display redraws used by the TUI layout engine' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
handleOutput ( session , '\x1b[H\x1b[Jidle redraw' );
expect ( session . terminalBuffer ). toBe ( '\x1b[H\x1b[Jidle redraw' );
});
it ( 'strips Codex scrollback erase without stripping visible-screen erase' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
handleOutput ( session , '\x1b[?1049h\x1b[2Jvisible\x1b[3Jscrollback\x1b[?1049l' );
expect ( session . terminalBuffer ). toBe ( '\x1b[2Jvisiblescrollback' );
});
2026-06-10 22:47:02 +02:00
it ( 'strips sequences split across PTY chunk boundaries (carry reassembly)' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
// '\x1b[?1049h' split mid-sequence, then '\x1b[3J' split before its final byte.
handleOutput ( session , 'before\x1b[?104' );
handleOutput ( session , '9h\x1b[2Jafter\x1b[3' );
handleOutput ( session , 'Jtail' );
expect ( session . terminalBuffer ). toBe ( 'before\x1b[2Jaftertail' );
expect ( emitted ). toEqual ([ 'before' , '\x1b[2Jafter' , 'tail' ]);
});
it ( 'emits nothing for a chunk that is only a partial CSI, and completes it next chunk' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
handleOutput ( session , '\x1b[?100' ); // pure partial — held, nothing emitted
handleOutput ( session , '6h\x1b[55;1H• Working' ); // completes ?1006h (stripped); rest passes
expect ( emitted ). toEqual ([ '\x1b[55;1H• Working' ]);
expect ( session . terminalBuffer ). toBe ( '\x1b[55;1H• Working' );
});
2026-06-10 13:18:45 -04:00
it ( 'preserves Codex erase-display redraw when the user pressed Ctrl+L' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
session . write ( '\x0c' );
handleOutput ( session , '\x1b[H\x1b[Jredraw after clear' );
expect ( session . terminalBuffer ). toBe ( '\x1b[H\x1b[Jredraw after clear' );
});
it ( 'passes native Codex TUI prompt/status redraws through without row repair' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
const bottomBandRedraw =
'\x1b[48;2;42;42;42m' +
'\x1b[60;2H\x1b[K' +
'\x1b[61;39H\x1b[K' +
'\x1b[62;2H\x1b[K' +
'\x1b[52;1H\x1b[49m\x1b[2m• \x1b[1mRunning node -e ...' +
'\x1b[60;1H\x1b[48;2;42;42;42m \r\n' +
'\x1b[1m› \x1b[0m\x1b[48;2;42;42;42m \x1b[2mUse /skills to list available skills\r\n' +
'\x1b[63;3H\x1b[49m\x1b[38;2;246;226;183mgpt-5.5 xhigh\x1b[39m' +
'\x1b[2m · \x1b[38;2;242;181;144mContext 42% left\x1b[39m' +
'\x1b[61;3H' ;
handleOutput ( session , bottomBandRedraw );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[52;1H\x1b[2K' );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[60;1H\x1b[2K' );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[63;1H\x1b[2K' );
expect ( emitted [ 0 ]). toContain ( bottomBandRedraw );
});
it ( 'passes Codex advisory rows through without row repair' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
const advisoryRedraw =
'\x1b[55;1H\x1b[2mMessages\x1b[Cto\x1b[Cbe submitted\x1b[Cafter\x1b[Cnext toolcall ' +
'(press esc to interrupt and send immediately)\x1b[56;1H' ;
handleOutput ( session , advisoryRedraw );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[55;1H\x1b[2K' );
expect ( emitted [ 0 ]). toContain ( advisoryRedraw );
});
it ( 'does not clear Codex resume-picker rows just because an option is selected' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
const resumePickerRedraw =
'\x1b[1;2H\x1b[36m\x1b[1mResume a previous session' +
'\x1b[3;2H\x1b[2mType to search Filter: \x1b[35m[Cwd]\x1b[39m\x1b[2m All' +
'\x1b[5;3H\x1b[33m\x1b[48;2;42;42;42m\x1b[1m❯ \x1b[2m22h ago ll' +
'\x1b[6;3H\x1b[2m 1d ago $kb-health' +
'\x1b[60;1H\x1b[2m──── 2 / 2 · 100% ─' +
'\x1b[61;1H enter resume esc exit ↑/↓ browse' ;
handleOutput ( session , resumePickerRedraw );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[4;1H\x1b[2K' );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[5;1H\x1b[2K' );
expect ( emitted [ 0 ]). toContain ( resumePickerRedraw );
});
it ( 'does not full-clear sparse Codex resume-picker navigation redraws' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
let sparseResumePickerRedraw = '' ;
for ( let row = 1 ; row <= 51 ; row ++ ) {
const col = row % 2 === 0 ? 239 : 27 ;
sparseResumePickerRedraw += ` \ x1b[ ${ row } ; ${ col } H \ x1b[K` ;
}
sparseResumePickerRedraw +=
'\x1b[21;3H \x1b[2m9d ago \x1b[mreview this webex room webexteams://im?space=672465b0-4fcb-11f1-9d54-51475df86e3a\x1b[K' +
'\x1b[22;3H\x1b[33m\x1b[1m❯ \x1b[m\x1b[33m\x1b[2m9d ago \x1b[m\x1b[33mcisco hybrid mesh firewall includes support for smart switch enforcement...\x1b[K' +
'\x1b[52;229H\x1b[39m\x1b[2m8\x1b[m' ;
handleOutput ( session , sparseResumePickerRedraw );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[H\x1b[2J' );
expect ( emitted [ 0 ]). toContain ( sparseResumePickerRedraw );
});
it ( 'passes Codex UI rows through when the status band moves downward' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
handleOutput (
session ,
'\x1b[55;1H\x1b[2m• Working (1s)\x1b[56;1H\x1b[1m› \x1b[0m ask\x1b[57;3Hgpt-5.5 · Context 80% left'
);
handleOutput (
session ,
'\x1b[58;1H\x1b[2m• Working (2s)\x1b[59;1H\x1b[1m› \x1b[0m ask\x1b[60;3Hgpt-5.5 · Context 79% left'
);
expect ( emitted [ 1 ]). not . toContain ( '\x1b[55;1H\x1b[2K' );
expect ( emitted [ 1 ]). not . toContain ( '\x1b[56;1H\x1b[2K' );
expect ( emitted [ 1 ]). not . toContain ( '\x1b[57;1H\x1b[2K' );
expect ( emitted [ 1 ]). toContain ( '\x1b[58;1H' );
expect ( emitted [ 1 ]). not . toContain ( '\x1b[54;1H\x1b[2K' );
});
it ( 'does not full-clear the viewport for stable Codex UI rows at the same position' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
const stableRedraw =
'\x1b[58;1H\x1b[2m• Working (2s)\x1b[59;1H\x1b[1m› \x1b[0m ask\x1b[60;3Hgpt-5.5 · Context 79% left' ;
handleOutput ( session , stableRedraw );
handleOutput ( session , stableRedraw . replace ( '2s' , '3s' ));
expect ( emitted [ 1 ]). not . toContain ( '\x1b[H\x1b[2J' );
});
it ( 'passes status-only Codex Working redraw rows through' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
const workingRedraw = '\x1b[55;1H\x1b[2m• Working (21s)' ;
handleOutput ( session , workingRedraw );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[55;1H\x1b[2K' );
expect ( emitted [ 0 ]). toContain ( workingRedraw );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[H\x1b[2J' );
});
it ( 'passes Codex spinner Working rows that omit elapsed time parentheses through' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 29 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
const spinnerRedraw = '\x1b[24;1H\x1b[38;5;254m\x1b[1m•\x1b[CWorking\x1b[27;3H' ;
handleOutput ( session , spinnerRedraw );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[24;1H\x1b[2K' );
expect ( emitted [ 0 ]). toContain ( spinnerRedraw );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[H\x1b[2J' );
});
it ( 'does not treat ordinary gpt model mentions as Codex status rows' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
const outputRow = '\x1b[20;1Hnormal output comparing gpt-5 and another model' ;
handleOutput ( session , outputRow );
expect ( emitted [ 0 ]). toContain ( outputRow );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[19;1H\x1b[2K' );
expect ( emitted [ 0 ]). not . toContain ( '\x1b[20;1H\x1b[2K' );
});
it ( 'does not inject row erases during partial Working spinner ticks' , () => {
const session = new Session ({ workingDir : '/tmp' , mode : 'codex' });
( session as unknown as SessionInternals ). _ptyRows = 63 ;
const emitted : string [] = [];
session . on ( 'terminal' , ( data ) => emitted . push ( data ));
handleOutput (
session ,
'\x1b[55;1H\x1b[2m• Working (1s)' +
'\x1b[56;1H\x1b[1m› \x1b[0m ask' +
'\x1b[57;3Hgpt-5.5 xhigh fast · codeman · Working · Context 79% left'
);
handleOutput ( session , '\x1b[55;1H\x1b[2m• Working (2s)' );
expect ( emitted [ 1 ]). not . toContain ( '\x1b[55;1H\x1b[2K' );
expect ( emitted [ 1 ]). not . toContain ( '\x1b[56;1H\x1b[2K' );
expect ( emitted [ 1 ]). not . toContain ( '\x1b[57;1H\x1b[2K' );
expect ( emitted [ 1 ]). toContain ( '\x1b[55;1H\x1b[2m• Working (2s)' );
});
});