mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
Add comprehensive tests for semantic chunking and search functionality
- Implemented tests for the ChunkConfig and Chunker classes, covering default and custom configurations. - Added tests for symbol-based chunking, including single and multiple symbols, handling of empty symbols, and preservation of line numbers. - Developed tests for sliding window chunking, ensuring correct chunking behavior with various content sizes and configurations. - Created integration tests for semantic search, validating embedding generation, vector storage, and search accuracy across a complex codebase. - Included performance tests for embedding generation and search operations. - Established tests for chunking strategies, comparing symbol-based and sliding window approaches. - Enhanced test coverage for edge cases, including handling of unicode characters and out-of-bounds symbol ranges.
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
"ccw": "./bin/ccw.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test",
|
||||
"test": "node --test tests/*.test.js",
|
||||
"test:codexlens": "node --test tests/codex-lens*.test.js",
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -10,6 +10,7 @@ import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normali
|
||||
import { getCliToolsStatus, getExecutionHistory, getExecutionDetail, deleteExecution, executeCliTool } from '../tools/cli-executor.js';
|
||||
import { getAllManifests } from './manifest.js';
|
||||
import { checkVenvStatus, bootstrapVenv, executeCodexLens, checkSemanticStatus, installSemantic } from '../tools/codex-lens.js';
|
||||
import { listTools } from '../tools/index.js';
|
||||
|
||||
// Claude config file paths
|
||||
const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json');
|
||||
@@ -521,14 +522,18 @@ export async function startServer(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CodexLens Semantic Search Install
|
||||
// API: CodexLens Semantic Search Install (fastembed, ONNX-based, ~200MB)
|
||||
if (pathname === '/api/codexlens/semantic/install' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
try {
|
||||
const result = await installSemantic();
|
||||
if (result.success) {
|
||||
const status = await checkSemanticStatus();
|
||||
return { success: true, message: 'Semantic search installed successfully', ...status };
|
||||
return {
|
||||
success: true,
|
||||
message: 'Semantic search installed successfully (fastembed)',
|
||||
...status
|
||||
};
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
@@ -547,6 +552,14 @@ export async function startServer(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CCW Endpoint Tools List
|
||||
if (pathname === '/api/ccw/tools') {
|
||||
const tools = listTools();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ tools }));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CCW Upgrade
|
||||
if (pathname === '/api/ccw/upgrade' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
@@ -1406,34 +1419,12 @@ function generateServerDashboard(initialPath) {
|
||||
jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/'));
|
||||
jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(getRecentPaths()));
|
||||
|
||||
// Add server mode flag and dynamic loading functions at the start of JS
|
||||
// Add server mode flag at the start of JS
|
||||
// Note: loadDashboardData and loadRecentPaths are defined in api.js module
|
||||
const serverModeScript = `
|
||||
// Server mode - load data dynamically
|
||||
window.SERVER_MODE = true;
|
||||
window.INITIAL_PATH = '${normalizePathForDisplay(initialPath).replace(/\\/g, '/')}';
|
||||
|
||||
async function loadDashboardData(path) {
|
||||
try {
|
||||
const res = await fetch('/api/data?path=' + encodeURIComponent(path));
|
||||
if (!res.ok) throw new Error('Failed to load data');
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecentPaths() {
|
||||
try {
|
||||
const res = await fetch('/api/recent-paths');
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return data.paths || [];
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
// Prepend server mode script to JS content
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tool-item.endpoint {
|
||||
border-left: 3px solid hsl(var(--indigo));
|
||||
}
|
||||
|
||||
.tool-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -29,6 +29,26 @@ async function loadDashboardData(path) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load recent paths from API (server mode only)
|
||||
* @returns {Promise<Array>} Array of recent paths or empty array if failed
|
||||
*/
|
||||
async function loadRecentPaths() {
|
||||
if (!window.SERVER_MODE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/recent-paths');
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
return data.paths || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to load recent paths:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Path Management ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -587,70 +587,75 @@ function renderSkillContextConfig() {
|
||||
const availableSkills = window.availableSkills || [];
|
||||
|
||||
if (selectedOption === 'auto') {
|
||||
return `
|
||||
<div class="bg-muted/30 rounded-lg p-4 text-sm text-muted-foreground">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i data-lucide="info" class="w-4 h-4"></i>
|
||||
<span class="font-medium">Auto Detection Mode</span>
|
||||
</div>
|
||||
<p>SKILLs will be automatically loaded when their name appears in your prompt.</p>
|
||||
<p class="mt-2">Available SKILLs: ${availableSkills.map(s => \`<span class="px-1.5 py-0.5 bg-emerald-500/10 text-emerald-500 rounded text-xs">${escapeHtml(s.name)}</span>\`).join(' ')}</p>
|
||||
</div>
|
||||
`;
|
||||
const skillBadges = availableSkills.map(function(s) {
|
||||
return '<span class="px-1.5 py-0.5 bg-emerald-500/10 text-emerald-500 rounded text-xs">' + escapeHtml(s.name) + '</span>';
|
||||
}).join(' ');
|
||||
return '<div class="bg-muted/30 rounded-lg p-4 text-sm text-muted-foreground">' +
|
||||
'<div class="flex items-center gap-2 mb-2">' +
|
||||
'<i data-lucide="info" class="w-4 h-4"></i>' +
|
||||
'<span class="font-medium">Auto Detection Mode</span>' +
|
||||
'</div>' +
|
||||
'<p>SKILLs will be automatically loaded when their name appears in your prompt.</p>' +
|
||||
'<p class="mt-2">Available SKILLs: ' + skillBadges + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-foreground">Configure SKILLs</span>
|
||||
<button type="button" onclick="addSkillConfig()"
|
||||
class="px-3 py-1.5 text-xs bg-primary text-primary-foreground rounded-lg hover:opacity-90 flex items-center gap-1">
|
||||
<i data-lucide="plus" class="w-3 h-3"></i> Add SKILL
|
||||
</button>
|
||||
</div>
|
||||
var configListHtml = '';
|
||||
if (skillConfigs.length === 0) {
|
||||
configListHtml = '<div class="text-center py-6 text-muted-foreground text-sm border border-dashed border-border rounded-lg">' +
|
||||
'<i data-lucide="package" class="w-8 h-8 mx-auto mb-2 opacity-50"></i>' +
|
||||
'<p>No SKILLs configured yet</p>' +
|
||||
'<p class="text-xs mt-1">Click "Add SKILL" to configure keyword triggers</p>' +
|
||||
'</div>';
|
||||
} else {
|
||||
configListHtml = skillConfigs.map(function(config, idx) {
|
||||
var skillOptions = availableSkills.map(function(s) {
|
||||
var selected = config.skill === s.id ? 'selected' : '';
|
||||
return '<option value="' + s.id + '" ' + selected + '>' + escapeHtml(s.name) + '</option>';
|
||||
}).join('');
|
||||
return '<div class="border border-border rounded-lg p-3 bg-card">' +
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<select onchange="updateSkillConfig(' + idx + ', \'skill\', this.value)" ' +
|
||||
'class="px-2 py-1 text-sm bg-background border border-border rounded text-foreground">' +
|
||||
'<option value="">Select SKILL...</option>' +
|
||||
skillOptions +
|
||||
'</select>' +
|
||||
'<button onclick="removeSkillConfig(' + idx + ')" ' +
|
||||
'class="p-1 text-muted-foreground hover:text-destructive rounded">' +
|
||||
'<i data-lucide="trash-2" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="space-y-1">' +
|
||||
'<label class="text-xs text-muted-foreground">Trigger Keywords (comma-separated)</label>' +
|
||||
'<input type="text" ' +
|
||||
'value="' + (config.keywords || '') + '" ' +
|
||||
'onchange="updateSkillConfig(' + idx + ', \'keywords\', this.value)" ' +
|
||||
'placeholder="e.g., react, hooks, component" ' +
|
||||
'class="w-full px-2 py-1.5 text-sm bg-background border border-border rounded text-foreground">' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
<div id="skillConfigsList" class="space-y-3">
|
||||
${skillConfigs.length === 0 ? \`
|
||||
<div class="text-center py-6 text-muted-foreground text-sm border border-dashed border-border rounded-lg">
|
||||
<i data-lucide="package" class="w-8 h-8 mx-auto mb-2 opacity-50"></i>
|
||||
<p>No SKILLs configured yet</p>
|
||||
<p class="text-xs mt-1">Click "Add SKILL" to configure keyword triggers</p>
|
||||
</div>
|
||||
\` : skillConfigs.map((config, idx) => \`
|
||||
<div class="border border-border rounded-lg p-3 bg-card">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<select onchange="updateSkillConfig(${idx}, 'skill', this.value)"
|
||||
class="px-2 py-1 text-sm bg-background border border-border rounded text-foreground">
|
||||
<option value="">Select SKILL...</option>
|
||||
${availableSkills.map(s => \`
|
||||
<option value="${s.id}" ${config.skill === s.id ? 'selected' : ''}>${escapeHtml(s.name)}</option>
|
||||
\`).join('')}
|
||||
</select>
|
||||
<button onclick="removeSkillConfig(${idx})"
|
||||
class="p-1 text-muted-foreground hover:text-destructive rounded">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-muted-foreground">Trigger Keywords (comma-separated)</label>
|
||||
<input type="text"
|
||||
value="${config.keywords || ''}"
|
||||
onchange="updateSkillConfig(${idx}, 'keywords', this.value)"
|
||||
placeholder="e.g., react, hooks, component"
|
||||
class="w-full px-2 py-1.5 text-sm bg-background border border-border rounded text-foreground">
|
||||
</div>
|
||||
</div>
|
||||
\`).join('')}
|
||||
</div>
|
||||
var noSkillsWarning = '';
|
||||
if (availableSkills.length === 0) {
|
||||
noSkillsWarning = '<div class="text-xs text-amber-500 flex items-center gap-1">' +
|
||||
'<i data-lucide="alert-triangle" class="w-3 h-3"></i>' +
|
||||
'No SKILLs found. Create SKILL packages in .claude/skills/' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
${availableSkills.length === 0 ? \`
|
||||
<div class="text-xs text-amber-500 flex items-center gap-1">
|
||||
<i data-lucide="alert-triangle" class="w-3 h-3"></i>
|
||||
No SKILLs found. Create SKILL packages in .claude/skills/
|
||||
</div>
|
||||
\` : ''}
|
||||
</div>
|
||||
`;
|
||||
return '<div class="space-y-4">' +
|
||||
'<div class="flex items-center justify-between">' +
|
||||
'<span class="text-sm font-medium text-foreground">Configure SKILLs</span>' +
|
||||
'<button type="button" onclick="addSkillConfig()" ' +
|
||||
'class="px-3 py-1.5 text-xs bg-primary text-primary-foreground rounded-lg hover:opacity-90 flex items-center gap-1">' +
|
||||
'<i data-lucide="plus" class="w-3 h-3"></i> Add SKILL' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div id="skillConfigsList" class="space-y-3">' + configListHtml + '</div>' +
|
||||
noSkillsWarning +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function addSkillConfig() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
var currentCliExecution = null;
|
||||
var cliExecutionOutput = '';
|
||||
var ccwInstallations = [];
|
||||
var ccwEndpointTools = [];
|
||||
|
||||
// ========== CCW Installations ==========
|
||||
async function loadCcwInstallations() {
|
||||
@@ -21,6 +22,21 @@ async function loadCcwInstallations() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CCW Endpoint Tools ==========
|
||||
async function loadCcwEndpointTools() {
|
||||
try {
|
||||
var response = await fetch('/api/ccw/tools');
|
||||
if (!response.ok) throw new Error('Failed to load CCW endpoint tools');
|
||||
var data = await response.json();
|
||||
ccwEndpointTools = data.tools || [];
|
||||
return ccwEndpointTools;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CCW endpoint tools:', err);
|
||||
ccwEndpointTools = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Rendering ==========
|
||||
async function renderCliManager() {
|
||||
var container = document.getElementById('mainContent');
|
||||
@@ -32,10 +48,12 @@ async function renderCliManager() {
|
||||
if (statsGrid) statsGrid.style.display = 'none';
|
||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||
|
||||
// Load data
|
||||
// Load data (including CodexLens status for tools section)
|
||||
await Promise.all([
|
||||
loadCliToolStatus(),
|
||||
loadCcwInstallations()
|
||||
loadCodexLensStatus(),
|
||||
loadCcwInstallations(),
|
||||
loadCcwEndpointTools()
|
||||
]);
|
||||
|
||||
container.innerHTML = '<div class="status-manager">' +
|
||||
@@ -43,11 +61,13 @@ async function renderCliManager() {
|
||||
'<div class="status-section" id="tools-section"></div>' +
|
||||
'<div class="status-section" id="ccw-section"></div>' +
|
||||
'</div>' +
|
||||
'<div class="status-section" id="ccw-endpoint-tools-section" style="margin-top: 1.5rem;"></div>' +
|
||||
'</div>';
|
||||
|
||||
// Render sub-panels
|
||||
renderToolsSection();
|
||||
renderCcwSection();
|
||||
renderCcwEndpointToolsSection();
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
@@ -221,6 +241,64 @@ function renderCcwSection() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== CCW Endpoint Tools Section (Full Width) ==========
|
||||
function renderCcwEndpointToolsSection() {
|
||||
var container = document.getElementById('ccw-endpoint-tools-section');
|
||||
if (!container) return;
|
||||
|
||||
var count = (ccwEndpointTools || []).length;
|
||||
var toolsHtml = '';
|
||||
|
||||
if (!ccwEndpointTools || ccwEndpointTools.length === 0) {
|
||||
toolsHtml = '<div class="ccw-empty-state">' +
|
||||
'<i data-lucide="wrench" class="w-8 h-8"></i>' +
|
||||
'<p>No endpoint tools found</p>' +
|
||||
'<button class="btn btn-sm btn-primary" onclick="loadCcwEndpointTools().then(function() { renderCcwEndpointToolsSection(); if (window.lucide) lucide.createIcons(); })">' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3"></i> Refresh</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
toolsHtml = '<div class="tools-list">' +
|
||||
ccwEndpointTools.map(function(t) {
|
||||
var name = t && t.name ? String(t.name) : 'unknown';
|
||||
var desc = t && t.description ? String(t.description) : '';
|
||||
var requiredCount = (t && t.parameters && Array.isArray(t.parameters.required)) ? t.parameters.required.length : 0;
|
||||
var propsCount = (t && t.parameters && t.parameters.properties) ? Object.keys(t.parameters.properties).length : 0;
|
||||
|
||||
return '<div class="tool-item endpoint">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot status-available" style="background: hsl(var(--indigo)); box-shadow: 0 0 6px hsl(var(--indigo) / 0.45);"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">' + escapeHtml(name) +
|
||||
'<span class="tool-type-badge">endpoint</span>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-desc">' + escapeHtml(desc || '—') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
'<span class="tool-status-text muted">' +
|
||||
'<i data-lucide="braces" class="w-3.5 h-3.5"></i> ' +
|
||||
propsCount + ' params' + (requiredCount ? (' · ' + requiredCount + ' required') : '') +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="server" class="w-4 h-4"></i> CCW Endpoint Tools</h3>' +
|
||||
'<span class="section-count">' + count + ' tool' + (count !== 1 ? 's' : '') + '</span>' +
|
||||
'</div>' +
|
||||
'<button class="btn-icon" onclick="loadCcwEndpointTools().then(function() { renderCcwEndpointToolsSection(); if (window.lucide) lucide.createIcons(); })" title="Refresh">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
toolsHtml;
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// CCW Install Carousel State
|
||||
var ccwCarouselIndex = 0;
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ except Exception as e:
|
||||
}
|
||||
|
||||
/**
|
||||
* Install semantic search dependencies
|
||||
* Install semantic search dependencies (fastembed, ONNX-based, ~200MB)
|
||||
* @returns {Promise<{success: boolean, error?: string}>}
|
||||
*/
|
||||
async function installSemantic() {
|
||||
@@ -163,12 +163,12 @@ async function installSemantic() {
|
||||
: join(CODEXLENS_VENV, 'bin', 'pip');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
console.log('[CodexLens] Installing semantic search dependencies...');
|
||||
console.log('[CodexLens] Installing semantic search dependencies (fastembed)...');
|
||||
console.log('[CodexLens] Using ONNX-based fastembed backend (~200MB)');
|
||||
|
||||
// Install sentence-transformers and numpy
|
||||
const child = spawn(pipPath, ['install', 'numpy>=1.24', 'sentence-transformers>=2.2'], {
|
||||
const child = spawn(pipPath, ['install', 'numpy>=1.24', 'fastembed>=0.2'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 300000 // 5 minutes for model download
|
||||
timeout: 600000 // 10 minutes for potential model download
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
@@ -178,7 +178,7 @@ async function installSemantic() {
|
||||
stdout += data.toString();
|
||||
// Log progress
|
||||
const line = data.toString().trim();
|
||||
if (line.includes('Downloading') || line.includes('Installing')) {
|
||||
if (line.includes('Downloading') || line.includes('Installing') || line.includes('Collecting')) {
|
||||
console.log(`[CodexLens] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
485
ccw/tests/codex-lens-integration.test.js
Normal file
485
ccw/tests/codex-lens-integration.test.js
Normal file
@@ -0,0 +1,485 @@
|
||||
/**
|
||||
* Integration Tests for CodexLens with actual file operations
|
||||
*
|
||||
* These tests create temporary files and directories to test
|
||||
* the full indexing and search workflow.
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync, readdirSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Import the codex-lens module
|
||||
const codexLensPath = new URL('../src/tools/codex-lens.js', import.meta.url).href;
|
||||
|
||||
describe('CodexLens Full Integration Tests', async () => {
|
||||
let codexLensModule;
|
||||
let testDir;
|
||||
let isReady = false;
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
codexLensModule = await import(codexLensPath);
|
||||
|
||||
// Check if CodexLens is installed
|
||||
const status = await codexLensModule.checkVenvStatus();
|
||||
isReady = status.ready;
|
||||
|
||||
if (!isReady) {
|
||||
console.log('CodexLens not installed - some integration tests will be skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temporary test directory
|
||||
testDir = join(tmpdir(), `codexlens-test-${Date.now()}`);
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
|
||||
// Create test Python files
|
||||
writeFileSync(join(testDir, 'main.py'), `
|
||||
"""Main module for testing."""
|
||||
|
||||
def hello_world():
|
||||
"""Say hello to the world."""
|
||||
print("Hello, World!")
|
||||
return "hello"
|
||||
|
||||
def calculate_sum(a, b):
|
||||
"""Calculate sum of two numbers."""
|
||||
return a + b
|
||||
|
||||
class Calculator:
|
||||
"""A simple calculator class."""
|
||||
|
||||
def __init__(self):
|
||||
self.result = 0
|
||||
|
||||
def add(self, value):
|
||||
"""Add value to result."""
|
||||
self.result += value
|
||||
return self.result
|
||||
|
||||
def subtract(self, value):
|
||||
"""Subtract value from result."""
|
||||
self.result -= value
|
||||
return self.result
|
||||
`);
|
||||
|
||||
writeFileSync(join(testDir, 'utils.py'), `
|
||||
"""Utility functions."""
|
||||
|
||||
def format_string(text):
|
||||
"""Format a string."""
|
||||
return text.strip().lower()
|
||||
|
||||
def validate_email(email):
|
||||
"""Validate email format."""
|
||||
return "@" in email and "." in email
|
||||
|
||||
async def fetch_data(url):
|
||||
"""Fetch data from URL (async)."""
|
||||
pass
|
||||
`);
|
||||
|
||||
// Create test JavaScript file
|
||||
writeFileSync(join(testDir, 'app.js'), `
|
||||
/**
|
||||
* Main application module
|
||||
*/
|
||||
|
||||
function initApp() {
|
||||
console.log('App initialized');
|
||||
}
|
||||
|
||||
const processData = async (data) => {
|
||||
return data.map(item => item.value);
|
||||
};
|
||||
|
||||
class Application {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
start() {
|
||||
console.log(\`Starting \${this.name}\`);
|
||||
}
|
||||
}
|
||||
|
||||
export { initApp, processData, Application };
|
||||
`);
|
||||
|
||||
console.log(`Test directory created at: ${testDir}`);
|
||||
} catch (err) {
|
||||
console.log('Setup failed:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
// Cleanup test directory
|
||||
if (testDir && existsSync(testDir)) {
|
||||
try {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
console.log('Test directory cleaned up');
|
||||
} catch (err) {
|
||||
console.log('Cleanup failed:', err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('Index Initialization', () => {
|
||||
it('should initialize index for test directory', async () => {
|
||||
if (!isReady || !testDir) {
|
||||
console.log('Skipping: CodexLens not ready or test dir not created');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'init',
|
||||
path: testDir
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
assert.ok('success' in result, 'Result should have success property');
|
||||
|
||||
if (result.success) {
|
||||
// Check that .codexlens directory was created
|
||||
const codexlensDir = join(testDir, '.codexlens');
|
||||
assert.ok(existsSync(codexlensDir), '.codexlens directory should exist');
|
||||
}
|
||||
});
|
||||
|
||||
it('should create index.db file', async () => {
|
||||
if (!isReady || !testDir) {
|
||||
console.log('Skipping: CodexLens not ready or test dir not created');
|
||||
return;
|
||||
}
|
||||
|
||||
const indexDb = join(testDir, '.codexlens', 'index.db');
|
||||
|
||||
// May need to wait for previous init to complete
|
||||
// Index.db should exist after successful init
|
||||
if (existsSync(join(testDir, '.codexlens'))) {
|
||||
// Check files in .codexlens directory
|
||||
const files = readdirSync(join(testDir, '.codexlens'));
|
||||
console.log('.codexlens contents:', files);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status Query', () => {
|
||||
it('should return index status for test directory', async () => {
|
||||
if (!isReady || !testDir) {
|
||||
console.log('Skipping: CodexLens not ready or test dir not created');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'status',
|
||||
path: testDir
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
console.log('Index status:', JSON.stringify(result, null, 2));
|
||||
|
||||
if (result.success) {
|
||||
// Navigate nested structure: result.status.result or result.result
|
||||
const statusData = result.status?.result || result.result || result.status || result;
|
||||
const hasIndexInfo = (
|
||||
'files' in statusData ||
|
||||
'db_path' in statusData ||
|
||||
result.output ||
|
||||
(result.status && 'success' in result.status)
|
||||
);
|
||||
assert.ok(hasIndexInfo, 'Status should contain index information or raw output');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Symbol Extraction', () => {
|
||||
it('should extract symbols from Python file', async () => {
|
||||
if (!isReady || !testDir) {
|
||||
console.log('Skipping: CodexLens not ready or test dir not created');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'symbol',
|
||||
file: join(testDir, 'main.py')
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
|
||||
if (result.success) {
|
||||
console.log('Symbols found:', result.symbols || result.output);
|
||||
|
||||
// Parse output if needed
|
||||
let symbols = result.symbols;
|
||||
if (!symbols && result.output) {
|
||||
try {
|
||||
const parsed = JSON.parse(result.output);
|
||||
symbols = parsed.result?.file?.symbols || parsed.symbols;
|
||||
} catch {
|
||||
// Keep raw output
|
||||
}
|
||||
}
|
||||
|
||||
if (symbols && Array.isArray(symbols)) {
|
||||
// Check for expected symbols
|
||||
const symbolNames = symbols.map(s => s.name);
|
||||
assert.ok(symbolNames.includes('hello_world') || symbolNames.some(n => n.includes('hello')),
|
||||
'Should find hello_world function');
|
||||
assert.ok(symbolNames.includes('Calculator') || symbolNames.some(n => n.includes('Calc')),
|
||||
'Should find Calculator class');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should extract symbols from JavaScript file', async () => {
|
||||
if (!isReady || !testDir) {
|
||||
console.log('Skipping: CodexLens not ready or test dir not created');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'symbol',
|
||||
file: join(testDir, 'app.js')
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
|
||||
if (result.success) {
|
||||
console.log('JS Symbols found:', result.symbols || result.output);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Full-Text Search', () => {
|
||||
it('should search for text in indexed files', async () => {
|
||||
if (!isReady || !testDir) {
|
||||
console.log('Skipping: CodexLens not ready or test dir not created');
|
||||
return;
|
||||
}
|
||||
|
||||
// First ensure index is initialized
|
||||
await codexLensModule.codexLensTool.execute({
|
||||
action: 'init',
|
||||
path: testDir
|
||||
});
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'search',
|
||||
query: 'hello',
|
||||
path: testDir,
|
||||
limit: 10
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
|
||||
if (result.success) {
|
||||
console.log('Search results:', result.results || result.output);
|
||||
}
|
||||
});
|
||||
|
||||
it('should search for class names', async () => {
|
||||
if (!isReady || !testDir) {
|
||||
console.log('Skipping: CodexLens not ready or test dir not created');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'search',
|
||||
query: 'Calculator',
|
||||
path: testDir,
|
||||
limit: 10
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
|
||||
if (result.success) {
|
||||
console.log('Class search results:', result.results || result.output);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Incremental Update', () => {
|
||||
it('should update index when file changes', async () => {
|
||||
if (!isReady || !testDir) {
|
||||
console.log('Skipping: CodexLens not ready or test dir not created');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new file
|
||||
const newFile = join(testDir, 'new_module.py');
|
||||
writeFileSync(newFile, `
|
||||
def new_function():
|
||||
"""A newly added function."""
|
||||
return "new"
|
||||
`);
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'update',
|
||||
files: [newFile],
|
||||
path: testDir
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
|
||||
if (result.success) {
|
||||
console.log('Update result:', result.updateResult || result.output);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle deleted files in update', async () => {
|
||||
if (!isReady || !testDir) {
|
||||
console.log('Skipping: CodexLens not ready or test dir not created');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reference a non-existent file
|
||||
const deletedFile = join(testDir, 'deleted_file.py');
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'update',
|
||||
files: [deletedFile],
|
||||
path: testDir
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
// Should handle gracefully without crashing
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexLens CLI Commands via executeCodexLens', async () => {
|
||||
let codexLensModule;
|
||||
let isReady = false;
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
codexLensModule = await import(codexLensPath);
|
||||
const status = await codexLensModule.checkVenvStatus();
|
||||
isReady = status.ready;
|
||||
} catch (err) {
|
||||
console.log('Setup failed:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
it('should execute --version command', async () => {
|
||||
if (!isReady) {
|
||||
console.log('Skipping: CodexLens not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: codexlens may not have --version, use --help instead
|
||||
const result = await codexLensModule.executeCodexLens(['--help']);
|
||||
assert.ok(typeof result === 'object');
|
||||
|
||||
if (result.success) {
|
||||
assert.ok(result.output, 'Should have output');
|
||||
}
|
||||
});
|
||||
|
||||
it('should execute status --json command', async () => {
|
||||
if (!isReady) {
|
||||
console.log('Skipping: CodexLens not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.executeCodexLens(['status', '--json'], {
|
||||
cwd: __dirname
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object');
|
||||
|
||||
if (result.success && result.output) {
|
||||
// Try to parse JSON output
|
||||
try {
|
||||
const parsed = JSON.parse(result.output);
|
||||
assert.ok(typeof parsed === 'object', 'Output should be valid JSON');
|
||||
} catch {
|
||||
// Output might not be JSON if index doesn't exist
|
||||
console.log('Status output (non-JSON):', result.output);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle inspect command', async () => {
|
||||
if (!isReady) {
|
||||
console.log('Skipping: CodexLens not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use this test file as input
|
||||
const testFile = join(__dirname, 'codex-lens.test.js');
|
||||
if (!existsSync(testFile)) {
|
||||
console.log('Skipping: Test file not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.executeCodexLens([
|
||||
'inspect', testFile, '--json'
|
||||
]);
|
||||
|
||||
assert.ok(typeof result === 'object');
|
||||
|
||||
if (result.success) {
|
||||
console.log('Inspect result received');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexLens Workspace Detection', async () => {
|
||||
let codexLensModule;
|
||||
let isReady = false;
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
codexLensModule = await import(codexLensPath);
|
||||
const status = await codexLensModule.checkVenvStatus();
|
||||
isReady = status.ready;
|
||||
} catch (err) {
|
||||
console.log('Setup failed:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
it('should detect existing workspace', async () => {
|
||||
if (!isReady) {
|
||||
console.log('Skipping: CodexLens not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to get status from project root where .codexlens might exist
|
||||
const projectRoot = join(__dirname, '..', '..');
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'status',
|
||||
path: projectRoot
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object');
|
||||
console.log('Project root status:', result.success ? 'Found' : 'Not found');
|
||||
});
|
||||
|
||||
it('should use global database when workspace not found', async () => {
|
||||
if (!isReady) {
|
||||
console.log('Skipping: CodexLens not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a path that definitely won't have .codexlens
|
||||
const tempPath = tmpdir();
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'status',
|
||||
path: tempPath
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object');
|
||||
// Should fall back to global database
|
||||
});
|
||||
});
|
||||
471
ccw/tests/codex-lens.test.js
Normal file
471
ccw/tests/codex-lens.test.js
Normal file
@@ -0,0 +1,471 @@
|
||||
/**
|
||||
* Tests for CodexLens API endpoints and tool integration
|
||||
*
|
||||
* Tests the following endpoints:
|
||||
* - GET /api/codexlens/status
|
||||
* - POST /api/codexlens/bootstrap
|
||||
* - POST /api/codexlens/init
|
||||
* - GET /api/codexlens/semantic/status
|
||||
* - POST /api/codexlens/semantic/install
|
||||
*
|
||||
* Also tests the codex-lens.js tool functions directly
|
||||
*/
|
||||
|
||||
import { describe, it, before, after, mock } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { createServer } from 'http';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Import the codex-lens module - use file:// URL format for Windows compatibility
|
||||
const codexLensPath = new URL('../src/tools/codex-lens.js', import.meta.url).href;
|
||||
|
||||
describe('CodexLens Tool Functions', async () => {
|
||||
let codexLensModule;
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
codexLensModule = await import(codexLensPath);
|
||||
} catch (err) {
|
||||
console.log('Note: codex-lens module import skipped (module may not be available):', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
describe('checkVenvStatus', () => {
|
||||
it('should return an object with ready property', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await codexLensModule.checkVenvStatus();
|
||||
assert.ok(typeof status === 'object', 'Status should be an object');
|
||||
assert.ok('ready' in status, 'Status should have ready property');
|
||||
assert.ok(typeof status.ready === 'boolean', 'ready should be boolean');
|
||||
|
||||
if (status.ready) {
|
||||
assert.ok('version' in status, 'Ready status should include version');
|
||||
} else {
|
||||
assert.ok('error' in status, 'Not ready status should include error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSemanticStatus', () => {
|
||||
it('should return semantic availability status', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await codexLensModule.checkSemanticStatus();
|
||||
assert.ok(typeof status === 'object', 'Status should be an object');
|
||||
assert.ok('available' in status, 'Status should have available property');
|
||||
assert.ok(typeof status.available === 'boolean', 'available should be boolean');
|
||||
|
||||
if (status.available) {
|
||||
assert.ok('backend' in status, 'Available status should include backend');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeCodexLens', () => {
|
||||
it('should execute codexlens command and return result', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// First check if CodexLens is ready
|
||||
const status = await codexLensModule.checkVenvStatus();
|
||||
if (!status.ready) {
|
||||
console.log('Skipping: CodexLens not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute a simple status command
|
||||
const result = await codexLensModule.executeCodexLens(['--help']);
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
assert.ok('success' in result, 'Result should have success property');
|
||||
|
||||
// --help should succeed
|
||||
if (result.success) {
|
||||
assert.ok('output' in result, 'Success result should have output');
|
||||
assert.ok(result.output.includes('CodexLens') || result.output.includes('codexlens'),
|
||||
'Help output should mention CodexLens');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle timeout gracefully', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await codexLensModule.checkVenvStatus();
|
||||
if (!status.ready) {
|
||||
console.log('Skipping: CodexLens not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a very short timeout to trigger timeout behavior
|
||||
// Note: This test may not always trigger timeout depending on system speed
|
||||
const result = await codexLensModule.executeCodexLens(['status', '--json'], { timeout: 1 });
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
assert.ok('success' in result, 'Result should have success property');
|
||||
});
|
||||
});
|
||||
|
||||
describe('codexLensTool.execute', () => {
|
||||
it('should handle check action', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({ action: 'check' });
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
assert.ok('ready' in result, 'Check result should have ready property');
|
||||
});
|
||||
|
||||
it('should throw error for unknown action', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
async () => codexLensModule.codexLensTool.execute({ action: 'unknown_action' }),
|
||||
/Unknown action/,
|
||||
'Should throw error for unknown action'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle status action', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const checkResult = await codexLensModule.checkVenvStatus();
|
||||
if (!checkResult.ready) {
|
||||
console.log('Skipping: CodexLens not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'status',
|
||||
path: __dirname
|
||||
});
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
assert.ok('success' in result, 'Result should have success property');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexLens API Endpoints (Integration)', async () => {
|
||||
// These tests require a running server
|
||||
// They test the actual HTTP endpoints
|
||||
|
||||
const TEST_PORT = 19999;
|
||||
let serverModule;
|
||||
let server;
|
||||
let baseUrl;
|
||||
|
||||
before(async () => {
|
||||
// Note: We cannot easily start the ccw server in tests
|
||||
// So we test the endpoint handlers directly or mock the server
|
||||
baseUrl = `http://localhost:${TEST_PORT}`;
|
||||
|
||||
// Try to import server module for handler testing
|
||||
try {
|
||||
// serverModule = await import(join(__dirname, '..', 'src', 'core', 'server.js'));
|
||||
console.log('Note: Server integration tests require manual server start');
|
||||
} catch (err) {
|
||||
console.log('Server module not available for direct testing');
|
||||
}
|
||||
});
|
||||
|
||||
describe('GET /api/codexlens/status', () => {
|
||||
it('should return JSON response with ready status', async () => {
|
||||
// This test requires a running server
|
||||
// Skip if server is not running
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/codexlens/status`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
assert.ok(typeof data === 'object', 'Response should be JSON object');
|
||||
assert.ok('ready' in data, 'Response should have ready property');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.cause?.code === 'ECONNREFUSED') {
|
||||
console.log('Skipping: Server not running on port', TEST_PORT);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/codexlens/init', () => {
|
||||
it('should initialize index for given path', async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/codexlens/init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: __dirname })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
assert.ok(typeof data === 'object', 'Response should be JSON object');
|
||||
assert.ok('success' in data, 'Response should have success property');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.cause?.code === 'ECONNREFUSED') {
|
||||
console.log('Skipping: Server not running on port', TEST_PORT);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/codexlens/semantic/status', () => {
|
||||
it('should return semantic search status', async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/codexlens/semantic/status`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
assert.ok(typeof data === 'object', 'Response should be JSON object');
|
||||
assert.ok('available' in data, 'Response should have available property');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.cause?.code === 'ECONNREFUSED') {
|
||||
console.log('Skipping: Server not running on port', TEST_PORT);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexLens Tool Definition', async () => {
|
||||
let codexLensModule;
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
codexLensModule = await import(codexLensPath);
|
||||
} catch (err) {
|
||||
console.log('Note: codex-lens module not available');
|
||||
}
|
||||
});
|
||||
|
||||
it('should have correct tool name', () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
assert.strictEqual(codexLensModule.codexLensTool.name, 'codex_lens');
|
||||
});
|
||||
|
||||
it('should have description', () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
assert.ok(codexLensModule.codexLensTool.description, 'Should have description');
|
||||
assert.ok(codexLensModule.codexLensTool.description.includes('CodexLens'),
|
||||
'Description should mention CodexLens');
|
||||
});
|
||||
|
||||
it('should have parameters schema', () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const { parameters } = codexLensModule.codexLensTool;
|
||||
assert.ok(parameters, 'Should have parameters');
|
||||
assert.strictEqual(parameters.type, 'object');
|
||||
assert.ok(parameters.properties, 'Should have properties');
|
||||
assert.ok(parameters.properties.action, 'Should have action property');
|
||||
assert.deepStrictEqual(parameters.required, ['action'], 'action should be required');
|
||||
});
|
||||
|
||||
it('should support all documented actions', () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const { parameters } = codexLensModule.codexLensTool;
|
||||
const supportedActions = parameters.properties.action.enum;
|
||||
|
||||
const expectedActions = ['init', 'search', 'symbol', 'status', 'update', 'bootstrap', 'check'];
|
||||
|
||||
for (const action of expectedActions) {
|
||||
assert.ok(supportedActions.includes(action), `Should support ${action} action`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have execute function', () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
assert.ok(typeof codexLensModule.codexLensTool.execute === 'function',
|
||||
'Should have execute function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexLens Path Configuration', () => {
|
||||
it('should use correct venv path based on platform', async () => {
|
||||
const codexLensDataDir = join(homedir(), '.codexlens');
|
||||
const codexLensVenv = join(codexLensDataDir, 'venv');
|
||||
|
||||
const expectedPython = process.platform === 'win32'
|
||||
? join(codexLensVenv, 'Scripts', 'python.exe')
|
||||
: join(codexLensVenv, 'bin', 'python');
|
||||
|
||||
// Just verify the path construction logic is correct
|
||||
assert.ok(expectedPython.includes('codexlens'), 'Python path should include codexlens');
|
||||
assert.ok(expectedPython.includes('venv'), 'Python path should include venv');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
assert.ok(expectedPython.includes('Scripts'), 'Windows should use Scripts directory');
|
||||
assert.ok(expectedPython.endsWith('.exe'), 'Windows should have .exe extension');
|
||||
} else {
|
||||
assert.ok(expectedPython.includes('bin'), 'Unix should use bin directory');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexLens Error Handling', async () => {
|
||||
let codexLensModule;
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
codexLensModule = await import(codexLensPath);
|
||||
} catch (err) {
|
||||
console.log('Note: codex-lens module not available');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle missing file parameter for symbol action', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const checkResult = await codexLensModule.checkVenvStatus();
|
||||
if (!checkResult.ready) {
|
||||
console.log('Skipping: CodexLens not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'symbol'
|
||||
// file is missing
|
||||
});
|
||||
|
||||
// Should either error or return success: false
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
});
|
||||
|
||||
it('should handle missing files parameter for update action', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'update'
|
||||
// files is missing
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
assert.strictEqual(result.success, false, 'Should return success: false');
|
||||
assert.ok(result.error, 'Should have error message');
|
||||
assert.ok(result.error.includes('files'), 'Error should mention files parameter');
|
||||
});
|
||||
|
||||
it('should handle empty files array for update action', async () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await codexLensModule.codexLensTool.execute({
|
||||
action: 'update',
|
||||
files: []
|
||||
});
|
||||
|
||||
assert.ok(typeof result === 'object', 'Result should be an object');
|
||||
assert.strictEqual(result.success, false, 'Should return success: false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexLens Search Parameters', async () => {
|
||||
let codexLensModule;
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
codexLensModule = await import(codexLensPath);
|
||||
} catch (err) {
|
||||
console.log('Note: codex-lens module not available');
|
||||
}
|
||||
});
|
||||
|
||||
it('should support text and semantic search modes', () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const { parameters } = codexLensModule.codexLensTool;
|
||||
const modeEnum = parameters.properties.mode?.enum;
|
||||
|
||||
assert.ok(modeEnum, 'Should have mode enum');
|
||||
assert.ok(modeEnum.includes('text'), 'Should support text mode');
|
||||
assert.ok(modeEnum.includes('semantic'), 'Should support semantic mode');
|
||||
});
|
||||
|
||||
it('should have limit parameter with default', () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const { parameters } = codexLensModule.codexLensTool;
|
||||
const limitProp = parameters.properties.limit;
|
||||
|
||||
assert.ok(limitProp, 'Should have limit property');
|
||||
assert.strictEqual(limitProp.type, 'number', 'limit should be number');
|
||||
assert.strictEqual(limitProp.default, 20, 'Default limit should be 20');
|
||||
});
|
||||
|
||||
it('should support output format options', () => {
|
||||
if (!codexLensModule) {
|
||||
console.log('Skipping: codex-lens module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const { parameters } = codexLensModule.codexLensTool;
|
||||
const formatEnum = parameters.properties.format?.enum;
|
||||
|
||||
assert.ok(formatEnum, 'Should have format enum');
|
||||
assert.ok(formatEnum.includes('json'), 'Should support json format');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user