Files
ccs/tests/unit/utils/browser/mcp-installer.test.ts
T
walkerandClaude Opus 4.6 eb8149e8fa feat(browser): 接入 CCS browser MCP 与最小交互闭环
新增 browser MCP 安装、Chrome 复用与运行时接线,
实现 navigate、click、type、take_screenshot 四个 phase-1 工具并补齐相关单测。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 11:19:16 +08:00

233 lines
6.7 KiB
TypeScript

import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as browserInstaller from '../../../../src/utils/browser/mcp-installer';
import {
ensureBrowserMcp,
ensureBrowserMcpConfig,
ensureBrowserMcpOrThrow,
getBrowserMcpServerName,
getBrowserMcpServerPath,
syncBrowserMcpToConfigDir,
uninstallBrowserMcp,
} from '../../../../src/utils/browser';
describe('ensureBrowserMcp', () => {
let tempHome: string | undefined;
let originalCcsHome: string | undefined;
function setupTempHome(): string {
const nextTempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-browser-mcp-'));
tempHome = nextTempHome;
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = nextTempHome;
return nextTempHome;
}
function getManagedConfig() {
return {
type: 'stdio',
command: 'node',
args: [getBrowserMcpServerPath()],
env: {
NODE_PATH: path.join(process.cwd(), 'node_modules'),
},
};
}
afterEach(() => {
mock.restore();
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
tempHome = undefined;
originalCcsHome = undefined;
});
it('installs the bundled browser MCP server and preserves existing user mcpServers entries', () => {
setupTempHome();
const claudeUserConfigPath = path.join(tempHome as string, '.claude.json');
fs.writeFileSync(
claudeUserConfigPath,
JSON.stringify(
{
mcpServers: {
existing: { command: 'uvx', args: ['some-server'] },
},
},
null,
2
) + '\n',
'utf8'
);
expect(ensureBrowserMcp()).toBe(true);
expect(fs.existsSync(getBrowserMcpServerPath())).toBe(true);
const config = JSON.parse(fs.readFileSync(claudeUserConfigPath, 'utf8')) as {
mcpServers: Record<string, unknown>;
};
expect(config.mcpServers.existing).toEqual({ command: 'uvx', args: ['some-server'] });
expect(config.mcpServers[getBrowserMcpServerName()]).toEqual(getManagedConfig());
});
it('preserves the existing ~/.claude.json permissions when provisioning browser MCP', () => {
setupTempHome();
const claudeUserConfigPath = path.join(tempHome as string, '.claude.json');
fs.writeFileSync(claudeUserConfigPath, JSON.stringify({ existing: true }, null, 2) + '\n', {
encoding: 'utf8',
mode: 0o600,
});
fs.chmodSync(claudeUserConfigPath, 0o600);
expect(ensureBrowserMcpConfig()).toBe(true);
expect(fs.statSync(claudeUserConfigPath).mode & 0o777).toBe(0o600);
});
it('copies the bundled browser MCP artifact while preserving source permissions', () => {
setupTempHome();
const bundledServerPath = path.join(
process.cwd(),
'lib',
'mcp',
'ccs-browser-server.cjs'
);
const originalMode = fs.statSync(bundledServerPath).mode & 0o777;
expect(ensureBrowserMcp()).toBe(true);
expect(fs.statSync(getBrowserMcpServerPath()).mode & 0o777).toBe(originalMode);
});
it('reconciles installed browser MCP permissions when contents already match', () => {
setupTempHome();
const bundledServerPath = path.join(
process.cwd(),
'lib',
'mcp',
'ccs-browser-server.cjs'
);
const originalMode = fs.statSync(bundledServerPath).mode & 0o777;
expect(ensureBrowserMcp()).toBe(true);
fs.chmodSync(getBrowserMcpServerPath(), 0o600);
expect(ensureBrowserMcp()).toBe(true);
expect(fs.statSync(getBrowserMcpServerPath()).mode & 0o777).toBe(originalMode);
});
it('removes the managed browser runtime while preserving unrelated server entries', () => {
setupTempHome();
const claudeUserConfigPath = path.join(tempHome as string, '.claude.json');
fs.writeFileSync(
claudeUserConfigPath,
JSON.stringify(
{
mcpServers: {
existing: { command: 'uvx', args: ['some-server'] },
},
},
null,
2
) + '\n',
'utf8'
);
expect(ensureBrowserMcp()).toBe(true);
const instancePath = path.join(tempHome as string, '.ccs', 'instances', 'work');
fs.mkdirSync(instancePath, { recursive: true });
fs.writeFileSync(
path.join(instancePath, '.claude.json'),
JSON.stringify(
{
mcpServers: {
existing: { command: 'uvx', args: ['instance-server'] },
[getBrowserMcpServerName()]: { command: 'node', args: ['/tmp/override.cjs'] },
},
otherKey: 'keep-me',
},
null,
2
) + '\n',
'utf8'
);
expect(uninstallBrowserMcp()).toBe(true);
expect(fs.existsSync(getBrowserMcpServerPath())).toBe(false);
const globalConfig = JSON.parse(fs.readFileSync(claudeUserConfigPath, 'utf8')) as {
mcpServers: Record<string, { command: string; args: string[] }>;
};
expect(globalConfig.mcpServers).toEqual({
existing: { command: 'uvx', args: ['some-server'] },
});
const instanceConfig = JSON.parse(
fs.readFileSync(path.join(instancePath, '.claude.json'), 'utf8')
) as {
otherKey: string;
mcpServers: Record<string, { command: string; args: string[] }>;
};
expect(instanceConfig.otherKey).toBe('keep-me');
expect(instanceConfig.mcpServers).toEqual({
existing: { command: 'uvx', args: ['instance-server'] },
});
});
it('syncs the managed browser MCP entry into an instance config dir', () => {
setupTempHome();
fs.writeFileSync(
path.join(tempHome as string, '.claude.json'),
JSON.stringify(
{
mcpServers: {
[getBrowserMcpServerName()]: getManagedConfig(),
},
},
null,
2
) + '\n',
'utf8'
);
const instancePath = path.join(tempHome as string, '.ccs', 'instances', 'work');
fs.mkdirSync(instancePath, { recursive: true });
expect(syncBrowserMcpToConfigDir(instancePath)).toBe(true);
const instanceConfig = JSON.parse(
fs.readFileSync(path.join(instancePath, '.claude.json'), 'utf8')
) as {
mcpServers: Record<string, unknown>;
};
expect(instanceConfig.mcpServers[getBrowserMcpServerName()]).toEqual(getManagedConfig());
});
it('throws when the bundled browser MCP runtime cannot be prepared', () => {
setupTempHome();
const ensureSpy = spyOn(browserInstaller, 'ensureBrowserMcp').mockReturnValue(false);
expect(() => ensureBrowserMcpOrThrow()).toThrow(
'Browser MCP is enabled, but CCS could not prepare the local browser tool.'
);
expect(ensureSpy).toHaveBeenCalled();
});
});