mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-27 05:24:34 -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_START = '\x1B[200~';
|
||||||
const PASTE_END = '\x1B[201~';
|
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 {
|
class MockStdin extends EventEmitter {
|
||||||
isTTY = true;
|
isTTY = true;
|
||||||
@@ -45,15 +48,6 @@ class MockStdin extends EventEmitter {
|
|||||||
write(text: string) {
|
write(text: string) {
|
||||||
this.emit('data', Buffer.from(text));
|
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', () => {
|
describe('KeypressContext - Kitty Protocol', () => {
|
||||||
@@ -171,9 +165,7 @@ describe('KeypressContext - Kitty Protocol', () => {
|
|||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
// Send kitty protocol sequence for numpad enter with Ctrl (modifier 5): ESC[57414;5u
|
// Send kitty protocol sequence for numpad enter with Ctrl (modifier 5): ESC[57414;5u
|
||||||
act(() => {
|
act(() => stdin.write(`\x1b[57414;5u`));
|
||||||
stdin.write(`\x1b[57414;5u`);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(keyHandler).toHaveBeenCalledWith(
|
expect(keyHandler).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -506,15 +498,14 @@ describe('KeypressContext - Kitty Protocol', () => {
|
|||||||
|
|
||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
// Send incomplete kitty sequence
|
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||||
act(() => stdin.pressKey({ sequence: '\x1b[1' }));
|
|
||||||
|
|
||||||
// Send Ctrl+C
|
// Send Ctrl+C
|
||||||
act(() => stdin.write('\x03'));
|
act(() => stdin.write('\x03'));
|
||||||
|
|
||||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
|
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
|
||||||
'\x1b[1',
|
INCOMPLETE_KITTY_SEQUENCE,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify Ctrl+C was handled
|
// Verify Ctrl+C was handled
|
||||||
@@ -543,19 +534,18 @@ describe('KeypressContext - Kitty Protocol', () => {
|
|||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
// Send incomplete kitty sequence
|
// Send incomplete kitty sequence
|
||||||
const sequence = '\x1b[12';
|
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||||
act(() => stdin.pressKey({ sequence }));
|
|
||||||
|
|
||||||
// Verify debug logging for accumulation
|
// Verify debug logging for accumulation
|
||||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
'[DEBUG] Kitty buffer accumulating:',
|
'[DEBUG] Kitty buffer accumulating:',
|
||||||
JSON.stringify(sequence),
|
JSON.stringify(INCOMPLETE_KITTY_SEQUENCE),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify warning for char codes
|
// Verify warning for char codes
|
||||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||||
'Kitty sequence buffer has content:',
|
'Kitty sequence buffer has content:',
|
||||||
JSON.stringify(sequence),
|
JSON.stringify(INCOMPLETE_KITTY_SEQUENCE),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -829,93 +819,75 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
// Terminals to test
|
// Terminals to test
|
||||||
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
|
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
|
||||||
|
|
||||||
// Key mappings: letter -> [keycode, accented character, shouldHaveMeta]
|
// Key mappings: letter -> [keycode, accented character]
|
||||||
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode
|
const keys: Record<string, [number, string]> = {
|
||||||
const keys: Record<string, [number, string, boolean]> = {
|
a: [97, 'å'],
|
||||||
a: [97, 'å', true],
|
o: [111, 'ø'],
|
||||||
o: [111, 'ø', true],
|
m: [109, 'µ'],
|
||||||
m: [109, 'µ', false],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it.each(
|
it.each(
|
||||||
terminals.flatMap((terminal) =>
|
terminals.flatMap((terminal) =>
|
||||||
Object.entries(keys).map(
|
Object.entries(keys).map(([key, [keycode, accentedChar]]) => {
|
||||||
([key, [keycode, accentedChar, shouldHaveMeta]]) => {
|
if (terminal === 'Ghostty') {
|
||||||
if (terminal === 'Ghostty') {
|
// Ghostty uses kitty protocol sequences
|
||||||
// Ghostty uses kitty protocol sequences
|
return {
|
||||||
return {
|
terminal,
|
||||||
terminal,
|
key,
|
||||||
key,
|
chunk: `\x1b[${keycode};3u`,
|
||||||
kittySequence: `\x1b[${keycode};3u`,
|
expected: {
|
||||||
expected: {
|
name: key,
|
||||||
name: key,
|
ctrl: false,
|
||||||
ctrl: false,
|
meta: true,
|
||||||
meta: true,
|
shift: false,
|
||||||
shift: false,
|
paste: false,
|
||||||
paste: false,
|
kittyProtocol: true,
|
||||||
kittyProtocol: true,
|
},
|
||||||
},
|
};
|
||||||
};
|
} else if (terminal === 'MacTerminal') {
|
||||||
} else if (terminal === 'MacTerminal') {
|
// Mac Terminal sends ESC + letter
|
||||||
// Mac Terminal sends ESC + letter
|
return {
|
||||||
return {
|
terminal,
|
||||||
terminal,
|
key,
|
||||||
key,
|
kitty: false,
|
||||||
kitty: false,
|
chunk: `\x1b${key}`,
|
||||||
input: {
|
expected: {
|
||||||
sequence: `\x1b${key}`,
|
sequence: `\x1b${key}`,
|
||||||
name: key,
|
name: key,
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
meta: true,
|
meta: true,
|
||||||
shift: false,
|
shift: false,
|
||||||
paste: false,
|
paste: false,
|
||||||
},
|
},
|
||||||
expected: {
|
};
|
||||||
sequence: `\x1b${key}`,
|
} else {
|
||||||
name: key,
|
// iTerm2 and VSCode send accented characters (å, ø, µ)
|
||||||
ctrl: false,
|
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode but
|
||||||
meta: true,
|
// gets converted to m with meta:true
|
||||||
shift: false,
|
return {
|
||||||
paste: false,
|
terminal,
|
||||||
},
|
key,
|
||||||
};
|
chunk: accentedChar,
|
||||||
} else {
|
expected: {
|
||||||
// iTerm2 and VSCode send accented characters (å, ø, µ)
|
name: key,
|
||||||
// Note: µ comes with meta:false but gets converted to m with meta:true
|
ctrl: false,
|
||||||
return {
|
meta: true, // Always expect meta:true after conversion
|
||||||
terminal,
|
shift: false,
|
||||||
key,
|
paste: false,
|
||||||
input: {
|
sequence: accentedChar,
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
'should handle Alt+$key in $terminal',
|
'should handle Alt+$key in $terminal',
|
||||||
({
|
({
|
||||||
kittySequence,
|
chunk,
|
||||||
input,
|
|
||||||
expected,
|
expected,
|
||||||
kitty = true,
|
kitty = true,
|
||||||
}: {
|
}: {
|
||||||
kittySequence?: string;
|
chunk: string;
|
||||||
input?: Partial<Key>;
|
|
||||||
expected: Partial<Key>;
|
expected: Partial<Key>;
|
||||||
kitty?: boolean;
|
kitty?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -930,11 +902,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
});
|
});
|
||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
if (kittySequence) {
|
act(() => stdin.write(chunk));
|
||||||
act(() => stdin.write(kittySequence));
|
|
||||||
} else if (input) {
|
|
||||||
act(() => stdin.pressKey(input));
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(keyHandler).toHaveBeenCalledWith(
|
expect(keyHandler).toHaveBeenCalledWith(
|
||||||
expect.objectContaining(expected),
|
expect.objectContaining(expected),
|
||||||
@@ -978,8 +946,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
|
|
||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
// Send incomplete kitty sequence
|
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
|
||||||
|
|
||||||
// Should not broadcast immediately
|
// Should not broadcast immediately
|
||||||
expect(keyHandler).not.toHaveBeenCalled();
|
expect(keyHandler).not.toHaveBeenCalled();
|
||||||
@@ -991,15 +958,13 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
expect(keyHandler).not.toHaveBeenCalled();
|
expect(keyHandler).not.toHaveBeenCalled();
|
||||||
|
|
||||||
// Advance past timeout
|
// Advance past timeout
|
||||||
act(() => {
|
act(() => vi.advanceTimersByTime(10));
|
||||||
vi.advanceTimersByTime(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should now broadcast the incomplete sequence as regular input
|
// Should now broadcast the incomplete sequence as regular input
|
||||||
expect(keyHandler).toHaveBeenCalledWith(
|
expect(keyHandler).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: '',
|
name: '',
|
||||||
sequence: '\x1b[1;',
|
sequence: INCOMPLETE_KITTY_SEQUENCE,
|
||||||
paste: false,
|
paste: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1079,8 +1044,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
|
|
||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
// Send incomplete kitty sequence
|
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
|
||||||
|
|
||||||
// Press Ctrl+C
|
// Press Ctrl+C
|
||||||
act(() => stdin.write('\x03'));
|
act(() => stdin.write('\x03'));
|
||||||
@@ -1189,13 +1153,13 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
// Start incomplete sequence
|
// Start incomplete sequence
|
||||||
act(() => stdin.pressKey({ sequence: '\x1b[1' }));
|
act(() => stdin.write('\x1b[97;13'));
|
||||||
|
|
||||||
// Advance time partway
|
// Advance time partway
|
||||||
act(() => vi.advanceTimersByTime(30));
|
act(() => vi.advanceTimersByTime(30));
|
||||||
|
|
||||||
// Add more to sequence
|
// Add more to sequence
|
||||||
act(() => stdin.write('3'));
|
act(() => stdin.write('5'));
|
||||||
|
|
||||||
// Advance time from the first timeout point
|
// Advance time from the first timeout point
|
||||||
act(() => vi.advanceTimersByTime(25));
|
act(() => vi.advanceTimersByTime(25));
|
||||||
@@ -1209,7 +1173,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
// Should now parse as complete enter key
|
// Should now parse as complete enter key
|
||||||
expect(keyHandler).toHaveBeenCalledWith(
|
expect(keyHandler).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'return',
|
name: 'a',
|
||||||
kittyProtocol: true,
|
kittyProtocol: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1221,8 +1185,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
|
|
||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
// Send incomplete kitty sequence
|
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
|
||||||
|
|
||||||
// Incomplete sequence should be buffered, not broadcast
|
// Incomplete sequence should be buffered, not broadcast
|
||||||
expect(keyHandler).not.toHaveBeenCalled();
|
expect(keyHandler).not.toHaveBeenCalled();
|
||||||
@@ -1235,7 +1198,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
expect(keyHandler).toHaveBeenCalledWith(
|
expect(keyHandler).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: '',
|
name: '',
|
||||||
sequence: '\x1b[1;',
|
sequence: INCOMPLETE_KITTY_SEQUENCE,
|
||||||
paste: false,
|
paste: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1247,8 +1210,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
|
|
||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
// Send incomplete kitty sequence
|
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
|
||||||
|
|
||||||
// Incomplete sequence should be buffered, not broadcast
|
// Incomplete sequence should be buffered, not broadcast
|
||||||
expect(keyHandler).not.toHaveBeenCalled();
|
expect(keyHandler).not.toHaveBeenCalled();
|
||||||
@@ -1261,7 +1223,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
expect(keyHandler).toHaveBeenCalledWith(
|
expect(keyHandler).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: '',
|
name: '',
|
||||||
sequence: '\x1b[1;',
|
sequence: INCOMPLETE_KITTY_SEQUENCE,
|
||||||
paste: false,
|
paste: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1274,8 +1236,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
|
|
||||||
act(() => result.current.subscribe(keyHandler));
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
// Send incomplete kitty sequence
|
act(() => stdin.write(INCOMPLETE_KITTY_SEQUENCE));
|
||||||
act(() => stdin.pressKey({ sequence: '\x1b[1;' }));
|
|
||||||
|
|
||||||
// Incomplete sequence should be buffered, not broadcast
|
// Incomplete sequence should be buffered, not broadcast
|
||||||
expect(keyHandler).not.toHaveBeenCalled();
|
expect(keyHandler).not.toHaveBeenCalled();
|
||||||
@@ -1288,7 +1249,7 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
expect(keyHandler).toHaveBeenCalledWith(
|
expect(keyHandler).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: '',
|
name: '',
|
||||||
sequence: '\x1b[1;',
|
sequence: INCOMPLETE_KITTY_SEQUENCE,
|
||||||
paste: false,
|
paste: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user