| |
| |
| |
| |
| |
|
|
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
| import { BrowserAgentInvocation } from './browserAgentInvocation.js'; |
| import { makeFakeConfig } from '../../test-utils/config.js'; |
| import type { Config } from '../../config/config.js'; |
| import type { MessageBus } from '../../confirmation-bus/message-bus.js'; |
| import { |
| type AgentInputs, |
| type SubagentProgress, |
| type SubagentActivityEvent, |
| } from '../types.js'; |
|
|
| |
| vi.mock('../../utils/debugLogger.js', () => ({ |
| debugLogger: { |
| log: vi.fn(), |
| warn: vi.fn(), |
| error: vi.fn(), |
| }, |
| })); |
|
|
| vi.mock('./browserAgentFactory.js', () => ({ |
| createBrowserAgentDefinition: vi.fn(), |
| cleanupBrowserAgent: vi.fn(), |
| })); |
|
|
| vi.mock('./inputBlocker.js', () => ({ |
| removeInputBlocker: vi.fn(), |
| })); |
|
|
| vi.mock('./automationOverlay.js', () => ({ |
| removeAutomationOverlay: vi.fn(), |
| })); |
|
|
| vi.mock('../../telemetry/metrics.js', () => ({ |
| recordBrowserAgentTaskOutcome: vi.fn(), |
| })); |
|
|
| vi.mock('../local-executor.js', () => ({ |
| LocalAgentExecutor: { |
| create: vi.fn(), |
| }, |
| })); |
|
|
| import { |
| createBrowserAgentDefinition, |
| cleanupBrowserAgent, |
| } from './browserAgentFactory.js'; |
| import { removeInputBlocker } from './inputBlocker.js'; |
| import { removeAutomationOverlay } from './automationOverlay.js'; |
| import { LocalAgentExecutor } from '../local-executor.js'; |
| import { recordBrowserAgentTaskOutcome } from '../../telemetry/metrics.js'; |
| import type { ToolLiveOutput } from '../../tools/tools.js'; |
|
|
| describe('BrowserAgentInvocation', () => { |
| let mockConfig: Config; |
| let mockMessageBus: MessageBus; |
| let mockParams: AgentInputs; |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
|
|
| mockConfig = makeFakeConfig({ |
| agents: { |
| overrides: { |
| browser_agent: { |
| enabled: true, |
| }, |
| }, |
| browser: { |
| headless: false, |
| sessionMode: 'isolated', |
| }, |
| }, |
| }); |
|
|
| mockMessageBus = { |
| publish: vi.fn().mockResolvedValue(undefined), |
| subscribe: vi.fn(), |
| unsubscribe: vi.fn(), |
| } as unknown as MessageBus; |
|
|
| mockParams = { |
| task: 'Navigate to example.com and click the button', |
| }; |
| }); |
|
|
| afterEach(() => { |
| vi.restoreAllMocks(); |
| }); |
|
|
| describe('constructor', () => { |
| it('should create invocation with params', () => { |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| expect(invocation.params).toEqual(mockParams); |
| }); |
|
|
| it('should use browser_agent as default tool name', () => { |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| expect(invocation['_toolName']).toBe('browser_agent'); |
| }); |
|
|
| it('should use custom tool name if provided', () => { |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| 'custom_name', |
| 'Custom Display Name', |
| ); |
|
|
| expect(invocation['_toolName']).toBe('custom_name'); |
| expect(invocation['_toolDisplayName']).toBe('Custom Display Name'); |
| }); |
| }); |
|
|
| describe('getDescription', () => { |
| it('should return description with input summary', () => { |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const description = invocation.getDescription(); |
|
|
| expect(description).toContain('browser agent'); |
| expect(description).toContain('task'); |
| }); |
|
|
| it('should truncate long input values', () => { |
| const longParams = { |
| task: 'A'.repeat(100), |
| }; |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| longParams, |
| mockMessageBus, |
| ); |
|
|
| const description = invocation.getDescription(); |
|
|
| |
| expect(description.length).toBeLessThanOrEqual(200); |
| }); |
| }); |
|
|
| describe('toolLocations', () => { |
| it('should return empty array by default', () => { |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const locations = invocation.toolLocations(); |
|
|
| expect(locations).toEqual([]); |
| }); |
| }); |
|
|
| describe('execute', () => { |
| let mockExecutor: { run: ReturnType<typeof vi.fn> }; |
|
|
| beforeEach(() => { |
| vi.mocked(createBrowserAgentDefinition).mockResolvedValue({ |
| definition: { |
| name: 'browser_agent', |
| description: 'mock definition', |
| kind: 'local', |
| inputConfig: {} as never, |
| outputConfig: {} as never, |
| processOutput: () => '', |
| modelConfig: { model: 'test' }, |
| runConfig: {}, |
| promptConfig: { query: '', systemPrompt: '' }, |
| toolConfig: { tools: ['analyze_screenshot', 'click'] }, |
| }, |
| browserManager: { |
| release: vi.fn(), |
| callTool: vi.fn().mockResolvedValue({ content: [] }), |
| } as never, |
| visionEnabled: true, |
| sessionMode: 'persistent', |
| }); |
|
|
| mockExecutor = { |
| run: vi.fn().mockResolvedValue({ |
| result: JSON.stringify({ success: true }), |
| terminate_reason: 'GOAL', |
| }), |
| }; |
|
|
| vi.mocked(LocalAgentExecutor.create).mockResolvedValue( |
| mockExecutor as never, |
| ); |
| vi.mocked(removeInputBlocker).mockClear(); |
| }); |
|
|
| it('should return result text and call cleanup on success', async () => { |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const controller = new AbortController(); |
| const updateOutput: (output: ToolLiveOutput) => void = vi.fn(); |
|
|
| const result = await invocation.execute({ |
| abortSignal: controller.signal, |
| updateOutput, |
| }); |
|
|
| expect(Array.isArray(result.llmContent)).toBe(true); |
| expect((result.llmContent as Array<{ text: string }>)[0].text).toContain( |
| 'Browser agent finished', |
| ); |
| expect(removeInputBlocker).toHaveBeenCalled(); |
| }); |
|
|
| it('should work without updateOutput (fire-and-forget)', async () => { |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const controller = new AbortController(); |
| |
| await expect( |
| invocation.execute({ abortSignal: controller.signal }), |
| ).resolves.toBeDefined(); |
| }); |
|
|
| it('should return error result when executor throws', async () => { |
| mockExecutor.run.mockRejectedValue(new Error('Unexpected crash')); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const controller = new AbortController(); |
| const result = await invocation.execute({ |
| abortSignal: controller.signal, |
| }); |
|
|
| expect(result.error).toBeDefined(); |
| expect(removeInputBlocker).toHaveBeenCalled(); |
| }); |
|
|
| |
|
|
| |
| |
| |
| |
| function setupActivityCapture(): { |
| capturedOnActivity: () => SubagentActivityEvent | undefined; |
| fireActivity: (event: SubagentActivityEvent) => void; |
| } { |
| let onActivityFn: ((e: SubagentActivityEvent) => void) | undefined; |
|
|
| vi.mocked(LocalAgentExecutor.create).mockImplementation( |
| async (_def, _config, onActivity) => { |
| onActivityFn = onActivity; |
| return mockExecutor as never; |
| }, |
| ); |
|
|
| return { |
| capturedOnActivity: () => undefined, |
| fireActivity: (event: SubagentActivityEvent) => { |
| onActivityFn?.(event); |
| }, |
| }; |
| } |
|
|
| it('should emit initial SubagentProgress with running state', async () => { |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| await invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| const firstCall = updateOutput.mock.calls[0]?.[0] as SubagentProgress; |
| expect(firstCall.isSubagentProgress).toBe(true); |
| expect(firstCall.state).toBe('running'); |
| expect(firstCall.recentActivity).toEqual([]); |
| }); |
|
|
| it('should emit completed SubagentProgress on success', async () => { |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| await invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| const lastCall = updateOutput.mock.calls[ |
| updateOutput.mock.calls.length - 1 |
| ]?.[0] as SubagentProgress; |
| expect(lastCall.isSubagentProgress).toBe(true); |
| expect(lastCall.state).toBe('completed'); |
| }); |
|
|
| it('should handle THOUGHT_CHUNK and emit structured progress', async () => { |
| const { fireActivity } = setupActivityCapture(); |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const executePromise = invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| |
| await Promise.resolve(); |
| await Promise.resolve(); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'THOUGHT_CHUNK', |
| data: { text: 'Navigating to the page...' }, |
| }); |
|
|
| await executePromise; |
|
|
| const progressCalls = updateOutput.mock.calls |
| .map((c) => c[0] as SubagentProgress) |
| .filter((p) => p.isSubagentProgress); |
|
|
| const thoughtProgress = progressCalls.find((p) => |
| p.recentActivity.some( |
| (a) => |
| a.type === 'thought' && |
| a.content.includes('Navigating to the page...'), |
| ), |
| ); |
|
|
| expect(thoughtProgress).toBeDefined(); |
| }); |
|
|
| it('should overwrite the thought content with new THOUGHT_CHUNK activity', async () => { |
| const { fireActivity } = setupActivityCapture(); |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const executePromise = invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| |
| await Promise.resolve(); |
| await Promise.resolve(); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'THOUGHT_CHUNK', |
| data: { text: 'I am thinking.' }, |
| }); |
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'THOUGHT_CHUNK', |
| data: { text: 'Now I will act.' }, |
| }); |
|
|
| await executePromise; |
|
|
| const progressCalls = updateOutput.mock.calls |
| .map((c) => c[0] as SubagentProgress) |
| .filter((p) => p.isSubagentProgress); |
|
|
| const lastCall = progressCalls[progressCalls.length - 1]; |
| expect(lastCall.recentActivity).toContainEqual( |
| expect.objectContaining({ |
| type: 'thought', |
| content: 'Now I will act.', |
| }), |
| ); |
| }); |
|
|
| it('should handle TOOL_CALL_START and TOOL_CALL_END with callId tracking', async () => { |
| const { fireActivity } = setupActivityCapture(); |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const executePromise = invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| await Promise.resolve(); |
| await Promise.resolve(); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'TOOL_CALL_START', |
| data: { |
| name: 'navigate_browser', |
| callId: 'call-1', |
| args: { url: 'https://example.com' }, |
| }, |
| }); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'TOOL_CALL_END', |
| data: { name: 'navigate_browser', id: 'call-1' }, |
| }); |
|
|
| await executePromise; |
|
|
| const progressCalls = updateOutput.mock.calls |
| .map((c) => c[0] as SubagentProgress) |
| .filter((p) => p.isSubagentProgress); |
|
|
| |
| const finalProgress = progressCalls[progressCalls.length - 1]; |
| const toolItem = finalProgress?.recentActivity.find( |
| (a) => a.type === 'tool_call' && a.content === 'navigate_browser', |
| ); |
| expect(toolItem).toBeDefined(); |
| expect(toolItem?.status).toBe('completed'); |
| }); |
|
|
| it('should sanitize sensitive data in tool call args', async () => { |
| const { fireActivity } = setupActivityCapture(); |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const executePromise = invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| await Promise.resolve(); |
| await Promise.resolve(); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'TOOL_CALL_START', |
| data: { |
| name: 'fill_form', |
| callId: 'call-2', |
| args: { password: 'supersecret123', url: 'https://example.com' }, |
| }, |
| }); |
|
|
| await executePromise; |
|
|
| const progressCalls = updateOutput.mock.calls |
| .map((c) => c[0] as SubagentProgress) |
| .filter((p) => p.isSubagentProgress); |
|
|
| const toolItem = progressCalls |
| .flatMap((p) => p.recentActivity) |
| .find((a) => a.type === 'tool_call' && a.content === 'fill_form'); |
|
|
| expect(toolItem).toBeDefined(); |
| expect(toolItem?.args).not.toContain('supersecret123'); |
| expect(toolItem?.args).toContain('[REDACTED]'); |
| }); |
|
|
| it('should handle ERROR event with callId and mark tool as errored', async () => { |
| const { fireActivity } = setupActivityCapture(); |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const executePromise = invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| await Promise.resolve(); |
| await Promise.resolve(); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'TOOL_CALL_START', |
| data: { name: 'click_element', callId: 'call-3', args: {} }, |
| }); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'ERROR', |
| data: { error: 'Element not found', callId: 'call-3' }, |
| }); |
|
|
| await executePromise; |
|
|
| const progressCalls = updateOutput.mock.calls |
| .map((c) => c[0] as SubagentProgress) |
| .filter((p) => p.isSubagentProgress); |
|
|
| const allItems = progressCalls.flatMap((p) => p.recentActivity); |
| const toolItem = allItems.find( |
| (a) => a.type === 'tool_call' && a.content === 'click_element', |
| ); |
| expect(toolItem?.status).toBe('error'); |
| }); |
|
|
| it('should sanitize sensitive data in ERROR event messages', async () => { |
| const { fireActivity } = setupActivityCapture(); |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const executePromise = invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| await Promise.resolve(); |
| await Promise.resolve(); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'ERROR', |
| data: { error: 'Auth failed: api_key=sk-secret-abc1234567890' }, |
| }); |
|
|
| await executePromise; |
|
|
| const progressCalls = updateOutput.mock.calls |
| .map((c) => c[0] as SubagentProgress) |
| .filter((p) => p.isSubagentProgress); |
|
|
| const errorItem = progressCalls |
| .flatMap((p) => p.recentActivity) |
| .find((a) => a.type === 'thought' && a.status === 'error'); |
|
|
| expect(errorItem).toBeDefined(); |
| expect(errorItem?.content).not.toContain('sk-secret-abc1234567890'); |
| expect(errorItem?.content).toContain('[REDACTED]'); |
| }); |
|
|
| it('should sanitize inline PEM content in error messages', async () => { |
| const { fireActivity } = setupActivityCapture(); |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const executePromise = invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| await Promise.resolve(); |
| await Promise.resolve(); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'ERROR', |
| data: { |
| error: |
| 'Failed to authenticate:\n-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA12345...\n-----END RSA PRIVATE KEY-----\nPlease check credentials.', |
| }, |
| }); |
|
|
| await executePromise; |
|
|
| const progressCalls = updateOutput.mock.calls |
| .map((c) => c[0] as SubagentProgress) |
| .filter((p) => p.isSubagentProgress); |
|
|
| const errorItem = progressCalls |
| .flatMap((p) => p.recentActivity) |
| .find((a) => a.type === 'thought' && a.status === 'error'); |
|
|
| expect(errorItem).toBeDefined(); |
| expect(errorItem?.content).toContain('[REDACTED_PEM]'); |
| expect(errorItem?.content).not.toContain('-----BEGIN'); |
| }); |
|
|
| it('should mark all running tools as errored when ERROR has no callId', async () => { |
| const { fireActivity } = setupActivityCapture(); |
| const updateOutput = vi.fn(); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| const executePromise = invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| await Promise.resolve(); |
| await Promise.resolve(); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'TOOL_CALL_START', |
| data: { name: 'tool_a', callId: 'c1', args: {} }, |
| }); |
|
|
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'TOOL_CALL_START', |
| data: { name: 'tool_b', callId: 'c2', args: {} }, |
| }); |
|
|
| |
| fireActivity({ |
| isSubagentActivityEvent: true, |
| agentName: 'browser_agent', |
| type: 'ERROR', |
| data: { error: 'Agent crashed' }, |
| }); |
|
|
| await executePromise; |
|
|
| const progressCalls = updateOutput.mock.calls |
| .map((c) => c[0] as SubagentProgress) |
| .filter((p) => p.isSubagentProgress); |
|
|
| const finalActivity = |
| progressCalls[progressCalls.length - 1].recentActivity; |
| const toolA = finalActivity.find( |
| (a) => a.type === 'tool_call' && a.content === 'tool_a', |
| ); |
| const toolB = finalActivity.find( |
| (a) => a.type === 'tool_call' && a.content === 'tool_b', |
| ); |
|
|
| |
| expect(toolA?.status).toBe('error'); |
| expect(toolB?.status).toBe('error'); |
| }); |
|
|
| it('should record successful task outcome metrics', async () => { |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
| await invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput: vi.fn(), |
| }); |
|
|
| expect(recordBrowserAgentTaskOutcome).toHaveBeenCalledWith( |
| mockConfig, |
| expect.objectContaining({ |
| success: true, |
| session_mode: 'persistent', |
| vision_enabled: true, |
| headless: false, |
| duration_ms: expect.any(Number), |
| }), |
| ); |
| }); |
|
|
| it('should record failed task outcome metrics', async () => { |
| vi.mocked(LocalAgentExecutor.create).mockResolvedValue({ |
| run: vi.fn().mockResolvedValue({ |
| result: JSON.stringify({ success: false, foo: 'bar' }), |
| }), |
| } as never); |
|
|
| const updateOutput = vi.fn(); |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
|
|
| await invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput, |
| }); |
|
|
| expect(recordBrowserAgentTaskOutcome).toHaveBeenCalledWith( |
| mockConfig, |
| expect.objectContaining({ |
| success: false, |
| session_mode: 'persistent', |
| vision_enabled: true, |
| headless: false, |
| duration_ms: expect.any(Number), |
| }), |
| ); |
| }); |
|
|
| it('should not call cleanupBrowserAgent (cleanup is handled by BrowserManager.resetAll)', async () => { |
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| mockParams, |
| mockMessageBus, |
| ); |
| await invocation.execute({ |
| abortSignal: new AbortController().signal, |
| updateOutput: vi.fn(), |
| }); |
|
|
| expect(cleanupBrowserAgent).not.toHaveBeenCalled(); |
| }); |
| }); |
|
|
| describe('cleanup', () => { |
| it('should clean up all pages on finally', async () => { |
| const mockBrowserManager = { |
| callTool: vi.fn().mockImplementation(async (toolName: string) => { |
| if (toolName === 'list_pages') { |
| return { |
| content: [{ type: 'text', text: '0: Page 1\n1: Page 2\n' }], |
| isError: false, |
| }; |
| } |
| return { isError: false }; |
| }), |
| release: vi.fn(), |
| }; |
|
|
| vi.mocked(createBrowserAgentDefinition).mockResolvedValue({ |
| definition: { |
| name: 'browser_agent', |
| description: 'mock definition', |
| kind: 'local', |
| inputConfig: {} as never, |
| outputConfig: {} as never, |
| processOutput: () => '', |
| modelConfig: { model: 'test' }, |
| runConfig: {}, |
| promptConfig: { query: '', systemPrompt: '' }, |
| toolConfig: { tools: [] }, |
| }, |
| browserManager: mockBrowserManager as never, |
| visionEnabled: true, |
| sessionMode: 'persistent', |
| }); |
|
|
| const mockExecutor = { |
| run: vi.fn().mockResolvedValue({ |
| result: JSON.stringify({ success: true }), |
| terminate_reason: 'GOAL', |
| }), |
| }; |
|
|
| vi.mocked(LocalAgentExecutor.create).mockResolvedValue( |
| mockExecutor as never, |
| ); |
|
|
| const invocation = new BrowserAgentInvocation( |
| mockConfig, |
| { task: 'test' }, |
| mockMessageBus, |
| ); |
|
|
| await invocation.execute({ abortSignal: new AbortController().signal }); |
|
|
| |
| expect(mockBrowserManager.callTool).toHaveBeenCalledWith( |
| 'list_pages', |
| expect.anything(), |
| expect.anything(), |
| true, |
| ); |
|
|
| |
| expect(mockBrowserManager.callTool).toHaveBeenCalledWith( |
| 'select_page', |
| { pageId: 0, bringToFront: false }, |
| expect.anything(), |
| true, |
| ); |
| expect(mockBrowserManager.callTool).toHaveBeenCalledWith( |
| 'select_page', |
| { pageId: 1, bringToFront: false }, |
| expect.anything(), |
| true, |
| ); |
|
|
| |
| expect(removeInputBlocker).toHaveBeenCalledTimes(3); |
| expect(removeAutomationOverlay).toHaveBeenCalledTimes(3); |
| }); |
| }); |
| }); |
|
|