/**
* Browser Bridge Mock
*
* Mock implementation for testing browser bridge functionality without actual browser UI.
* Simulates human responses and SSE communication.
*/
import axios, { AxiosInstance } from 'axios';
import { EventSource } from 'eventsource';
export interface MockBrowserResponse {
requestId: string;
response: any;
delay?: number; // Delay before responding (ms)
}
export interface BrowserBridgeConfig {
baseUrl?: string;
port?: number;
}
/**
* Mock browser bridge for testing human-in-the-loop functionality
*/
export class BrowserBridgeMock {
private httpClient: AxiosInstance;
private eventSource: EventSource | null = null;
private baseUrl: string;
private responses = new Map<string, MockBrowserResponse>();
private autoRespond = true;
private responseDelay = 100; // Default delay for responses
constructor(config: BrowserBridgeConfig = {}) {
this.baseUrl = config.baseUrl || `http://localhost:${config.port || 3000}`;
this.httpClient = axios.create({
baseURL: this.baseUrl,
timeout: 5000,
});
}
/**
* Connect to the browser bridge SSE endpoint
*/
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.eventSource = new EventSource(`${this.baseUrl}/mcp/browser-events`);
this.eventSource.onopen = () => {
resolve();
};
this.eventSource.onerror = (error) => {
reject(new Error(`SSE connection failed: ${error}`));
};
this.eventSource.onmessage = (event) => {
this.handleServerEvent(event);
};
// Timeout for connection
setTimeout(() => {
if (this.eventSource?.readyState !== EventSource.OPEN) {
reject(new Error('SSE connection timeout'));
}
}, 5000);
} catch (error) {
reject(error);
}
});
}
/**
* Disconnect from the browser bridge
*/
disconnect(): void {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
/**
* Set whether to auto-respond to requests
*/
setAutoRespond(enabled: boolean): void {
this.autoRespond = enabled;
}
/**
* Set default response delay
*/
setResponseDelay(delayMs: number): void {
this.responseDelay = delayMs;
}
/**
* Prepare a mock response for a specific request
*/
prepareMockResponse(requestId: string, response: any, delay?: number): void {
this.responses.set(requestId, {
requestId,
response,
delay: delay || this.responseDelay
});
}
/**
* Send a response to the server
*/
async sendResponse(requestId: string, response: any): Promise<void> {
try {
await this.httpClient.post('/mcp/response', {
requestId,
response
});
} catch (error) {
throw new Error(`Failed to send response: ${error}`);
}
}
/**
* Handle events from the server
*/
private async handleServerEvent(event: MessageEvent): Promise<void> {
try {
const data = JSON.parse(event.data);
switch (data.type) {
case 'human_request':
await this.handleHumanRequest(data.data);
break;
case 'request_timeout':
this.handleRequestTimeout(data.data);
break;
case 'request_cancelled':
this.handleRequestCancelled(data.data);
break;
default:
// Ignore unknown event types
break;
}
} catch (error) {
console.error('Error handling server event:', error);
}
}
/**
* Handle human request from server
*/
private async handleHumanRequest(requestData: any): Promise<void> {
const requestId = requestData.id;
const mockResponse = this.responses.get(requestId);
if (this.autoRespond) {
const response = mockResponse?.response || this.generateDefaultResponse(requestData);
const delay = mockResponse?.delay || this.responseDelay;
// Simulate human response time
setTimeout(async () => {
try {
await this.sendResponse(requestId, response);
this.responses.delete(requestId); // Clean up
} catch (error) {
console.error('Error sending mock response:', error);
}
}, delay);
}
}
/**
* Handle request timeout
*/
private handleRequestTimeout(data: any): void {
console.log(`Request ${data.requestId} timed out:`, data.message);
this.responses.delete(data.requestId);
}
/**
* Handle request cancellation
*/
private handleRequestCancelled(data: any): void {
console.log(`Request ${data.requestId} was cancelled:`, data.message);
this.responses.delete(data.requestId);
}
/**
* Generate default response based on request type
*/
private generateDefaultResponse(requestData: any): any {
switch (requestData.type) {
case 'ask-one-question':
return {
answer: 'Mock response to: ' + requestData.question,
completionStatus: 'done'
};
case 'ask-multiple-choice':
return {
answers: requestData.questions.map((q: any, index: number) => ({
questionIndex: index,
selectedOptions: [0], // Select first option
comment: 'Mock selection'
})),
completionStatus: 'done'
};
case 'challenge-hypothesis':
return {
responses: requestData.hypotheses.map((_: any, index: number) => ({
hypothesisIndex: index,
agreementLevel: 4, // Neutral
comment: 'Mock evaluation'
}))
};
case 'choose-next':
return {
selectedOption: requestData.options[0]?.id || 'mock-option',
message: 'Mock selection'
};
default:
return { mockResponse: true };
}
}
/**
* Wait for a specific number of requests
*/
async waitForRequests(count: number, timeout = 10000): Promise<any[]> {
const requests: any[] = [];
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
reject(new Error(`Timeout waiting for ${count} requests`));
}, timeout);
const originalHandler = this.handleServerEvent.bind(this);
this.handleServerEvent = async (event: MessageEvent) => {
const data = JSON.parse(event.data);
if (data.type === 'human_request') {
requests.push(data.data);
if (requests.length === count) {
clearTimeout(timeoutHandle);
this.handleServerEvent = originalHandler;
resolve(requests);
}
}
return originalHandler(event);
};
});
}
/**
* Check if the browser bridge is available
*/
async isAvailable(): Promise<boolean> {
try {
await this.httpClient.get('/mcp/browser-events', { timeout: 1000 });
return true;
} catch {
return false;
}
}
}
/**
* Create and connect a browser bridge mock
*/
export async function createBrowserBridgeMock(config?: BrowserBridgeConfig): Promise<BrowserBridgeMock> {
const mock = new BrowserBridgeMock(config);
await mock.connect();
return mock;
}