mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-02 01:11:24 -07:00
fix(core): fix race condition between browser agent and main closing process (#24340)
This commit is contained in:
@@ -79,6 +79,7 @@ import * as fs from 'node:fs';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { getBrowserConsentIfNeeded } from '../../utils/browserConsent.js';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
|
||||
describe('BrowserManager', () => {
|
||||
let mockConfig: Config;
|
||||
@@ -125,6 +126,16 @@ describe('BrowserManager', () => {
|
||||
}),
|
||||
}) as unknown as InstanceType<typeof Client>,
|
||||
);
|
||||
|
||||
vi.mocked(StdioClientTransport).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
stderr: {
|
||||
on: vi.fn(),
|
||||
},
|
||||
}) as unknown as InstanceType<typeof StdioClientTransport>,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -682,11 +693,29 @@ describe('BrowserManager', () => {
|
||||
describe('close', () => {
|
||||
it('should close MCP connections', async () => {
|
||||
const manager = new BrowserManager(mockConfig);
|
||||
const client = await manager.getRawMcpClient();
|
||||
await manager.getRawMcpClient();
|
||||
|
||||
await manager.close();
|
||||
expect(manager.isConnected()).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT log error when transport closes during intentional close()', async () => {
|
||||
const manager = new BrowserManager(mockConfig);
|
||||
await manager.ensureConnection();
|
||||
|
||||
const transportInstance =
|
||||
vi.mocked(StdioClientTransport).mock.results[0]?.value;
|
||||
|
||||
// Trigger onclose during close()
|
||||
vi.spyOn(transportInstance, 'close').mockImplementation(async () => {
|
||||
transportInstance.onclose?.();
|
||||
});
|
||||
|
||||
await manager.close();
|
||||
|
||||
expect(client.close).toHaveBeenCalled();
|
||||
expect(debugLogger.error).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('transport closed unexpectedly'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -765,6 +794,25 @@ describe('BrowserManager', () => {
|
||||
// Should not throw
|
||||
await expect(BrowserManager.resetAll()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should NOT log error when transport closes during resetAll()', async () => {
|
||||
const instance = BrowserManager.getInstance(mockConfig);
|
||||
await instance.ensureConnection();
|
||||
|
||||
const transportInstance =
|
||||
vi.mocked(StdioClientTransport).mock.results[0]?.value;
|
||||
|
||||
// Trigger onclose during close() which is called by resetAll()
|
||||
vi.spyOn(transportInstance, 'close').mockImplementation(async () => {
|
||||
transportInstance.onclose?.();
|
||||
});
|
||||
|
||||
await BrowserManager.resetAll();
|
||||
|
||||
expect(debugLogger.error).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('transport closed unexpectedly'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConnected', () => {
|
||||
@@ -788,7 +836,7 @@ describe('BrowserManager', () => {
|
||||
});
|
||||
|
||||
describe('reconnection', () => {
|
||||
it('should reconnect after unexpected disconnect', async () => {
|
||||
it('should reconnect after unexpected disconnect and log error', async () => {
|
||||
const manager = new BrowserManager(mockConfig);
|
||||
await manager.ensureConnection();
|
||||
|
||||
@@ -799,6 +847,10 @@ describe('BrowserManager', () => {
|
||||
transportInstance.onclose();
|
||||
}
|
||||
|
||||
expect(debugLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('transport closed unexpectedly'),
|
||||
);
|
||||
|
||||
// Manager should recognize disconnection
|
||||
expect(manager.isConnected()).toBe(false);
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@ export class BrowserManager {
|
||||
private mcpTransport: StdioClientTransport | undefined;
|
||||
private discoveredTools: McpTool[] = [];
|
||||
private disconnected = false;
|
||||
private isClosing = false;
|
||||
private connectionPromise: Promise<void> | undefined;
|
||||
|
||||
/** State for action rate limiting */
|
||||
@@ -360,7 +361,7 @@ export class BrowserManager {
|
||||
*/
|
||||
async ensureConnection(): Promise<void> {
|
||||
// Already connected and healthy — nothing to do
|
||||
if (this.rawMcpClient && !this.disconnected) {
|
||||
if (this.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -424,6 +425,7 @@ export class BrowserManager {
|
||||
* the transport will terminate the browser.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
this.isClosing = true;
|
||||
// Close MCP client first
|
||||
if (this.rawMcpClient) {
|
||||
try {
|
||||
@@ -463,6 +465,7 @@ export class BrowserManager {
|
||||
* BrowserManager instance.
|
||||
*/
|
||||
private async connectMcp(): Promise<void> {
|
||||
this.isClosing = false;
|
||||
debugLogger.log('Connecting isolated MCP client to chrome-devtools-mcp...');
|
||||
|
||||
// Create raw MCP SDK Client (not the wrapper McpClient)
|
||||
@@ -571,11 +574,14 @@ export class BrowserManager {
|
||||
}
|
||||
|
||||
this.mcpTransport.onclose = () => {
|
||||
this.disconnected = true;
|
||||
if (this.isClosing) {
|
||||
return;
|
||||
}
|
||||
debugLogger.error(
|
||||
'chrome-devtools-mcp transport closed unexpectedly. ' +
|
||||
'The MCP server process may have crashed.',
|
||||
);
|
||||
this.disconnected = true;
|
||||
};
|
||||
this.mcpTransport.onerror = (error: Error) => {
|
||||
debugLogger.error(
|
||||
|
||||
Reference in New Issue
Block a user