mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-19 20:14:43 +00:00
feat(browser): 完成 browser MCP 第一阶段能力与配置打通
- 增加 browser_wait_for、browser_eval 及 click/query 扩展能力 - 打通 browser eval_mode 的配置、状态、启动链路与设置界面 - 补充浏览器相关测试隔离、文档和 CI parity 收尾调整
This commit is contained in:
1 parent
a0183073b3
commit
b90eae3892
25 files changed
+2370
-97
No files matched your search
@@ -72,6 +72,7 @@ describe('browser status', () => {
|
||||
source: 'config',
|
||||
effectiveUserDataDir: join(tempHome, '.ccs', 'browser', 'chrome-user-data'),
|
||||
devtoolsPort: 9222,
|
||||
evalMode: 'readonly',
|
||||
managedMcpServerName: 'ccs-browser',
|
||||
});
|
||||
expect(status.claude.launchCommands.linux).toContain('--remote-debugging-port=9222');
|
||||
@@ -79,6 +80,7 @@ describe('browser status', () => {
|
||||
enabled: true,
|
||||
state: 'enabled',
|
||||
serverName: 'ccs_browser',
|
||||
evalMode: 'readonly',
|
||||
supportsConfigOverrides: true,
|
||||
});
|
||||
} finally {
|
||||
@@ -93,9 +95,11 @@ describe('browser status', () => {
|
||||
enabled: true,
|
||||
user_data_dir: '/config-browser',
|
||||
devtools_port: 9333,
|
||||
eval_mode: 'readwrite',
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
eval_mode: 'disabled',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -108,6 +112,7 @@ describe('browser status', () => {
|
||||
CCS_BROWSER_DEVTOOLS_PORT: '9444',
|
||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9444',
|
||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/test',
|
||||
CCS_BROWSER_EVAL_MODE: 'readwrite',
|
||||
});
|
||||
const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({
|
||||
path: '/usr/local/bin/codex',
|
||||
@@ -125,8 +130,11 @@ describe('browser status', () => {
|
||||
source: 'CCS_BROWSER_USER_DATA_DIR',
|
||||
effectiveUserDataDir: '/env-browser',
|
||||
devtoolsPort: 9444,
|
||||
evalMode: 'readwrite',
|
||||
});
|
||||
expect(status.claude.runtimeEnv?.CCS_BROWSER_DEVTOOLS_PORT).toBe('9444');
|
||||
expect(status.claude.runtimeEnv?.CCS_BROWSER_EVAL_MODE).toBe('readwrite');
|
||||
expect(status.codex.evalMode).toBe('disabled');
|
||||
} finally {
|
||||
runtimeSpy.mockRestore();
|
||||
codexSpy.mockRestore();
|
||||
@@ -140,9 +148,11 @@ describe('browser status', () => {
|
||||
enabled: true,
|
||||
user_data_dir: '/tmp/browser-profile',
|
||||
devtools_port: 9222,
|
||||
eval_mode: 'readonly',
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
eval_mode: 'readonly',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -178,6 +188,7 @@ describe('browser status', () => {
|
||||
CCS_BROWSER_DEVTOOLS_PORT: '50123',
|
||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:50123',
|
||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/legacy',
|
||||
CCS_BROWSER_EVAL_MODE: 'readonly',
|
||||
});
|
||||
const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({
|
||||
path: '/usr/local/bin/codex',
|
||||
@@ -192,8 +203,10 @@ describe('browser status', () => {
|
||||
expect(runtimeSpy.mock.calls[0]?.[0]).toEqual({
|
||||
profileDir: '/legacy-browser',
|
||||
devtoolsPort: undefined,
|
||||
evalMode: 'readonly',
|
||||
});
|
||||
expect(status.claude.runtimeEnv?.CCS_BROWSER_DEVTOOLS_PORT).toBe('50123');
|
||||
expect(status.claude.runtimeEnv?.CCS_BROWSER_EVAL_MODE).toBe('readonly');
|
||||
} finally {
|
||||
runtimeSpy.mockRestore();
|
||||
codexSpy.mockRestore();
|
||||
@@ -207,9 +220,11 @@ describe('browser status', () => {
|
||||
enabled: true,
|
||||
user_data_dir: '/tmp/config-browser',
|
||||
devtools_port: 9222,
|
||||
eval_mode: 'disabled',
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
eval_mode: 'readwrite',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -220,6 +235,7 @@ describe('browser status', () => {
|
||||
CCS_BROWSER_DEVTOOLS_PORT: '9222',
|
||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9222',
|
||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/config',
|
||||
CCS_BROWSER_EVAL_MODE: 'disabled',
|
||||
});
|
||||
const codexSpy = spyOn(codexDetector, 'getCodexBinaryInfo').mockReturnValue({
|
||||
path: '/usr/local/bin/codex',
|
||||
@@ -234,6 +250,7 @@ describe('browser status', () => {
|
||||
expect(runtimeSpy.mock.calls[0]?.[0]).toEqual({
|
||||
profileDir: '/tmp/config-browser',
|
||||
devtoolsPort: '9222',
|
||||
evalMode: 'disabled',
|
||||
});
|
||||
} finally {
|
||||
runtimeSpy.mockRestore();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
@@ -12,6 +12,13 @@ describe('chrome reuse resolver', () => {
|
||||
const originalHome = process.env.HOME;
|
||||
const originalLocalAppData = process.env.LOCALAPPDATA;
|
||||
const originalUserProfile = process.env.USERPROFILE;
|
||||
const originalBrowserUserDataDir = process.env.CCS_BROWSER_USER_DATA_DIR;
|
||||
const originalBrowserProfileDir = process.env.CCS_BROWSER_PROFILE_DIR;
|
||||
const originalBrowserDevtoolsPort = process.env.CCS_BROWSER_DEVTOOLS_PORT;
|
||||
const originalBrowserDevtoolsHost = process.env.CCS_BROWSER_DEVTOOLS_HOST;
|
||||
const originalBrowserDevtoolsHttpUrl = process.env.CCS_BROWSER_DEVTOOLS_HTTP_URL;
|
||||
const originalBrowserDevtoolsWsUrl = process.env.CCS_BROWSER_DEVTOOLS_WS_URL;
|
||||
const originalBrowserEvalMode = process.env.CCS_BROWSER_EVAL_MODE;
|
||||
let tempDirs: string[] = [];
|
||||
let servers: Array<{ stop: () => void }> = [];
|
||||
|
||||
@@ -71,7 +78,7 @@ describe('chrome reuse resolver', () => {
|
||||
return server;
|
||||
}
|
||||
|
||||
async function reserveClosedPort(): Promise<number> {
|
||||
function reserveClosedPort(): number {
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch() {
|
||||
@@ -79,13 +86,27 @@ describe('chrome reuse resolver', () => {
|
||||
},
|
||||
});
|
||||
const port = server.port;
|
||||
server.stop(true);
|
||||
if (port === undefined) {
|
||||
server.stop();
|
||||
throw new Error('Failed to reserve a DevTools test port');
|
||||
}
|
||||
server.stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.CCS_BROWSER_USER_DATA_DIR;
|
||||
delete process.env.CCS_BROWSER_PROFILE_DIR;
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_PORT;
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_HOST;
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_HTTP_URL;
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_WS_URL;
|
||||
delete process.env.CCS_BROWSER_EVAL_MODE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const server of servers) {
|
||||
server.stop(true);
|
||||
server.stop();
|
||||
}
|
||||
servers = [];
|
||||
|
||||
@@ -111,6 +132,48 @@ describe('chrome reuse resolver', () => {
|
||||
} else {
|
||||
process.env.USERPROFILE = originalUserProfile;
|
||||
}
|
||||
|
||||
if (originalBrowserUserDataDir === undefined) {
|
||||
delete process.env.CCS_BROWSER_USER_DATA_DIR;
|
||||
} else {
|
||||
process.env.CCS_BROWSER_USER_DATA_DIR = originalBrowserUserDataDir;
|
||||
}
|
||||
|
||||
if (originalBrowserProfileDir === undefined) {
|
||||
delete process.env.CCS_BROWSER_PROFILE_DIR;
|
||||
} else {
|
||||
process.env.CCS_BROWSER_PROFILE_DIR = originalBrowserProfileDir;
|
||||
}
|
||||
|
||||
if (originalBrowserDevtoolsPort === undefined) {
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_PORT;
|
||||
} else {
|
||||
process.env.CCS_BROWSER_DEVTOOLS_PORT = originalBrowserDevtoolsPort;
|
||||
}
|
||||
|
||||
if (originalBrowserDevtoolsHost === undefined) {
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_HOST;
|
||||
} else {
|
||||
process.env.CCS_BROWSER_DEVTOOLS_HOST = originalBrowserDevtoolsHost;
|
||||
}
|
||||
|
||||
if (originalBrowserDevtoolsHttpUrl === undefined) {
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_HTTP_URL;
|
||||
} else {
|
||||
process.env.CCS_BROWSER_DEVTOOLS_HTTP_URL = originalBrowserDevtoolsHttpUrl;
|
||||
}
|
||||
|
||||
if (originalBrowserDevtoolsWsUrl === undefined) {
|
||||
delete process.env.CCS_BROWSER_DEVTOOLS_WS_URL;
|
||||
} else {
|
||||
process.env.CCS_BROWSER_DEVTOOLS_WS_URL = originalBrowserDevtoolsWsUrl;
|
||||
}
|
||||
|
||||
if (originalBrowserEvalMode === undefined) {
|
||||
delete process.env.CCS_BROWSER_EVAL_MODE;
|
||||
} else {
|
||||
process.env.CCS_BROWSER_EVAL_MODE = originalBrowserEvalMode;
|
||||
}
|
||||
});
|
||||
|
||||
it('uses explicit profile-dir before the default path and resolves the websocket target', async () => {
|
||||
@@ -175,7 +238,7 @@ describe('chrome reuse resolver', () => {
|
||||
it('throws a clear error when DevToolsActivePort metadata is missing', async () => {
|
||||
const profileDir = createTempDir('ccs-chrome-missing-metadata-');
|
||||
|
||||
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
`Chrome reuse metadata not found: ${path.join(profileDir, 'DevToolsActivePort')}`
|
||||
);
|
||||
});
|
||||
@@ -184,17 +247,17 @@ describe('chrome reuse resolver', () => {
|
||||
const profileDir = createTempDir('ccs-chrome-invalid-metadata-');
|
||||
writeDevToolsActivePort(profileDir, 'not-a-port\n/devtools/browser/target');
|
||||
|
||||
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
`Chrome reuse metadata is invalid: ${path.join(profileDir, 'DevToolsActivePort')}`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws before launch fallback when the DevTools endpoint is stale or unreachable', async () => {
|
||||
const profileDir = createTempDir('ccs-chrome-unreachable-');
|
||||
const port = await reserveClosedPort();
|
||||
const port = reserveClosedPort();
|
||||
writeDevToolsActivePort(profileDir, `${port}\n/devtools/browser/stale`);
|
||||
|
||||
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
`Chrome DevTools endpoint is unreachable: http://127.0.0.1:${port}`
|
||||
);
|
||||
});
|
||||
@@ -204,7 +267,7 @@ describe('chrome reuse resolver', () => {
|
||||
const server = await startDevToolsServer({ Browser: 'Chrome/136.0.0.0' });
|
||||
writeDevToolsActivePort(profileDir, `${server.port}\n/devtools/browser/no-ws`);
|
||||
|
||||
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
`Chrome DevTools endpoint did not provide a websocket target: http://127.0.0.1:${server.port}/json/version`
|
||||
);
|
||||
});
|
||||
@@ -214,7 +277,7 @@ describe('chrome reuse resolver', () => {
|
||||
const server = await startFailingDevToolsServer(500);
|
||||
writeDevToolsActivePort(profileDir, `${server.port}\n/devtools/browser/bad-status`);
|
||||
|
||||
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
`Chrome DevTools endpoint is unreachable: http://127.0.0.1:${server.port}`
|
||||
);
|
||||
});
|
||||
@@ -224,7 +287,7 @@ describe('chrome reuse resolver', () => {
|
||||
const server = await startMalformedJsonDevToolsServer();
|
||||
writeDevToolsActivePort(profileDir, `${server.port}\n/devtools/browser/bad-json`);
|
||||
|
||||
await expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
expect(resolveBrowserRuntimeEnv({ profileDir })).rejects.toThrow(
|
||||
`Chrome DevTools endpoint is unreachable: http://127.0.0.1:${server.port}`
|
||||
);
|
||||
});
|
||||
@@ -262,7 +325,7 @@ describe('chrome reuse resolver', () => {
|
||||
'missing-profile'
|
||||
);
|
||||
|
||||
await expect(resolveBrowserRuntimeEnv({ profileDir: missingProfileDir })).rejects.toThrow(
|
||||
expect(resolveBrowserRuntimeEnv({ profileDir: missingProfileDir })).rejects.toThrow(
|
||||
`Chrome profile directory is invalid: ${missingProfileDir}`
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user