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:
catlog22
2025-12-12 19:55:35 +08:00
parent c42f91a7fe
commit 4faa5f1c95
27 changed files with 4812 additions and 129 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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 ==========
/**

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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}`);
}
});