Files
makelore/tests/unit/agent-browser-plugin.test.ts
brother7 e97a7ce9df feat: 增加共享 Agent Browser 调试能力
需求:在 AI 编程会话中让用户与 Agent 共享同一浏览器页面,并查看控制台与网络信息。

实现:新增沙箱浏览器内核、Host API/渲染器面板、OpenCode 工具接入及安全边界测试。
2026-07-31 14:53:36 +08:00

125 lines
4.5 KiB
TypeScript

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<string, unknown>, 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'],
});
});
});