fix(cli): resolve Ghostty/raw-mode False Cancellation in oauth flow (#25026)

Co-authored-by: David Pierce <davidapierce@google.com>
This commit is contained in:
Aarchi Kumari
2026-05-02 02:38:56 +05:30
committed by GitHub
parent dc5b3114c0
commit a93d2a1d1c
2 changed files with 64 additions and 1 deletions
@@ -1452,6 +1452,67 @@ describe('oauth2', () => {
stdinRemoveListenerSpy.mockRestore();
});
it('should NOT cancel when 0x03 is embedded in a multi-byte escape sequence (Ghostty/VS Code WSL false-positive)', async () => {
// Only a lone 0x03 byte is Ctrl+C; a multi-byte escape sequence that
// merely contains 0x03 (e.g. from Ghostty on init/resize) must not cancel.
const stdinOnSpy = vi
.spyOn(process.stdin, 'on')
.mockImplementation(() => process.stdin);
vi.spyOn(process.stdin, 'removeListener').mockImplementation(
() => process.stdin,
);
const mockHttpServer = {
listen: vi.fn(),
close: vi.fn(),
on: vi.fn(),
address: () => ({ port: 3000 }),
};
(http.createServer as Mock).mockImplementation(
() => mockHttpServer as unknown as http.Server,
);
vi.mocked(OAuth2Client).mockImplementation(
() =>
({
generateAuthUrl: vi.fn().mockReturnValue('https://example.com'),
on: vi.fn(),
}) as unknown as OAuth2Client,
);
vi.mocked(open).mockImplementation(
async () => ({ on: vi.fn() }) as never,
);
const clientPromise = getOauthClient(
AuthType.LOGIN_WITH_GOOGLE,
mockConfig,
);
// Grab the registered stdin data handler
let dataHandler: ((data: Buffer) => void) | undefined;
await vi.waitFor(() => {
dataHandler = stdinOnSpy.mock.calls.find(
(c: [string | symbol, ...unknown[]]) => c[0] === 'data',
)?.[1] as (data: Buffer) => void;
if (!dataHandler) throw new Error('handler not registered');
});
// Fire an escape sequence embedding 0x03 — must NOT cancel.
dataHandler!(Buffer.from([0x1b, 0x5b, 0x03, 0x4d])); // ESC [ 0x03 M
// Promise must still be pending (not rejected).
const result = await Promise.race([
clientPromise.then(
() => 'resolved',
() => 'rejected',
),
new Promise<string>((r) => setTimeout(() => r('pending'), 50)),
]);
expect(result).toBe('pending');
stdinOnSpy.mockRestore();
vi.spyOn(process.stdin, 'removeListener').mockRestore();
});
it('should throw FatalCancellationError when consent is denied', async () => {
vi.spyOn(coreEvents, 'emitConsentRequest').mockImplementation(
(payload) => {
+3 -1
View File
@@ -356,8 +356,10 @@ async function initOauthClient(
// Note that SIGINT might not get raised on Ctrl+C in raw mode
// so we also need to look for Ctrl+C directly in stdin.
// Only match a lone 0x03 byte — some terminals (e.g. Ghostty) embed
// 0x03 inside multi-byte escape sequences, causing false cancellations.
stdinHandler = (data: Buffer) => {
if (data.includes(0x03)) {
if (data.length === 1 && data[0] === 0x03) {
reject(
new FatalCancellationError('Authentication cancelled by user.'),
);