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

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

View 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');
});
});