feat: 增加共享 Agent Browser 调试能力
需求:在 AI 编程会话中让用户与 Agent 共享同一浏览器页面,并查看控制台与网络信息。 实现:新增沙箱浏览器内核、Host API/渲染器面板、OpenCode 工具接入及安全边界测试。
This commit is contained in:
124
tests/unit/agent-browser-plugin.test.ts
Normal file
124
tests/unit/agent-browser-plugin.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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'],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user