mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-12 17:21:19 +08:00
chore: bump version to 7.2.7
- Enhance smart-search with advanced MCP integration - Add GEMINI_API_KEY configuration support in codexlens - Update MCP server with new tool handlers - Add tests for smart-search MCP usage - Update documentation
This commit is contained in:
@@ -11,26 +11,46 @@ import { dirname, join } from 'node:path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const serverPath = join(__dirname, '../bin/ccw-mcp.js');
|
||||
|
||||
async function waitForServerStart(child) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Server start timeout'));
|
||||
}, 5000);
|
||||
|
||||
const onData = (data) => {
|
||||
const message = data.toString();
|
||||
if (message.includes('started')) {
|
||||
clearTimeout(timeout);
|
||||
child.stderr.off('data', onData);
|
||||
child.off('exit', onExit);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const onExit = (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
child.stderr.off('data', onData);
|
||||
reject(new Error(`Server exited before start (code=${code}, signal=${signal})`));
|
||||
};
|
||||
|
||||
child.stderr.on('data', onData);
|
||||
child.once('exit', onExit);
|
||||
});
|
||||
}
|
||||
|
||||
describe('MCP Server', () => {
|
||||
let serverProcess;
|
||||
|
||||
before(async () => {
|
||||
// Start the MCP server
|
||||
const serverPath = join(__dirname, '../bin/ccw-mcp.js');
|
||||
serverProcess = spawn('node', [serverPath], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// Wait for server to start
|
||||
await new Promise((resolve) => {
|
||||
serverProcess.stderr.once('data', (data) => {
|
||||
const message = data.toString();
|
||||
if (message.includes('started')) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
await waitForServerStart(serverProcess);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@@ -157,4 +177,43 @@ describe('MCP Server', () => {
|
||||
// Error could be "not enabled" (filtered by default tools) or "not found" (all tools enabled)
|
||||
assert(response.result.content[0].text.includes('not enabled') || response.result.content[0].text.includes('not found'));
|
||||
});
|
||||
it('should exit when stdout disconnects during a request', async () => {
|
||||
const disconnectedProcess = spawn('node', [serverPath], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForServerStart(disconnectedProcess);
|
||||
|
||||
const exitPromise = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
disconnectedProcess.kill('SIGKILL');
|
||||
reject(new Error('Server did not exit after stdout disconnect'));
|
||||
}, 1500);
|
||||
|
||||
disconnectedProcess.once('exit', (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ code, signal });
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate the MCP client disappearing before the server sends its response.
|
||||
disconnectedProcess.stdout.destroy();
|
||||
disconnectedProcess.stdin.write(JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 4,
|
||||
method: 'tools/list',
|
||||
params: {}
|
||||
}) + '\n');
|
||||
|
||||
const exitResult = await exitPromise;
|
||||
|
||||
assert.equal(exitResult.code, 0);
|
||||
assert.equal(exitResult.signal, null);
|
||||
} finally {
|
||||
if (disconnectedProcess.exitCode === null) {
|
||||
disconnectedProcess.kill('SIGKILL');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +45,19 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
assert.match(props.apiMaxWorkers.description, /endpoint pool/i);
|
||||
assert.match(schema.description, /apiMaxWorkers=8/i);
|
||||
assert.match(props.path.description, /single file path/i);
|
||||
assert.ok(props.output_mode.enum.includes('ace'));
|
||||
assert.match(props.output_mode.description, /ACE-style/i);
|
||||
assert.equal(props.output_mode.default, 'ace');
|
||||
});
|
||||
|
||||
it('defaults auto embedding warmup to enabled unless explicitly disabled', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const { __testables } = smartSearchModule;
|
||||
assert.equal(__testables.isAutoEmbedMissingEnabled(undefined), true);
|
||||
assert.equal(__testables.isAutoEmbedMissingEnabled({}), true);
|
||||
assert.equal(__testables.isAutoEmbedMissingEnabled({ embedding_auto_embed_missing: true }), true);
|
||||
assert.equal(__testables.isAutoEmbedMissingEnabled({ embedding_auto_embed_missing: false }), false);
|
||||
});
|
||||
|
||||
it('honors explicit small limit values', async () => {
|
||||
@@ -58,6 +71,7 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
action: 'search',
|
||||
query: 'hit',
|
||||
path: dir,
|
||||
output_mode: 'full',
|
||||
limit: 1,
|
||||
regex: false,
|
||||
tokenize: false,
|
||||
@@ -82,6 +96,7 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
action: 'search',
|
||||
query: 'TARGET_TOKEN',
|
||||
path: target,
|
||||
output_mode: 'full',
|
||||
regex: false,
|
||||
tokenize: false,
|
||||
});
|
||||
@@ -112,6 +127,7 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
action: 'search',
|
||||
query: wrappedQuery,
|
||||
path: wrappedPath,
|
||||
output_mode: 'full',
|
||||
regex: false,
|
||||
caseSensitive: false,
|
||||
});
|
||||
@@ -121,6 +137,66 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
assert.ok(toolResult.result.results.length >= 1);
|
||||
});
|
||||
|
||||
it('falls back to literal ripgrep matching for invalid regex-like code queries', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const dir = createWorkspace();
|
||||
const target = join(dir, 'component.ts');
|
||||
writeFileSync(target, 'defineExpose({ handleResize });\n');
|
||||
|
||||
const toolResult = await smartSearchModule.handler({
|
||||
action: 'search',
|
||||
query: 'defineExpose({ handleResize',
|
||||
path: dir,
|
||||
output_mode: 'full',
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
assert.equal(toolResult.success, true, toolResult.error);
|
||||
assert.equal(toolResult.result.success, true);
|
||||
assert.ok(toolResult.result.results.length >= 1);
|
||||
assert.match(toolResult.result.metadata.warning, /literal ripgrep matching/i);
|
||||
});
|
||||
|
||||
it('renders grouped ace-style output by default with multi-line chunks', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const dir = createWorkspace();
|
||||
const target = join(dir, 'ace-target.ts');
|
||||
writeFileSync(target, [
|
||||
'const before = 1;',
|
||||
'const TARGET_TOKEN = 1;',
|
||||
'const after = 2;',
|
||||
'',
|
||||
'function useToken() {',
|
||||
' return TARGET_TOKEN;',
|
||||
'}',
|
||||
].join('\n'));
|
||||
|
||||
const toolResult = await smartSearchModule.handler({
|
||||
action: 'search',
|
||||
query: 'TARGET_TOKEN',
|
||||
path: dir,
|
||||
contextLines: 1,
|
||||
regex: false,
|
||||
tokenize: false,
|
||||
});
|
||||
|
||||
assert.equal(toolResult.success, true, toolResult.error);
|
||||
assert.equal(toolResult.result.success, true);
|
||||
assert.equal(toolResult.result.results.format, 'ace');
|
||||
assert.equal(Array.isArray(toolResult.result.results.groups), true);
|
||||
assert.equal(Array.isArray(toolResult.result.results.sections), true);
|
||||
assert.equal(toolResult.result.results.groups.length, 1);
|
||||
assert.equal(toolResult.result.results.groups[0].sections.length, 2);
|
||||
assert.match(toolResult.result.results.text, /The following code sections were retrieved:/);
|
||||
assert.match(toolResult.result.results.text, /Path: .*ace-target\.ts/);
|
||||
assert.match(toolResult.result.results.text, /Chunk 1: lines 1-3/);
|
||||
assert.match(toolResult.result.results.text, />\s+2 \| const TARGET_TOKEN = 1;/);
|
||||
assert.match(toolResult.result.results.text, /Chunk 2: lines 5-7/);
|
||||
assert.equal(toolResult.result.metadata.pagination.total >= 1, true);
|
||||
});
|
||||
|
||||
it('defaults embed selection to local-fast for bulk indexing', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
@@ -181,6 +257,50 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
assert.equal(smartSearchModule.__testables.hasCentralizedVectorArtifacts(dir), true);
|
||||
});
|
||||
|
||||
it('recognizes CodexLens CLI compatibility failures and invalid regex fallback', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const compatibilityError = [
|
||||
'UsageError: Got unexpected extra arguments (20 0 fts)',
|
||||
'TypeError: TyperArgument.make_metavar() takes 1 positional argument but 2 were given',
|
||||
].join('\n');
|
||||
|
||||
assert.equal(
|
||||
smartSearchModule.__testables.isCodexLensCliCompatibilityError(compatibilityError),
|
||||
true,
|
||||
);
|
||||
|
||||
const resolution = smartSearchModule.__testables.resolveRipgrepQueryMode(
|
||||
'defineExpose({ handleResize',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(resolution.regex, false);
|
||||
assert.equal(resolution.literalFallback, true);
|
||||
assert.match(resolution.warning, /literal ripgrep matching/i);
|
||||
});
|
||||
|
||||
it('builds actionable index suggestions for unhealthy index states', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const suggestions = smartSearchModule.__testables.buildIndexSuggestions(
|
||||
{
|
||||
indexed: true,
|
||||
has_embeddings: false,
|
||||
embeddings_coverage_percent: 0,
|
||||
warning: 'Index exists but no embeddings generated. Run smart_search(action="embed") to build the vector index.',
|
||||
},
|
||||
{
|
||||
workingDirectory: 'D:/tmp/demo',
|
||||
searchPaths: ['.'],
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(Array.isArray(suggestions), true);
|
||||
assert.match(suggestions[0].command, /smart_search\(action="embed"/);
|
||||
});
|
||||
|
||||
it('surfaces backend failure details when fuzzy search fully fails', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
@@ -189,6 +309,7 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
action: 'search',
|
||||
query: 'TARGET_TOKEN',
|
||||
path: missingPath,
|
||||
output_mode: 'full',
|
||||
regex: false,
|
||||
tokenize: false,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user