fix(cli): resolve TTY hang on headless environments by unconditionally resuming process.stdin before React Ink launch (#23673)

This commit is contained in:
Coco Sheng
2026-03-25 14:18:43 -04:00
committed by GitHub
parent 0bb6c25dc7
commit 830f7dec61
4 changed files with 149 additions and 1 deletions
@@ -860,6 +860,85 @@ describe('oauth2', () => {
global.setTimeout = originalSetTimeout;
});
it('should clear the authorization timeout immediately upon successful web login to prevent memory leaks', async () => {
const mockAuthUrl = 'https://example.com/auth';
const mockCode = 'test-code';
const mockState = 'test-state';
const mockOAuth2Client = {
generateAuthUrl: vi.fn().mockReturnValue(mockAuthUrl),
getToken: vi.fn().mockResolvedValue({
tokens: {
access_token: 'test-token',
refresh_token: 'test-refresh',
},
}),
setCredentials: vi.fn().mockImplementation(function (
this: { credentials?: unknown },
creds: unknown,
) {
this.credentials = creds;
}),
getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
on: vi.fn(),
credentials: {},
} as unknown as OAuth2Client;
vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);
vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
vi.mocked(open).mockImplementation(
async () => ({ on: vi.fn() }) as never,
);
let requestCallback!: http.RequestListener;
let serverListeningCallback: (value: unknown) => void;
const serverListeningPromise = new Promise(
(resolve) => (serverListeningCallback = resolve),
);
const mockHttpServer = {
listen: vi.fn(
(_port: number, _host: string, callback?: () => void) => {
if (callback) callback();
serverListeningCallback(undefined);
},
),
close: vi.fn(),
on: vi.fn(),
address: () => ({ port: 3000 }),
};
(http.createServer as Mock).mockImplementation((cb) => {
requestCallback = cb;
return mockHttpServer as unknown as http.Server;
});
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
const clientPromise = getOauthClient(
AuthType.LOGIN_WITH_GOOGLE,
mockConfig,
);
await serverListeningPromise;
const mockReq = {
url: `/oauth2callback?code=${mockCode}&state=${mockState}`,
} as http.IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
on: vi.fn(),
} as unknown as http.ServerResponse;
// Trigger the successful server response
requestCallback(mockReq, mockRes);
await clientPromise;
// Verify that the watchdog timer was cleared correctly
expect(clearTimeoutSpy).toHaveBeenCalled();
clearTimeoutSpy.mockRestore();
});
it('should handle OAuth callback errors with descriptive messages', async () => {
const mockAuthUrl = 'https://example.com/auth';
const mockOAuth2Client = {
+5 -1
View File
@@ -332,8 +332,9 @@ async function initOauthClient(
// Add timeout to prevent infinite waiting when browser tab gets stuck
const authTimeout = 5 * 60 * 1000; // 5 minutes timeout
let timeoutId: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
timeoutId = setTimeout(() => {
reject(
new FatalAuthenticationError(
'Authentication timed out after 5 minutes. The browser tab may have gotten stuck in a loading state. ' +
@@ -371,6 +372,9 @@ async function initOauthClient(
cancellationPromise,
]);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (sigIntHandler) {
process.removeListener('SIGINT', sigIntHandler);
}