mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
Use raw writes to stdin in test (#11871)
This commit is contained in:
committed by
GitHub
parent
b16fe7b646
commit
0fe82a2f4e
@@ -33,6 +33,9 @@ vi.mock('ink', async (importOriginal) => {
|
||||
|
||||
const PASTE_START = '\x1B[200~';
|
||||
const PASTE_END = '\x1B[201~';
|
||||
// readline will not emit most incomplete kitty sequences but it will give
|
||||
// up on sequences like this where the modifier (135) has more than two digits.
|
||||
const INCOMPLETE_KITTY_SEQUENCE = '\x1b[97;135';
|
||||
|
||||
class MockStdin extends EventEmitter {
|
||||
isTTY = true;
|
||||
@@ -45,15 +48,6 @@ class MockStdin extends EventEmitter {
|
||||
write(text: string) {
|
||||
this.emit('data', Buffer.from(text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to directly simulate keyPress events. Certain keypress events might
|
||||
* be impossible to fire in certain versions of node. This allows us to
|
||||
* sidestep readline entirely and just emit the keypress we want.
|
||||
*/
|
||||
pressKey(key: Partial<Key>) {
|
||||
this.emit('keypress', null, key);
|
||||
}
|
||||
}
|
||||
|
||||
describe('KeypressContext - Kitty Protocol', () => {
|
||||
@@ -171,9 +165,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send kitty protocol sequence for numpad enter with Ctrl (modifier 5): ESC[57414;5u
|
||||
act(() => {
|
||||
stdin.write(`\x1b[57414;5u`);
|
||||
});
|
||||
act(() => stdin.write(`\x1b[57414;5u`));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -506,15 +498,14 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1' }));
|
||||
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||
|
||||
// Send Ctrl+C
|
||||
act(() => stdin.write('\x03'));
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
|
||||
'\x1b[1',
|
||||
INCOMPLETE_KITTY_SEQUENCE,
|
||||
);
|
||||
|
||||
// Verify Ctrl+C was handled
|
||||
@@ -543,19 +534,18 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
const sequence = '\x1b[12';
|
||||
act(() => stdin.pressKey({ sequence }));
|
||||
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||
|
||||
// Verify debug logging for accumulation
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[DEBUG] Kitty buffer accumulating:',
|
||||
JSON.stringify(sequence),
|
||||
JSON.stringify(INCOMPLETE_KITTY_SEQUENCE),
|
||||
);
|
||||
|
||||
// Verify warning for char codes
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'Kitty sequence buffer has content:',
|
||||
JSON.stringify(sequence),
|
||||
JSON.stringify(INCOMPLETE_KITTY_SEQUENCE),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -829,93 +819,75 @@ describe('Kitty Sequence Parsing', () => {
|
||||
// Terminals to test
|
||||
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
|
||||
|
||||
// Key mappings: letter -> [keycode, accented character, shouldHaveMeta]
|
||||
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode
|
||||
const keys: Record<string, [number, string, boolean]> = {
|
||||
a: [97, 'å', true],
|
||||
o: [111, 'ø', true],
|
||||
m: [109, 'µ', false],
|
||||
// Key mappings: letter -> [keycode, accented character]
|
||||
const keys: Record<string, [number, string]> = {
|
||||
a: [97, 'å'],
|
||||
o: [111, 'ø'],
|
||||
m: [109, 'µ'],
|
||||
};
|
||||
|
||||
it.each(
|
||||
terminals.flatMap((terminal) =>
|
||||
Object.entries(keys).map(
|
||||
([key, [keycode, accentedChar, shouldHaveMeta]]) => {
|
||||
if (terminal === 'Ghostty') {
|
||||
// Ghostty uses kitty protocol sequences
|
||||
return {
|
||||
terminal,
|
||||
key,
|
||||
kittySequence: `\x1b[${keycode};3u`,
|
||||
expected: {
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true,
|
||||
shift: false,
|
||||
paste: false,
|
||||
kittyProtocol: true,
|
||||
},
|
||||
};
|
||||
} else if (terminal === 'MacTerminal') {
|
||||
// Mac Terminal sends ESC + letter
|
||||
return {
|
||||
terminal,
|
||||
key,
|
||||
kitty: false,
|
||||
input: {
|
||||
sequence: `\x1b${key}`,
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true,
|
||||
shift: false,
|
||||
paste: false,
|
||||
},
|
||||
expected: {
|
||||
sequence: `\x1b${key}`,
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true,
|
||||
shift: false,
|
||||
paste: false,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// iTerm2 and VSCode send accented characters (å, ø, µ)
|
||||
// Note: µ comes with meta:false but gets converted to m with meta:true
|
||||
return {
|
||||
terminal,
|
||||
key,
|
||||
input: {
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: shouldHaveMeta,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: accentedChar,
|
||||
},
|
||||
expected: {
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true, // Always expect meta:true after conversion
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: accentedChar,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
),
|
||||
Object.entries(keys).map(([key, [keycode, accentedChar]]) => {
|
||||
if (terminal === 'Ghostty') {
|
||||
// Ghostty uses kitty protocol sequences
|
||||
return {
|
||||
terminal,
|
||||
key,
|
||||
chunk: `\x1b[${keycode};3u`,
|
||||
expected: {
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true,
|
||||
shift: false,
|
||||
paste: false,
|
||||
kittyProtocol: true,
|
||||
},
|
||||
};
|
||||
} else if (terminal === 'MacTerminal') {
|
||||
// Mac Terminal sends ESC + letter
|
||||
return {
|
||||
terminal,
|
||||
key,
|
||||
kitty: false,
|
||||
chunk: `\x1b${key}`,
|
||||
expected: {
|
||||
sequence: `\x1b${key}`,
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true,
|
||||
shift: false,
|
||||
paste: false,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// iTerm2 and VSCode send accented characters (å, ø, µ)
|
||||
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode but
|
||||
// gets converted to m with meta:true
|
||||
return {
|
||||
terminal,
|
||||
key,
|
||||
chunk: accentedChar,
|
||||
expected: {
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true, // Always expect meta:true after conversion
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: accentedChar,
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
),
|
||||
)(
|
||||
'should handle Alt+$key in $terminal',
|
||||
({
|
||||
kittySequence,
|
||||
input,
|
||||
chunk,
|
||||
expected,
|
||||
kitty = true,
|
||||
}: {
|
||||
kittySequence?: string;
|
||||
input?: Partial<Key>;
|
||||
chunk: string;
|
||||
expected: Partial<Key>;
|
||||
kitty?: boolean;
|
||||
}) => {
|
||||
@@ -930,11 +902,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
});
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
if (kittySequence) {
|
||||
act(() => stdin.write(kittySequence));
|
||||
} else if (input) {
|
||||
act(() => stdin.pressKey(input));
|
||||
}
|
||||
act(() => stdin.write(chunk));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining(expected),
|
||||
@@ -978,8 +946,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||
|
||||
// Should not broadcast immediately
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
@@ -991,15 +958,13 @@ describe('Kitty Sequence Parsing', () => {
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Advance past timeout
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(10);
|
||||
});
|
||||
act(() => vi.advanceTimersByTime(10));
|
||||
|
||||
// Should now broadcast the incomplete sequence as regular input
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: '',
|
||||
sequence: '\x1b[1;',
|
||||
sequence: INCOMPLETE_KITTY_SEQUENCE,
|
||||
paste: false,
|
||||
}),
|
||||
);
|
||||
@@ -1079,8 +1044,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||
|
||||
// Press Ctrl+C
|
||||
act(() => stdin.write('\x03'));
|
||||
@@ -1189,13 +1153,13 @@ describe('Kitty Sequence Parsing', () => {
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Start incomplete sequence
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1' }));
|
||||
act(() => stdin.write('\x1b[97;13'));
|
||||
|
||||
// Advance time partway
|
||||
act(() => vi.advanceTimersByTime(30));
|
||||
|
||||
// Add more to sequence
|
||||
act(() => stdin.write('3'));
|
||||
act(() => stdin.write('5'));
|
||||
|
||||
// Advance time from the first timeout point
|
||||
act(() => vi.advanceTimersByTime(25));
|
||||
@@ -1209,7 +1173,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
// Should now parse as complete enter key
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'return',
|
||||
name: 'a',
|
||||
kittyProtocol: true,
|
||||
}),
|
||||
);
|
||||
@@ -1221,8 +1185,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||
|
||||
// Incomplete sequence should be buffered, not broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
@@ -1235,7 +1198,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: '',
|
||||
sequence: '\x1b[1;',
|
||||
sequence: INCOMPLETE_KITTY_SEQUENCE,
|
||||
paste: false,
|
||||
}),
|
||||
);
|
||||
@@ -1247,8 +1210,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||
|
||||
// Incomplete sequence should be buffered, not broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
@@ -1261,7 +1223,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: '',
|
||||
sequence: '\x1b[1;',
|
||||
sequence: INCOMPLETE_KITTY_SEQUENCE,
|
||||
paste: false,
|
||||
}),
|
||||
);
|
||||
@@ -1274,8 +1236,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Send incomplete kitty sequence
|
||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
||||
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||
|
||||
// Incomplete sequence should be buffered, not broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
@@ -1288,7 +1249,7 @@ describe('Kitty Sequence Parsing', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: '',
|
||||
sequence: '\x1b[1;',
|
||||
sequence: INCOMPLETE_KITTY_SEQUENCE,
|
||||
paste: false,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user