import { afterEach, describe, expect, it, vi } from 'vitest'; import type { ToolContext } from '@opencode-ai/plugin/tool'; import { NianCodeAgentBrowserPlugin } from '../../.opencode/plugins/agent-browser-tools'; const originalBaseUrl = process.env.NIANCODE_HOST_API_BASE_URL; const originalToken = process.env.NIANCODE_HOST_API_TOKEN; function toolContext(directory: string): ToolContext { return { sessionID: 'session-1', messageID: 'message-1', agent: 'build', directory, worktree: directory, abort: new AbortController().signal, metadata: vi.fn(), ask: vi.fn(), }; } function response(payload: Record, status = 200): Response { return new Response(JSON.stringify(payload), { status, headers: { 'Content-Type': 'application/json' }, }); } afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks(); if (originalBaseUrl === undefined) delete process.env.NIANCODE_HOST_API_BASE_URL; else process.env.NIANCODE_HOST_API_BASE_URL = originalBaseUrl; if (originalToken === undefined) delete process.env.NIANCODE_HOST_API_TOKEN; else process.env.NIANCODE_HOST_API_TOKEN = originalToken; }); describe('Agent Browser OpenCode plugin', () => { it('loads the bundled runtime plugin artifact', async () => { const bundled = await import( '../../.opencode/skills/agent-browser/.opencode/plugins/niancode-agent-browser.js' ); expect(bundled.NianCodeAgentBrowserPlugin).toBeTypeOf('function'); }); it('binds lifecycle requests to context.directory without exposing model-selected ids', async () => { process.env.NIANCODE_HOST_API_BASE_URL = 'http://127.0.0.1:43210/'; process.env.NIANCODE_HOST_API_TOKEN = 'host-token'; const fetchMock = vi.fn(async () => response({ success: true, browser: { state: 'attached', url: 'http://localhost:5173' }, })); vi.stubGlobal('fetch', fetchMock); const plugin = await NianCodeAgentBrowserPlugin(); await plugin.tool.browser_context.execute( { action: 'open', url: 'http://localhost:5173' }, toolContext('D:\\Students\\demo'), ); expect(fetchMock).toHaveBeenCalledOnce(); expect(fetchMock.mock.calls[0][0]).toBe('http://127.0.0.1:43210/api/agent-browser/open'); const init = fetchMock.mock.calls[0][1] as RequestInit; expect(init.headers).toMatchObject({ Authorization: 'Bearer host-token' }); expect(JSON.parse(String(init.body))).toEqual({ project_path: 'D:\\Students\\demo', url: 'http://localhost:5173', }); expect(String(init.body)).not.toMatch(/projectId|tabId|webContentsId/); }); it('returns Full CDP results and event pages without stripping nested data', async () => { process.env.NIANCODE_HOST_API_BASE_URL = 'http://127.0.0.1:43210'; process.env.NIANCODE_HOST_API_TOKEN = 'host-token'; const fullResult = { kind: 'inline', value: { result: { type: 'object', value: { headers: { authorization: 'page-owned-value' }, nested: [1, 2, 3] }, }, }, }; const fullPage = { events: [{ sequence: 41, method: 'Network.responseReceived', params: { response: { headers: { 'x-debug': 'complete' } } }, }], nextCursor: 41, hasMore: false, }; const fetchMock = vi.fn() .mockResolvedValueOnce(response({ success: true, result: fullResult })) .mockResolvedValueOnce(response({ success: true, page: fullPage })); vi.stubGlobal('fetch', fetchMock); const plugin = await NianCodeAgentBrowserPlugin(); const context = toolContext('D:\\Students\\demo'); const sendResult = await plugin.tool.browser_cdp.execute({ action: 'send', method: 'Runtime.evaluate', params: { expression: 'window.__debugState' }, }, context); const eventsResult = await plugin.tool.browser_cdp.execute({ action: 'read_events', after: 30, methods: ['Network.responseReceived'], }, context); expect(sendResult).toMatchObject({ output: JSON.stringify(fullResult, null, 2) }); expect(eventsResult).toMatchObject({ output: JSON.stringify(fullPage, null, 2) }); expect(JSON.parse(String((fetchMock.mock.calls[0][1] as RequestInit).body))).toEqual({ project_path: 'D:\\Students\\demo', method: 'Runtime.evaluate', params: { expression: 'window.__debugState' }, }); expect(JSON.parse(String((fetchMock.mock.calls[1][1] as RequestInit).body))).toEqual({ project_path: 'D:\\Students\\demo', after: 30, methods: ['Network.responseReceived'], }); }); });