mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 14:17:11 +00:00
feat: add first-class CCS WebSearch runtime
This commit is contained in:
1 parent
c45b2a1138
commit
e260df7178
26 files changed
+1241
-158
No files matched your search
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { appendThirdPartyWebSearchToolArgs } from '../../../../src/utils/websearch/claude-tool-args';
|
||||
|
||||
describe('appendThirdPartyWebSearchToolArgs', () => {
|
||||
it('appends native WebSearch suppression when no tool flags are present', () => {
|
||||
expect(appendThirdPartyWebSearchToolArgs(['smoke'])).toEqual([
|
||||
'smoke',
|
||||
'--disallowedTools',
|
||||
'WebSearch',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not append duplicate suppression when WebSearch is already disallowed', () => {
|
||||
expect(appendThirdPartyWebSearchToolArgs(['smoke', '--disallowedTools', 'WebSearch'])).toEqual(
|
||||
['smoke', '--disallowedTools', 'WebSearch']
|
||||
);
|
||||
});
|
||||
|
||||
it('detects comma-separated disallowed tool values', () => {
|
||||
expect(appendThirdPartyWebSearchToolArgs(['smoke', '--disallowedTools=Read,WebSearch'])).toEqual(
|
||||
['smoke', '--disallowedTools=Read,WebSearch']
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { getHookPath } from '../../../../src/utils/websearch/hook-config';
|
||||
import {
|
||||
ensureWebSearchMcp,
|
||||
getWebSearchMcpServerName,
|
||||
getWebSearchMcpServerPath,
|
||||
uninstallWebSearchMcp,
|
||||
} from '../../../../src/utils/websearch/mcp-installer';
|
||||
|
||||
describe('ensureWebSearchMcp', () => {
|
||||
let tempHome: string | undefined;
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
function setupTempHome(): string {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-websearch-mcp-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
return tempHome;
|
||||
}
|
||||
|
||||
function getCcsDir(): string {
|
||||
if (!tempHome) {
|
||||
throw new Error('tempHome not initialized');
|
||||
}
|
||||
return path.join(tempHome, '.ccs');
|
||||
}
|
||||
|
||||
function writeEnabledConfig(): void {
|
||||
const ccsDir = getCcsDir();
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
['version: 12', 'websearch:', ' enabled: true', ' providers:', ' duckduckgo:', ' enabled: true', ''].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
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 MCP server and preserves existing user mcpServers entries', () => {
|
||||
setupTempHome();
|
||||
writeEnabledConfig();
|
||||
|
||||
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(ensureWebSearchMcp()).toBe(true);
|
||||
expect(fs.existsSync(getHookPath())).toBe(true);
|
||||
expect(fs.existsSync(getWebSearchMcpServerPath())).toBe(true);
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(claudeUserConfigPath, 'utf8')) as {
|
||||
mcpServers: Record<string, { command: string; args: string[] }>;
|
||||
};
|
||||
|
||||
expect(config.mcpServers.existing).toEqual({ command: 'uvx', args: ['some-server'] });
|
||||
expect(config.mcpServers[getWebSearchMcpServerName()]).toEqual({
|
||||
command: 'node',
|
||||
args: [getWebSearchMcpServerPath()],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false and preserves malformed ~/.claude.json', () => {
|
||||
setupTempHome();
|
||||
writeEnabledConfig();
|
||||
|
||||
const claudeUserConfigPath = path.join(tempHome as string, '.claude.json');
|
||||
fs.writeFileSync(claudeUserConfigPath, '{ invalid json', 'utf8');
|
||||
|
||||
expect(ensureWebSearchMcp()).toBe(false);
|
||||
expect(fs.readFileSync(claudeUserConfigPath, 'utf8')).toBe('{ invalid json');
|
||||
});
|
||||
|
||||
it('removes the managed MCP runtime while preserving unrelated server entries', () => {
|
||||
setupTempHome();
|
||||
writeEnabledConfig();
|
||||
|
||||
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(ensureWebSearchMcp()).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'] },
|
||||
[getWebSearchMcpServerName()]: { command: 'node', args: ['/tmp/override.cjs'] },
|
||||
},
|
||||
otherKey: 'keep-me',
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
expect(uninstallWebSearchMcp()).toBe(true);
|
||||
expect(fs.existsSync(getWebSearchMcpServerPath())).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'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user