mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-18 18:48:48 +08:00
feat: enhance search, ranking, reranker and CLI tooling across ccw and codex-lens
Major improvements to smart-search, chain-search cascade, ranking pipeline, reranker factory, CLI history store, codex-lens integration, and uv-manager. Simplify command-generator skill by inlining phases. Add comprehensive tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
118
ccw/tests/cli-history-cross-project.test.js
Normal file
118
ccw/tests/cli-history-cross-project.test.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Cross-project regression coverage for `ccw cli history` and `ccw cli detail`.
|
||||
*/
|
||||
|
||||
import { after, afterEach, before, describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const TEST_CCW_HOME = mkdtempSync(join(tmpdir(), 'ccw-cli-history-cross-home-'));
|
||||
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
|
||||
|
||||
const cliCommandPath = new URL('../dist/commands/cli.js', import.meta.url).href;
|
||||
const cliExecutorPath = new URL('../dist/tools/cli-executor.js', import.meta.url).href;
|
||||
const historyStorePath = new URL('../dist/tools/cli-history-store.js', import.meta.url).href;
|
||||
|
||||
function createConversation({ id, prompt, updatedAt }) {
|
||||
return {
|
||||
id,
|
||||
created_at: updatedAt,
|
||||
updated_at: updatedAt,
|
||||
tool: 'gemini',
|
||||
model: 'default',
|
||||
mode: 'analysis',
|
||||
category: 'user',
|
||||
total_duration_ms: 456,
|
||||
turn_count: 1,
|
||||
latest_status: 'success',
|
||||
turns: [
|
||||
{
|
||||
turn: 1,
|
||||
timestamp: updatedAt,
|
||||
prompt,
|
||||
duration_ms: 456,
|
||||
status: 'success',
|
||||
exit_code: 0,
|
||||
output: {
|
||||
stdout: 'CROSS PROJECT OK',
|
||||
stderr: '',
|
||||
truncated: false,
|
||||
cached: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe('ccw cli history/detail cross-project', async () => {
|
||||
let cliModule;
|
||||
let cliExecutorModule;
|
||||
let historyStoreModule;
|
||||
|
||||
before(async () => {
|
||||
cliModule = await import(cliCommandPath);
|
||||
cliExecutorModule = await import(cliExecutorPath);
|
||||
historyStoreModule = await import(historyStorePath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restoreAll();
|
||||
try {
|
||||
historyStoreModule?.closeAllStores?.();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
try {
|
||||
historyStoreModule?.closeAllStores?.();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
rmSync(TEST_CCW_HOME, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('finds history and detail for executions stored in another registered project', async () => {
|
||||
const projectRoot = mkdtempSync(join(tmpdir(), 'ccw-cli-cross-project-history-'));
|
||||
const unrelatedCwd = mkdtempSync(join(tmpdir(), 'ccw-cli-cross-project-cwd-'));
|
||||
const previousCwd = process.cwd();
|
||||
|
||||
try {
|
||||
const store = new historyStoreModule.CliHistoryStore(projectRoot);
|
||||
store.saveConversation(createConversation({
|
||||
id: 'CONV-CROSS-PROJECT-1',
|
||||
prompt: 'Cross project prompt',
|
||||
updatedAt: new Date('2025-02-01T00:00:01.000Z').toISOString(),
|
||||
}));
|
||||
store.close();
|
||||
|
||||
const logs = [];
|
||||
mock.method(console, 'log', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
|
||||
process.chdir(unrelatedCwd);
|
||||
|
||||
await cliModule.cliCommand('history', [], { limit: '20' });
|
||||
assert.ok(logs.some((line) => line.includes('CONV-CROSS-PROJECT-1')));
|
||||
|
||||
await cliExecutorModule.getExecutionHistoryAsync(projectRoot, { limit: 1 });
|
||||
|
||||
logs.length = 0;
|
||||
await cliModule.cliCommand('detail', ['CONV-CROSS-PROJECT-1'], {});
|
||||
assert.ok(logs.some((line) => line.includes('Conversation Detail')));
|
||||
assert.ok(logs.some((line) => line.includes('CONV-CROSS-PROJECT-1')));
|
||||
assert.ok(logs.some((line) => line.includes('Cross project prompt')));
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
rmSync(unrelatedCwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -123,6 +123,39 @@ describe('ccw cli output --final', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('loads cached output from another registered project without --project', async () => {
|
||||
const projectRoot = createTestProjectRoot();
|
||||
const unrelatedCwd = createTestProjectRoot();
|
||||
const previousCwd = process.cwd();
|
||||
const store = new historyStoreModule.CliHistoryStore(projectRoot);
|
||||
|
||||
try {
|
||||
store.saveConversation(createConversation({
|
||||
id: 'EXEC-CROSS-PROJECT-OUTPUT',
|
||||
stdoutFull: 'cross project raw output',
|
||||
parsedOutput: 'cross project parsed output',
|
||||
finalOutput: 'cross project final output',
|
||||
}));
|
||||
|
||||
process.chdir(unrelatedCwd);
|
||||
|
||||
const logs = [];
|
||||
mock.method(console, 'log', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
await cliModule.cliCommand('output', ['EXEC-CROSS-PROJECT-OUTPUT'], {});
|
||||
|
||||
assert.equal(logs.at(-1), 'cross project final output');
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
store.close();
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
rmSync(unrelatedCwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('fails fast for explicit --final when no final agent result can be recovered', async () => {
|
||||
const projectRoot = createTestProjectRoot();
|
||||
const store = new historyStoreModule.CliHistoryStore(projectRoot);
|
||||
@@ -159,4 +192,34 @@ describe('ccw cli output --final', async () => {
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('prints CCW execution ID guidance when output cannot find the requested execution', async () => {
|
||||
const projectRoot = createTestProjectRoot();
|
||||
const previousCwd = process.cwd();
|
||||
|
||||
try {
|
||||
process.chdir(projectRoot);
|
||||
|
||||
const errors = [];
|
||||
const exitCodes = [];
|
||||
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', (...args) => {
|
||||
errors.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(process, 'exit', (code) => {
|
||||
exitCodes.push(code);
|
||||
});
|
||||
|
||||
await cliModule.cliCommand('output', ['rebuttal-structure-analysis'], {});
|
||||
|
||||
assert.deepEqual(exitCodes, [1]);
|
||||
assert.ok(errors.some((line) => line.includes('real CCW execution ID')));
|
||||
assert.ok(errors.some((line) => line.includes('CCW_EXEC_ID')));
|
||||
assert.ok(errors.some((line) => line.includes('ccw cli show or ccw cli history')));
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,6 +163,42 @@ describe('ccw cli show running time formatting', async () => {
|
||||
assert.match(rendered, /1h\.\.\./);
|
||||
});
|
||||
|
||||
it('lists executions from other registered projects in show output', async () => {
|
||||
const projectRoot = mkdtempSync(join(tmpdir(), 'ccw-cli-show-cross-project-'));
|
||||
const unrelatedCwd = mkdtempSync(join(tmpdir(), 'ccw-cli-show-cross-cwd-'));
|
||||
const previousCwd = process.cwd();
|
||||
|
||||
try {
|
||||
process.chdir(unrelatedCwd);
|
||||
const store = new historyStoreModule.CliHistoryStore(projectRoot);
|
||||
store.saveConversation(createConversationRecord({
|
||||
id: 'EXEC-CROSS-PROJECT-SHOW',
|
||||
prompt: 'cross project show prompt',
|
||||
updatedAt: new Date('2025-02-02T00:00:00.000Z').toISOString(),
|
||||
durationMs: 1800,
|
||||
}));
|
||||
store.close();
|
||||
|
||||
stubActiveExecutionsResponse([]);
|
||||
|
||||
const logs = [];
|
||||
mock.method(console, 'log', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
await cliModule.cliCommand('show', [], {});
|
||||
|
||||
const rendered = logs.join('\n');
|
||||
assert.match(rendered, /EXEC-CROSS-PROJECT-SHOW/);
|
||||
assert.match(rendered, /cross project show prompt/);
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
rmSync(unrelatedCwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('suppresses stale running rows when saved history is newer than the active start time', async () => {
|
||||
const projectRoot = mkdtempSync(join(tmpdir(), 'ccw-cli-show-stale-project-'));
|
||||
const previousCwd = process.cwd();
|
||||
|
||||
@@ -13,6 +13,38 @@ after(() => {
|
||||
});
|
||||
|
||||
describe('CodexLens CLI compatibility retries', () => {
|
||||
it('builds hidden Python spawn options for CLI invocations', async () => {
|
||||
const moduleUrl = new URL(`../dist/tools/codex-lens.js?spawn-opts=${Date.now()}`, import.meta.url).href;
|
||||
const { __testables } = await import(moduleUrl);
|
||||
|
||||
const options = __testables.buildCodexLensSpawnOptions(tmpdir(), 12345);
|
||||
|
||||
assert.equal(options.cwd, tmpdir());
|
||||
assert.equal(options.shell, false);
|
||||
assert.equal(options.timeout, 12345);
|
||||
assert.equal(options.windowsHide, true);
|
||||
assert.equal(options.env.PYTHONIOENCODING, 'utf-8');
|
||||
});
|
||||
|
||||
it('probes Python version without a shell-backed console window', async () => {
|
||||
const moduleUrl = new URL(`../dist/tools/codex-lens.js?python-probe=${Date.now()}`, import.meta.url).href;
|
||||
const { __testables } = await import(moduleUrl);
|
||||
const probeCalls = [];
|
||||
|
||||
const version = __testables.probePythonVersion({ command: 'python', args: [], display: 'python' }, (command, args, options) => {
|
||||
probeCalls.push({ command, args, options });
|
||||
return { status: 0, stdout: '', stderr: 'Python 3.11.9\n' };
|
||||
});
|
||||
|
||||
assert.equal(version, 'Python 3.11.9');
|
||||
assert.equal(probeCalls.length, 1);
|
||||
assert.equal(probeCalls[0].command, 'python');
|
||||
assert.deepEqual(probeCalls[0].args, ['--version']);
|
||||
assert.equal(probeCalls[0].options.shell, false);
|
||||
assert.equal(probeCalls[0].options.windowsHide, true);
|
||||
assert.equal(probeCalls[0].options.env.PYTHONIOENCODING, 'utf-8');
|
||||
});
|
||||
|
||||
it('initializes a tiny index even when CLI emits compatibility conflicts first', async () => {
|
||||
const moduleUrl = new URL(`../dist/tools/codex-lens.js?compat=${Date.now()}`, import.meta.url).href;
|
||||
const { checkVenvStatus, executeCodexLens } = await import(moduleUrl);
|
||||
@@ -32,4 +64,76 @@ describe('CodexLens CLI compatibility retries', () => {
|
||||
assert.equal(result.success, true, result.error ?? 'Expected init to succeed');
|
||||
assert.ok((result.output ?? '').length > 0 || (result.warning ?? '').length > 0, 'Expected init output or compatibility warning');
|
||||
});
|
||||
|
||||
it('synthesizes a machine-readable fallback when JSON search output is empty', async () => {
|
||||
const moduleUrl = new URL(`../dist/tools/codex-lens.js?compat-empty=${Date.now()}`, import.meta.url).href;
|
||||
const { __testables } = await import(moduleUrl);
|
||||
|
||||
const normalized = __testables.normalizeSearchCommandResult(
|
||||
{ success: true },
|
||||
{ query: 'missing symbol', cwd: tmpdir(), limit: 5, filesOnly: false },
|
||||
);
|
||||
|
||||
assert.equal(normalized.success, true);
|
||||
assert.match(normalized.warning ?? '', /empty stdout/i);
|
||||
assert.deepEqual(normalized.results, {
|
||||
success: true,
|
||||
result: {
|
||||
query: 'missing symbol',
|
||||
count: 0,
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns structured semantic search results for a local embedded workspace', async () => {
|
||||
const codexLensUrl = new URL(`../dist/tools/codex-lens.js?compat-search=${Date.now()}`, import.meta.url).href;
|
||||
const smartSearchUrl = new URL(`../dist/tools/smart-search.js?compat-search=${Date.now()}`, import.meta.url).href;
|
||||
const codexLensModule = await import(codexLensUrl);
|
||||
const smartSearchModule = await import(smartSearchUrl);
|
||||
|
||||
const ready = await codexLensModule.checkVenvStatus(true);
|
||||
if (!ready.ready) {
|
||||
console.log('Skipping: CodexLens not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const semantic = await codexLensModule.checkSemanticStatus();
|
||||
if (!semantic.available) {
|
||||
console.log('Skipping: semantic dependencies not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectDir = mkdtempSync(join(tmpdir(), 'codexlens-search-'));
|
||||
tempDirs.push(projectDir);
|
||||
writeFileSync(
|
||||
join(projectDir, 'sample.ts'),
|
||||
'export function greet(name) { return `hello ${name}`; }\nexport const sum = (a, b) => a + b;\n',
|
||||
);
|
||||
|
||||
const init = await smartSearchModule.handler({ action: 'init', path: projectDir });
|
||||
assert.equal(init.success, true, init.error ?? 'Expected smart-search init to succeed');
|
||||
|
||||
const embed = await smartSearchModule.handler({
|
||||
action: 'embed',
|
||||
path: projectDir,
|
||||
embeddingBackend: 'local',
|
||||
force: true,
|
||||
});
|
||||
assert.equal(embed.success, true, embed.error ?? 'Expected smart-search embed to succeed');
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'search',
|
||||
path: projectDir,
|
||||
query: 'greet function',
|
||||
mode: 'semantic',
|
||||
format: 'json',
|
||||
});
|
||||
|
||||
assert.equal(result.success, true, result.error ?? 'Expected semantic search compatibility fallback to succeed');
|
||||
const payload = result.results?.result ?? result.results;
|
||||
assert.ok(Array.isArray(payload?.results), 'Expected structured search results payload');
|
||||
assert.ok(payload.results.length > 0, 'Expected at least one structured semantic search result');
|
||||
assert.doesNotMatch(result.error ?? '', /unexpected extra arguments/i);
|
||||
});
|
||||
});
|
||||
|
||||
66
ccw/tests/codexlens-path.test.js
Normal file
66
ccw/tests/codexlens-path.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { after, afterEach, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { createRequire, syncBuiltinESMExports } from 'node:module';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('node:fs');
|
||||
|
||||
const originalExistsSync = fs.existsSync;
|
||||
const originalCodexLensDataDir = process.env.CODEXLENS_DATA_DIR;
|
||||
const tempDirs = [];
|
||||
|
||||
afterEach(() => {
|
||||
fs.existsSync = originalExistsSync;
|
||||
syncBuiltinESMExports();
|
||||
|
||||
if (originalCodexLensDataDir === undefined) {
|
||||
delete process.env.CODEXLENS_DATA_DIR;
|
||||
} else {
|
||||
process.env.CODEXLENS_DATA_DIR = originalCodexLensDataDir;
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
rmSync(tempDirs.pop(), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('codexlens-path hidden python selection', () => {
|
||||
it('prefers pythonw.exe for hidden Windows subprocesses when available', async () => {
|
||||
if (process.platform !== 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataDir = mkdtempSync(join(tmpdir(), 'ccw-codexlens-hidden-python-'));
|
||||
tempDirs.push(dataDir);
|
||||
process.env.CODEXLENS_DATA_DIR = dataDir;
|
||||
|
||||
const expectedPythonw = join(dataDir, 'venv', 'Scripts', 'pythonw.exe');
|
||||
fs.existsSync = (path) => String(path) === expectedPythonw;
|
||||
syncBuiltinESMExports();
|
||||
|
||||
const moduleUrl = new URL(`../dist/utils/codexlens-path.js?t=${Date.now()}`, import.meta.url);
|
||||
const mod = await import(moduleUrl.href);
|
||||
|
||||
assert.equal(mod.getCodexLensHiddenPython(), expectedPythonw);
|
||||
});
|
||||
|
||||
it('falls back to python.exe when pythonw.exe is unavailable', async () => {
|
||||
const dataDir = mkdtempSync(join(tmpdir(), 'ccw-codexlens-hidden-fallback-'));
|
||||
tempDirs.push(dataDir);
|
||||
process.env.CODEXLENS_DATA_DIR = dataDir;
|
||||
|
||||
fs.existsSync = () => false;
|
||||
syncBuiltinESMExports();
|
||||
|
||||
const moduleUrl = new URL(`../dist/utils/codexlens-path.js?t=${Date.now()}`, import.meta.url);
|
||||
const mod = await import(moduleUrl.href);
|
||||
|
||||
assert.equal(mod.getCodexLensHiddenPython(), mod.getCodexLensPython());
|
||||
});
|
||||
});
|
||||
@@ -105,7 +105,10 @@ describe('memory-embedder-bridge', () => {
|
||||
assert.equal(spawnCalls.length, 1);
|
||||
assert.equal(spawnCalls[0].args.at(-2), 'status');
|
||||
assert.equal(spawnCalls[0].args.at(-1), 'C:\\tmp\\db.sqlite');
|
||||
assert.equal(spawnCalls[0].options.shell, false);
|
||||
assert.equal(spawnCalls[0].options.timeout, 30000);
|
||||
assert.equal(spawnCalls[0].options.windowsHide, true);
|
||||
assert.equal(spawnCalls[0].options.env.PYTHONIOENCODING, 'utf-8');
|
||||
});
|
||||
|
||||
it('generateEmbeddings builds args for sourceId, batchSize, and force', async () => {
|
||||
@@ -138,7 +141,10 @@ describe('memory-embedder-bridge', () => {
|
||||
assert.equal(args[batchSizeIndex + 1], '4');
|
||||
|
||||
assert.ok(args.includes('--force'));
|
||||
assert.equal(spawnCalls[0].options.shell, false);
|
||||
assert.equal(spawnCalls[0].options.timeout, 300000);
|
||||
assert.equal(spawnCalls[0].options.windowsHide, true);
|
||||
assert.equal(spawnCalls[0].options.env.PYTHONIOENCODING, 'utf-8');
|
||||
|
||||
spawnCalls.length = 0;
|
||||
spawnPlan.push({
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('LiteLLM client bridge', () => {
|
||||
|
||||
assert.equal(available, true);
|
||||
assert.equal(spawnCalls.length, 1);
|
||||
assert.equal(spawnCalls[0].command, 'python');
|
||||
assert.equal(spawnCalls[0].command, mod.getCodexLensVenvPython());
|
||||
assert.deepEqual(spawnCalls[0].args, ['-m', 'ccw_litellm.cli', 'version']);
|
||||
});
|
||||
|
||||
@@ -117,6 +117,19 @@ describe('LiteLLM client bridge', () => {
|
||||
assert.equal(spawnCalls[0].command, 'python3');
|
||||
});
|
||||
|
||||
it('spawns LiteLLM Python with hidden window options', async () => {
|
||||
spawnPlan.push({ type: 'close', code: 0, stdout: '1.2.3\n' });
|
||||
|
||||
const client = new mod.LiteLLMClient({ timeout: 10 });
|
||||
const available = await client.isAvailable();
|
||||
|
||||
assert.equal(available, true);
|
||||
assert.equal(spawnCalls.length, 1);
|
||||
assert.equal(spawnCalls[0].options.shell, false);
|
||||
assert.equal(spawnCalls[0].options.windowsHide, true);
|
||||
assert.equal(spawnCalls[0].options.env.PYTHONIOENCODING, 'utf-8');
|
||||
});
|
||||
|
||||
it('isAvailable returns false on spawn error', async () => {
|
||||
spawnPlan.push({ type: 'error', error: new Error('ENOENT') });
|
||||
|
||||
@@ -154,7 +167,7 @@ describe('LiteLLM client bridge', () => {
|
||||
|
||||
assert.deepEqual(cfg, { ok: true });
|
||||
assert.equal(spawnCalls.length, 1);
|
||||
assert.deepEqual(spawnCalls[0].args, ['-m', 'ccw_litellm.cli', 'config', '--json']);
|
||||
assert.deepEqual(spawnCalls[0].args, ['-m', 'ccw_litellm.cli', 'config']);
|
||||
});
|
||||
|
||||
it('getConfig throws on malformed JSON', async () => {
|
||||
|
||||
@@ -76,6 +76,26 @@ describe('Smart Search - Query Intent + RRF Weights', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyIntent lexical routing', () => {
|
||||
it('routes config/backend queries to exact when index and embeddings are available', () => {
|
||||
if (!smartSearchModule) return;
|
||||
const classification = smartSearchModule.__testables.classifyIntent(
|
||||
'embedding backend fastembed local litellm api config',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(classification.mode, 'exact');
|
||||
assert.match(classification.reasoning, /lexical priority/i);
|
||||
});
|
||||
|
||||
it('routes generated artifact queries to exact when index and embeddings are available', () => {
|
||||
if (!smartSearchModule) return;
|
||||
const classification = smartSearchModule.__testables.classifyIntent('dist bundle output', true, true);
|
||||
assert.strictEqual(classification.mode, 'exact');
|
||||
assert.match(classification.reasoning, /generated artifact/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adjustWeightsByIntent', () => {
|
||||
it('maps keyword intent to exact-heavy weights', () => {
|
||||
if (!smartSearchModule) return;
|
||||
@@ -119,4 +139,3 @@ describe('Smart Search - Query Intent + RRF Weights', async () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { afterEach, before, describe, it } from 'node:test';
|
||||
import { after, afterEach, before, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const smartSearchPath = new URL('../dist/tools/smart-search.js', import.meta.url).href;
|
||||
const originalAutoInitMissing = process.env.CODEXLENS_AUTO_INIT_MISSING;
|
||||
const originalAutoEmbedMissing = process.env.CODEXLENS_AUTO_EMBED_MISSING;
|
||||
|
||||
describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
let smartSearchModule;
|
||||
const tempDirs = [];
|
||||
|
||||
before(async () => {
|
||||
process.env.CODEXLENS_AUTO_INIT_MISSING = 'false';
|
||||
try {
|
||||
smartSearchModule = await import(smartSearchPath);
|
||||
} catch (err) {
|
||||
@@ -18,10 +21,30 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (originalAutoInitMissing === undefined) {
|
||||
delete process.env.CODEXLENS_AUTO_INIT_MISSING;
|
||||
} else {
|
||||
process.env.CODEXLENS_AUTO_INIT_MISSING = originalAutoInitMissing;
|
||||
}
|
||||
|
||||
if (originalAutoEmbedMissing === undefined) {
|
||||
delete process.env.CODEXLENS_AUTO_EMBED_MISSING;
|
||||
return;
|
||||
}
|
||||
process.env.CODEXLENS_AUTO_EMBED_MISSING = originalAutoEmbedMissing;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
rmSync(tempDirs.pop(), { recursive: true, force: true });
|
||||
}
|
||||
if (smartSearchModule?.__testables) {
|
||||
smartSearchModule.__testables.__resetRuntimeOverrides();
|
||||
smartSearchModule.__testables.__resetBackgroundJobs();
|
||||
}
|
||||
process.env.CODEXLENS_AUTO_INIT_MISSING = 'false';
|
||||
delete process.env.CODEXLENS_AUTO_EMBED_MISSING;
|
||||
});
|
||||
|
||||
function createWorkspace() {
|
||||
@@ -30,6 +53,15 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
return dir;
|
||||
}
|
||||
|
||||
function createDetachedChild() {
|
||||
return {
|
||||
on() {
|
||||
return this;
|
||||
},
|
||||
unref() {},
|
||||
};
|
||||
}
|
||||
|
||||
it('keeps schema defaults aligned with runtime docs', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
@@ -50,14 +82,202 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
assert.equal(props.output_mode.default, 'ace');
|
||||
});
|
||||
|
||||
it('defaults auto embedding warmup to enabled unless explicitly disabled', () => {
|
||||
it('defaults auto embedding warmup off on Windows unless explicitly enabled', () => {
|
||||
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);
|
||||
delete process.env.CODEXLENS_AUTO_EMBED_MISSING;
|
||||
assert.equal(__testables.isAutoEmbedMissingEnabled(undefined), process.platform !== 'win32');
|
||||
assert.equal(__testables.isAutoEmbedMissingEnabled({}), process.platform !== 'win32');
|
||||
assert.equal(
|
||||
__testables.isAutoEmbedMissingEnabled({ embedding_auto_embed_missing: true }),
|
||||
process.platform === 'win32' ? false : true,
|
||||
);
|
||||
assert.equal(__testables.isAutoEmbedMissingEnabled({ embedding_auto_embed_missing: false }), false);
|
||||
process.env.CODEXLENS_AUTO_EMBED_MISSING = 'true';
|
||||
assert.equal(__testables.isAutoEmbedMissingEnabled({ embedding_auto_embed_missing: false }), true);
|
||||
process.env.CODEXLENS_AUTO_EMBED_MISSING = 'off';
|
||||
assert.equal(__testables.isAutoEmbedMissingEnabled({ embedding_auto_embed_missing: true }), false);
|
||||
});
|
||||
|
||||
it('defaults auto index warmup off on Windows unless explicitly enabled', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const { __testables } = smartSearchModule;
|
||||
delete process.env.CODEXLENS_AUTO_INIT_MISSING;
|
||||
assert.equal(__testables.isAutoInitMissingEnabled(), process.platform !== 'win32');
|
||||
process.env.CODEXLENS_AUTO_INIT_MISSING = 'off';
|
||||
assert.equal(__testables.isAutoInitMissingEnabled(), false);
|
||||
process.env.CODEXLENS_AUTO_INIT_MISSING = '1';
|
||||
assert.equal(__testables.isAutoInitMissingEnabled(), true);
|
||||
});
|
||||
|
||||
it('explains when Windows disables background warmup by default', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const { __testables } = smartSearchModule;
|
||||
delete process.env.CODEXLENS_AUTO_INIT_MISSING;
|
||||
delete process.env.CODEXLENS_AUTO_EMBED_MISSING;
|
||||
|
||||
const initReason = __testables.getAutoInitMissingDisabledReason();
|
||||
const embedReason = __testables.getAutoEmbedMissingDisabledReason({});
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
assert.match(initReason, /disabled by default on Windows/i);
|
||||
assert.match(embedReason, /disabled by default on Windows/i);
|
||||
assert.match(embedReason, /auto_embed_missing=true/i);
|
||||
} else {
|
||||
assert.match(initReason, /disabled/i);
|
||||
assert.match(embedReason, /disabled/i);
|
||||
}
|
||||
});
|
||||
|
||||
it('builds hidden subprocess options for Smart Search child processes', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const options = smartSearchModule.__testables.buildSmartSearchSpawnOptions(tmpdir(), {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
timeout: 12345,
|
||||
});
|
||||
|
||||
assert.equal(options.cwd, tmpdir());
|
||||
assert.equal(options.shell, false);
|
||||
assert.equal(options.windowsHide, true);
|
||||
assert.equal(options.detached, true);
|
||||
assert.equal(options.timeout, 12345);
|
||||
assert.equal(options.env.PYTHONIOENCODING, 'utf-8');
|
||||
});
|
||||
|
||||
it('avoids detached background warmup children on Windows consoles', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
assert.equal(
|
||||
smartSearchModule.__testables.shouldDetachBackgroundSmartSearchProcess(),
|
||||
process.platform !== 'win32',
|
||||
);
|
||||
});
|
||||
|
||||
it('checks tool availability without shell-based lookup popups', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const lookupCalls = [];
|
||||
const available = smartSearchModule.__testables.checkToolAvailability(
|
||||
'rg',
|
||||
(command, args, options) => {
|
||||
lookupCalls.push({ command, args, options });
|
||||
return { status: 0, stdout: '', stderr: '' };
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(available, true);
|
||||
assert.equal(lookupCalls.length, 1);
|
||||
assert.equal(lookupCalls[0].command, process.platform === 'win32' ? 'where' : 'which');
|
||||
assert.deepEqual(lookupCalls[0].args, ['rg']);
|
||||
assert.equal(lookupCalls[0].options.shell, false);
|
||||
assert.equal(lookupCalls[0].options.windowsHide, true);
|
||||
assert.equal(lookupCalls[0].options.stdio, 'ignore');
|
||||
assert.equal(lookupCalls[0].options.env.PYTHONIOENCODING, 'utf-8');
|
||||
});
|
||||
|
||||
it('starts background static index build once for unindexed paths', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const { __testables } = smartSearchModule;
|
||||
const dir = createWorkspace();
|
||||
const fakePython = join(dir, 'python.exe');
|
||||
writeFileSync(fakePython, '');
|
||||
process.env.CODEXLENS_AUTO_INIT_MISSING = 'true';
|
||||
|
||||
const spawnCalls = [];
|
||||
__testables.__setRuntimeOverrides({
|
||||
getVenvPythonPath: () => fakePython,
|
||||
now: () => 1234567890,
|
||||
spawnProcess: (command, args, options) => {
|
||||
spawnCalls.push({ command, args, options });
|
||||
return createDetachedChild();
|
||||
},
|
||||
});
|
||||
|
||||
const scope = { workingDirectory: dir, searchPaths: ['.'] };
|
||||
const indexStatus = { indexed: false, has_embeddings: false };
|
||||
|
||||
const first = await __testables.maybeStartBackgroundAutoInit(scope, indexStatus);
|
||||
const second = await __testables.maybeStartBackgroundAutoInit(scope, indexStatus);
|
||||
|
||||
assert.match(first.note, /started/i);
|
||||
assert.match(second.note, /already running/i);
|
||||
assert.equal(spawnCalls.length, 1);
|
||||
assert.equal(spawnCalls[0].command, fakePython);
|
||||
assert.deepEqual(spawnCalls[0].args, ['-m', 'codexlens', 'index', 'init', dir, '--no-embeddings']);
|
||||
assert.equal(spawnCalls[0].options.cwd, dir);
|
||||
assert.equal(
|
||||
spawnCalls[0].options.detached,
|
||||
smartSearchModule.__testables.shouldDetachBackgroundSmartSearchProcess(),
|
||||
);
|
||||
assert.equal(spawnCalls[0].options.windowsHide, true);
|
||||
});
|
||||
|
||||
it('starts background embedding build without detached Windows consoles', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const { __testables } = smartSearchModule;
|
||||
const dir = createWorkspace();
|
||||
const fakePython = join(dir, 'python.exe');
|
||||
writeFileSync(fakePython, '');
|
||||
process.env.CODEXLENS_AUTO_EMBED_MISSING = 'true';
|
||||
|
||||
const spawnCalls = [];
|
||||
__testables.__setRuntimeOverrides({
|
||||
getVenvPythonPath: () => fakePython,
|
||||
checkSemanticStatus: async () => ({ available: true, litellmAvailable: true }),
|
||||
now: () => 1234567890,
|
||||
spawnProcess: (command, args, options) => {
|
||||
spawnCalls.push({ command, args, options });
|
||||
return createDetachedChild();
|
||||
},
|
||||
});
|
||||
|
||||
const status = await __testables.maybeStartBackgroundAutoEmbed(
|
||||
{ workingDirectory: dir, searchPaths: ['.'] },
|
||||
{
|
||||
indexed: true,
|
||||
has_embeddings: false,
|
||||
config: { embedding_backend: 'fastembed' },
|
||||
},
|
||||
);
|
||||
|
||||
assert.match(status.note, /started/i);
|
||||
assert.equal(spawnCalls.length, 1);
|
||||
assert.equal(spawnCalls[0].command, fakePython);
|
||||
assert.deepEqual(spawnCalls[0].args.slice(0, 1), ['-c']);
|
||||
assert.equal(spawnCalls[0].options.cwd, dir);
|
||||
assert.equal(
|
||||
spawnCalls[0].options.detached,
|
||||
smartSearchModule.__testables.shouldDetachBackgroundSmartSearchProcess(),
|
||||
);
|
||||
assert.equal(spawnCalls[0].options.windowsHide, true);
|
||||
assert.equal(spawnCalls[0].options.stdio, 'ignore');
|
||||
});
|
||||
|
||||
it('surfaces warnings when background static index warmup cannot start', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const { __testables } = smartSearchModule;
|
||||
const dir = createWorkspace();
|
||||
process.env.CODEXLENS_AUTO_INIT_MISSING = 'true';
|
||||
|
||||
__testables.__setRuntimeOverrides({
|
||||
getVenvPythonPath: () => join(dir, 'missing-python.exe'),
|
||||
});
|
||||
|
||||
const status = await __testables.maybeStartBackgroundAutoInit(
|
||||
{ workingDirectory: dir, searchPaths: ['.'] },
|
||||
{ indexed: false, has_embeddings: false },
|
||||
);
|
||||
|
||||
assert.match(status.warning, /Automatic static index warmup could not start/i);
|
||||
assert.match(status.warning, /not ready yet/i);
|
||||
});
|
||||
|
||||
it('honors explicit small limit values', async () => {
|
||||
@@ -246,15 +466,98 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
assert.match(String(matches[0].file).replace(/\\/g, '/'), /target\.ts$/);
|
||||
});
|
||||
|
||||
it('detects centralized vector artifacts as full embedding coverage evidence', () => {
|
||||
it('uses root-scoped embedding status instead of subtree artifacts', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const dir = createWorkspace();
|
||||
writeFileSync(join(dir, '_vectors.hnsw'), 'hnsw');
|
||||
writeFileSync(join(dir, '_vectors_meta.db'), 'meta');
|
||||
writeFileSync(join(dir, '_binary_vectors.mmap'), 'mmap');
|
||||
const summary = smartSearchModule.__testables.extractEmbeddingsStatusSummary({
|
||||
total_indexes: 3,
|
||||
indexes_with_embeddings: 2,
|
||||
total_chunks: 24,
|
||||
coverage_percent: 66.7,
|
||||
root: {
|
||||
total_files: 4,
|
||||
files_with_embeddings: 0,
|
||||
total_chunks: 0,
|
||||
coverage_percent: 0,
|
||||
has_embeddings: false,
|
||||
},
|
||||
subtree: {
|
||||
total_indexes: 3,
|
||||
indexes_with_embeddings: 2,
|
||||
total_files: 12,
|
||||
files_with_embeddings: 8,
|
||||
total_chunks: 24,
|
||||
coverage_percent: 66.7,
|
||||
},
|
||||
centralized: {
|
||||
dense_index_exists: true,
|
||||
binary_index_exists: true,
|
||||
meta_db_exists: true,
|
||||
usable: false,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(smartSearchModule.__testables.hasCentralizedVectorArtifacts(dir), true);
|
||||
assert.equal(summary.coveragePercent, 0);
|
||||
assert.equal(summary.totalChunks, 0);
|
||||
assert.equal(summary.hasEmbeddings, false);
|
||||
});
|
||||
|
||||
it('accepts validated root centralized readiness from CLI status payloads', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const summary = smartSearchModule.__testables.extractEmbeddingsStatusSummary({
|
||||
total_indexes: 2,
|
||||
indexes_with_embeddings: 1,
|
||||
total_chunks: 10,
|
||||
coverage_percent: 25,
|
||||
root: {
|
||||
total_files: 2,
|
||||
files_with_embeddings: 1,
|
||||
total_chunks: 3,
|
||||
coverage_percent: 50,
|
||||
has_embeddings: true,
|
||||
},
|
||||
centralized: {
|
||||
usable: true,
|
||||
dense_ready: true,
|
||||
chunk_metadata_rows: 3,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(summary.coveragePercent, 50);
|
||||
assert.equal(summary.totalChunks, 3);
|
||||
assert.equal(summary.hasEmbeddings, true);
|
||||
});
|
||||
|
||||
it('prefers embeddings_status over legacy embeddings summary payloads', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const payload = smartSearchModule.__testables.selectEmbeddingsStatusPayload({
|
||||
embeddings: {
|
||||
total_indexes: 7,
|
||||
indexes_with_embeddings: 4,
|
||||
total_chunks: 99,
|
||||
},
|
||||
embeddings_status: {
|
||||
total_indexes: 7,
|
||||
total_chunks: 3,
|
||||
root: {
|
||||
total_files: 2,
|
||||
files_with_embeddings: 1,
|
||||
total_chunks: 3,
|
||||
coverage_percent: 50,
|
||||
has_embeddings: true,
|
||||
},
|
||||
centralized: {
|
||||
usable: true,
|
||||
dense_ready: true,
|
||||
chunk_metadata_rows: 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(payload.root.total_chunks, 3);
|
||||
assert.equal(payload.centralized.usable, true);
|
||||
});
|
||||
|
||||
it('recognizes CodexLens CLI compatibility failures and invalid regex fallback', () => {
|
||||
@@ -281,6 +584,37 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
assert.match(resolution.warning, /literal ripgrep matching/i);
|
||||
});
|
||||
|
||||
it('suppresses compatibility-only fuzzy warnings when ripgrep already produced hits', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
assert.equal(
|
||||
smartSearchModule.__testables.shouldSurfaceCodexLensFtsCompatibilityWarning({
|
||||
compatibilityTriggeredThisQuery: true,
|
||||
skipExactDueToCompatibility: false,
|
||||
ripgrepResultCount: 2,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
smartSearchModule.__testables.shouldSurfaceCodexLensFtsCompatibilityWarning({
|
||||
compatibilityTriggeredThisQuery: true,
|
||||
skipExactDueToCompatibility: false,
|
||||
ripgrepResultCount: 0,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
smartSearchModule.__testables.shouldSurfaceCodexLensFtsCompatibilityWarning({
|
||||
compatibilityTriggeredThisQuery: false,
|
||||
skipExactDueToCompatibility: true,
|
||||
ripgrepResultCount: 0,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('builds actionable index suggestions for unhealthy index states', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
@@ -318,4 +652,52 @@ describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
assert.match(toolResult.error, /Both search backends failed:/);
|
||||
assert.match(toolResult.error, /(FTS|Ripgrep)/);
|
||||
});
|
||||
|
||||
it('returns structured semantic results after local init and embed without JSON parse warnings', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const codexLensModule = await import(new URL(`../dist/tools/codex-lens.js?smart-semantic=${Date.now()}`, import.meta.url).href);
|
||||
const ready = await codexLensModule.checkVenvStatus(true);
|
||||
if (!ready.ready) {
|
||||
console.log('Skipping: CodexLens not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const semantic = await codexLensModule.checkSemanticStatus();
|
||||
if (!semantic.available) {
|
||||
console.log('Skipping: semantic dependencies not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = createWorkspace();
|
||||
writeFileSync(
|
||||
join(dir, 'sample.ts'),
|
||||
'export function parseCodexLensOutput() { return stripAnsiOutput(); }\nexport const sum = (a, b) => a + b;\n',
|
||||
);
|
||||
|
||||
const init = await smartSearchModule.handler({ action: 'init', path: dir });
|
||||
assert.equal(init.success, true, init.error ?? 'Expected init to succeed');
|
||||
|
||||
const embed = await smartSearchModule.handler({
|
||||
action: 'embed',
|
||||
path: dir,
|
||||
embeddingBackend: 'local',
|
||||
force: true,
|
||||
});
|
||||
assert.equal(embed.success, true, embed.error ?? 'Expected local embed to succeed');
|
||||
|
||||
const search = await smartSearchModule.handler({
|
||||
action: 'search',
|
||||
mode: 'semantic',
|
||||
path: dir,
|
||||
query: 'parse CodexLens output strip ANSI',
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
assert.equal(search.success, true, search.error ?? 'Expected semantic search to succeed');
|
||||
assert.equal(search.result.success, true);
|
||||
assert.equal(search.result.results.format, 'ace');
|
||||
assert.ok(search.result.results.total >= 1, 'Expected at least one structured semantic match');
|
||||
assert.doesNotMatch(search.result.metadata?.warning ?? '', /Failed to parse JSON output/i);
|
||||
});
|
||||
});
|
||||
|
||||
97
ccw/tests/unified-vector-index.test.ts
Normal file
97
ccw/tests/unified-vector-index.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { after, beforeEach, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { createRequire } from 'node:module';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('node:fs') as typeof import('node:fs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const childProcess = require('node:child_process') as typeof import('node:child_process');
|
||||
|
||||
class FakeChildProcess extends EventEmitter {
|
||||
stdout = new EventEmitter();
|
||||
stderr = new EventEmitter();
|
||||
stdinChunks: string[] = [];
|
||||
stdin = {
|
||||
write: (chunk: string | Buffer) => {
|
||||
this.stdinChunks.push(String(chunk));
|
||||
return true;
|
||||
},
|
||||
end: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
type SpawnCall = {
|
||||
command: string;
|
||||
args: string[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: any;
|
||||
child: FakeChildProcess;
|
||||
};
|
||||
|
||||
const spawnCalls: SpawnCall[] = [];
|
||||
const tempDirs: string[] = [];
|
||||
let embedderAvailable = true;
|
||||
|
||||
const originalExistsSync = fs.existsSync;
|
||||
const originalSpawn = childProcess.spawn;
|
||||
|
||||
fs.existsSync = ((..._args: unknown[]) => embedderAvailable) as typeof fs.existsSync;
|
||||
|
||||
childProcess.spawn = ((command: string, args: string[] = [], options: unknown = {}) => {
|
||||
const child = new FakeChildProcess();
|
||||
spawnCalls.push({ command: String(command), args: args.map(String), options, child });
|
||||
|
||||
queueMicrotask(() => {
|
||||
child.stdout.emit('data', JSON.stringify({
|
||||
success: true,
|
||||
total_chunks: 4,
|
||||
hnsw_available: true,
|
||||
hnsw_count: 4,
|
||||
dimension: 384,
|
||||
}));
|
||||
child.emit('close', 0);
|
||||
});
|
||||
|
||||
return child as unknown as ReturnType<typeof childProcess.spawn>;
|
||||
}) as typeof childProcess.spawn;
|
||||
|
||||
after(() => {
|
||||
fs.existsSync = originalExistsSync;
|
||||
childProcess.spawn = originalSpawn;
|
||||
while (tempDirs.length > 0) {
|
||||
rmSync(tempDirs.pop() as string, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('unified-vector-index', () => {
|
||||
beforeEach(() => {
|
||||
embedderAvailable = true;
|
||||
spawnCalls.length = 0;
|
||||
});
|
||||
|
||||
it('spawns CodexLens venv python with hidden window options', async () => {
|
||||
const projectDir = mkdtempSync(join(tmpdir(), 'ccw-unified-vector-index-'));
|
||||
tempDirs.push(projectDir);
|
||||
|
||||
const moduleUrl = new URL('../dist/core/unified-vector-index.js', import.meta.url);
|
||||
moduleUrl.searchParams.set('t', String(Date.now()));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mod: any = await import(moduleUrl.href);
|
||||
|
||||
const index = new mod.UnifiedVectorIndex(projectDir);
|
||||
const status = await index.getStatus();
|
||||
|
||||
assert.equal(status.success, true);
|
||||
assert.equal(spawnCalls.length, 1);
|
||||
assert.equal(spawnCalls[0].options.shell, false);
|
||||
assert.equal(spawnCalls[0].options.windowsHide, true);
|
||||
assert.equal(spawnCalls[0].options.env.PYTHONIOENCODING, 'utf-8');
|
||||
assert.deepEqual(spawnCalls[0].options.stdio, ['pipe', 'pipe', 'pipe']);
|
||||
assert.match(spawnCalls[0].child.stdinChunks.join(''), /"operation":"status"/);
|
||||
});
|
||||
});
|
||||
@@ -3,13 +3,16 @@ import assert from 'node:assert/strict';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const uvManagerPath = new URL('../dist/utils/uv-manager.js', import.meta.url).href;
|
||||
const pythonUtilsPath = new URL('../dist/utils/python-utils.js', import.meta.url).href;
|
||||
|
||||
describe('CodexLens UV python preference', async () => {
|
||||
let mod;
|
||||
let pythonUtils;
|
||||
const originalPython = process.env.CCW_PYTHON;
|
||||
|
||||
before(async () => {
|
||||
mod = await import(uvManagerPath);
|
||||
pythonUtils = await import(pythonUtilsPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -25,6 +28,73 @@ describe('CodexLens UV python preference', async () => {
|
||||
assert.equal(mod.getPreferredCodexLensPythonSpec(), 'C:/Custom/Python/python.exe');
|
||||
});
|
||||
|
||||
it('parses py launcher commands into spawn-safe command specs', () => {
|
||||
const spec = pythonUtils.parsePythonCommandSpec('py -3.11');
|
||||
|
||||
assert.equal(spec.command, 'py');
|
||||
assert.deepEqual(spec.args, ['-3.11']);
|
||||
assert.equal(spec.display, 'py -3.11');
|
||||
});
|
||||
|
||||
it('treats unquoted Windows-style executable paths as a single command', () => {
|
||||
const spec = pythonUtils.parsePythonCommandSpec('C:/Program Files/Python311/python.exe');
|
||||
|
||||
assert.equal(spec.command, 'C:/Program Files/Python311/python.exe');
|
||||
assert.deepEqual(spec.args, []);
|
||||
assert.equal(spec.display, '"C:/Program Files/Python311/python.exe"');
|
||||
});
|
||||
|
||||
it('probes Python launcher versions without opening a shell window', () => {
|
||||
const probeCalls = [];
|
||||
const version = pythonUtils.probePythonCommandVersion(
|
||||
{ command: 'py', args: ['-3.11'], display: 'py -3.11' },
|
||||
(command, args, options) => {
|
||||
probeCalls.push({ command, args, options });
|
||||
return { status: 0, stdout: '', stderr: 'Python 3.11.9\n' };
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(version, 'Python 3.11.9');
|
||||
assert.equal(probeCalls.length, 1);
|
||||
assert.equal(probeCalls[0].command, 'py');
|
||||
assert.deepEqual(probeCalls[0].args, ['-3.11', '--version']);
|
||||
assert.equal(probeCalls[0].options.shell, false);
|
||||
assert.equal(probeCalls[0].options.windowsHide, true);
|
||||
assert.equal(probeCalls[0].options.env.PYTHONIOENCODING, 'utf-8');
|
||||
});
|
||||
|
||||
it('looks up uv on PATH without spawning a visible shell window', () => {
|
||||
const lookupCalls = [];
|
||||
const found = mod.__testables.findExecutableOnPath('uv', (command, args, options) => {
|
||||
lookupCalls.push({ command, args, options });
|
||||
return { status: 0, stdout: 'C:/Tools/uv.exe\n', stderr: '' };
|
||||
});
|
||||
|
||||
assert.equal(found, 'C:/Tools/uv.exe');
|
||||
assert.equal(lookupCalls.length, 1);
|
||||
assert.equal(lookupCalls[0].command, process.platform === 'win32' ? 'where' : 'which');
|
||||
assert.deepEqual(lookupCalls[0].args, ['uv']);
|
||||
assert.equal(lookupCalls[0].options.shell, false);
|
||||
assert.equal(lookupCalls[0].options.windowsHide, true);
|
||||
assert.equal(lookupCalls[0].options.env.PYTHONIOENCODING, 'utf-8');
|
||||
});
|
||||
|
||||
it('checks Windows launcher preferences with hidden subprocess options', () => {
|
||||
const probeCalls = [];
|
||||
const available = mod.__testables.hasWindowsPythonLauncherVersion('3.11', (command, args, options) => {
|
||||
probeCalls.push({ command, args, options });
|
||||
return { status: 0, stdout: '', stderr: 'Python 3.11.9\n' };
|
||||
});
|
||||
|
||||
assert.equal(available, true);
|
||||
assert.equal(probeCalls.length, 1);
|
||||
assert.equal(probeCalls[0].command, 'py');
|
||||
assert.deepEqual(probeCalls[0].args, ['-3.11', '--version']);
|
||||
assert.equal(probeCalls[0].options.shell, false);
|
||||
assert.equal(probeCalls[0].options.windowsHide, true);
|
||||
assert.equal(probeCalls[0].options.env.PYTHONIOENCODING, 'utf-8');
|
||||
});
|
||||
|
||||
it('prefers Python 3.11 or 3.10 on Windows when available', () => {
|
||||
if (process.platform !== 'win32') return;
|
||||
delete process.env.CCW_PYTHON;
|
||||
|
||||
Reference in New Issue
Block a user