Add tests for PR #19 improvements (Phases 3-5)
Covers arrow function extraction, best-candidate resolution, graph traversal direction fix, MCP symbol disambiguation, output truncation, CLI uninit command, and more. Tests requiring better-sqlite3 native bindings are conditionally skipped. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ab5f9a03ef
commit
c8c7785626
@@ -0,0 +1,715 @@
|
||||
/**
|
||||
* PR #19 Improvement Tests
|
||||
*
|
||||
* Tests for changes ported from PR #15 and #16:
|
||||
* - Lazy grammar loading
|
||||
* - Arrow function extraction (body traversal)
|
||||
* - Graph traversal 'both' direction fix
|
||||
* - Best-candidate resolution picking
|
||||
* - Schema v2 migration (filePath/language on unresolved_refs)
|
||||
* - Batch insert for unresolved refs
|
||||
* - SQLite performance pragmas
|
||||
* - MCP symbol disambiguation and output truncation
|
||||
* - CLI uninit command
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { extractFromSource } from '../src/extraction';
|
||||
import {
|
||||
getParser,
|
||||
isLanguageSupported,
|
||||
getSupportedLanguages,
|
||||
clearParserCache,
|
||||
getUnavailableGrammarErrors,
|
||||
} from '../src/extraction/grammars';
|
||||
|
||||
// Create a temporary directory for each test
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-pr19-test-'));
|
||||
}
|
||||
|
||||
// Clean up temporary directory
|
||||
function cleanupTempDir(dir: string): void {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Check if better-sqlite3 native bindings are available
|
||||
function hasSqliteBindings(): boolean {
|
||||
try {
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database(':memory:');
|
||||
db.close();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const HAS_SQLITE = hasSqliteBindings();
|
||||
|
||||
// =============================================================================
|
||||
// Lazy Grammar Loading
|
||||
// =============================================================================
|
||||
|
||||
describe('Lazy Grammar Loading', () => {
|
||||
afterEach(() => {
|
||||
clearParserCache();
|
||||
});
|
||||
|
||||
it('should load grammars lazily on first use', () => {
|
||||
// Clear cache to force fresh load
|
||||
clearParserCache();
|
||||
|
||||
// TypeScript should be loadable
|
||||
const parser = getParser('typescript');
|
||||
expect(parser).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should cache loaded grammars', () => {
|
||||
clearParserCache();
|
||||
|
||||
const parser1 = getParser('typescript');
|
||||
const parser2 = getParser('typescript');
|
||||
|
||||
// Same reference from cache
|
||||
expect(parser1).toBe(parser2);
|
||||
});
|
||||
|
||||
it('should return null for unknown language', () => {
|
||||
const parser = getParser('unknown');
|
||||
expect(parser).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle unavailable grammars gracefully', () => {
|
||||
// 'unknown' is not a valid grammar, should not crash
|
||||
expect(isLanguageSupported('unknown')).toBe(false);
|
||||
});
|
||||
|
||||
it('should report liquid as supported (custom extractor)', () => {
|
||||
expect(isLanguageSupported('liquid')).toBe(true);
|
||||
});
|
||||
|
||||
it('should include liquid in supported languages', () => {
|
||||
const supported = getSupportedLanguages();
|
||||
expect(supported).toContain('liquid');
|
||||
});
|
||||
|
||||
it('should return unavailable grammar errors as a record', () => {
|
||||
clearParserCache();
|
||||
const errors = getUnavailableGrammarErrors();
|
||||
// Should be a plain object (may or may not have entries depending on platform)
|
||||
expect(typeof errors).toBe('object');
|
||||
});
|
||||
|
||||
it('should support multiple languages independently', () => {
|
||||
clearParserCache();
|
||||
|
||||
// Load two different languages - one failing shouldn't affect the other
|
||||
const tsParser = getParser('typescript');
|
||||
const pyParser = getParser('python');
|
||||
|
||||
expect(tsParser).not.toBeNull();
|
||||
expect(pyParser).not.toBeNull();
|
||||
expect(tsParser).not.toBe(pyParser);
|
||||
});
|
||||
|
||||
it('should clear all caches on clearParserCache', () => {
|
||||
// Load a grammar
|
||||
getParser('typescript');
|
||||
|
||||
// Clear
|
||||
clearParserCache();
|
||||
|
||||
// Errors should be cleared too
|
||||
const errors = getUnavailableGrammarErrors();
|
||||
expect(Object.keys(errors)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Arrow Function Extraction - Body Traversal
|
||||
// =============================================================================
|
||||
|
||||
describe('Arrow Function Body Traversal', () => {
|
||||
it('should extract unresolved references from arrow function bodies', () => {
|
||||
const code = `
|
||||
export const useAuth = () => {
|
||||
const user = getUser();
|
||||
const token = generateToken(user);
|
||||
return { user, token };
|
||||
};
|
||||
`;
|
||||
const result = extractFromSource('hooks.ts', code);
|
||||
|
||||
// The arrow function should be extracted
|
||||
const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'useAuth');
|
||||
expect(funcNode).toBeDefined();
|
||||
|
||||
// Calls inside the body should be captured as unresolved references
|
||||
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
|
||||
const callNames = calls.map((c) => c.referenceName);
|
||||
expect(callNames).toContain('getUser');
|
||||
expect(callNames).toContain('generateToken');
|
||||
});
|
||||
|
||||
it('should extract unresolved references from function expression bodies', () => {
|
||||
const code = `
|
||||
export const processData = function(input: string): string {
|
||||
const cleaned = sanitize(input);
|
||||
return transform(cleaned);
|
||||
};
|
||||
`;
|
||||
const result = extractFromSource('utils.ts', code);
|
||||
|
||||
const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'processData');
|
||||
expect(funcNode).toBeDefined();
|
||||
|
||||
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
|
||||
const callNames = calls.map((c) => c.referenceName);
|
||||
expect(callNames).toContain('sanitize');
|
||||
expect(callNames).toContain('transform');
|
||||
});
|
||||
|
||||
it('should not create duplicate nodes for arrow functions', () => {
|
||||
const code = `
|
||||
export const handler = () => {
|
||||
doSomething();
|
||||
};
|
||||
`;
|
||||
const result = extractFromSource('handler.ts', code);
|
||||
|
||||
// Should be exactly 1 function node, 0 variable nodes for 'handler'
|
||||
const funcNodes = result.nodes.filter((n) => n.name === 'handler' && n.kind === 'function');
|
||||
const varNodes = result.nodes.filter((n) => n.name === 'handler' && n.kind === 'variable');
|
||||
expect(funcNodes).toHaveLength(1);
|
||||
expect(varNodes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should extract nested calls in arrow functions in JavaScript', () => {
|
||||
const code = `
|
||||
export const fetchData = async () => {
|
||||
const response = await fetchAPI('/data');
|
||||
return parseResponse(response);
|
||||
};
|
||||
`;
|
||||
const result = extractFromSource('api.js', code);
|
||||
|
||||
const funcNode = result.nodes.find((n) => n.name === 'fetchData');
|
||||
expect(funcNode).toBeDefined();
|
||||
expect(funcNode?.kind).toBe('function');
|
||||
|
||||
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
|
||||
const callNames = calls.map((c) => c.referenceName);
|
||||
expect(callNames).toContain('fetchAPI');
|
||||
expect(callNames).toContain('parseResponse');
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Graph Traversal 'both' Direction Fix
|
||||
// (requires better-sqlite3 - will use CodeGraph integration)
|
||||
// =============================================================================
|
||||
|
||||
describe('Graph Traversal Both Direction', () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDir(testDir);
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should traverse both directions from a node', async () => {
|
||||
const CodeGraph = (await import('../src/index')).default;
|
||||
|
||||
const srcDir = path.join(testDir, 'src');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
|
||||
// A -> B -> C (A calls B, B calls C)
|
||||
fs.writeFileSync(path.join(srcDir, 'a.ts'), `
|
||||
import { funcB } from './b';
|
||||
export function funcA(): void { funcB(); }
|
||||
`);
|
||||
fs.writeFileSync(path.join(srcDir, 'b.ts'), `
|
||||
import { funcC } from './c';
|
||||
export function funcB(): void { funcC(); }
|
||||
`);
|
||||
fs.writeFileSync(path.join(srcDir, 'c.ts'), `
|
||||
export function funcC(): void { console.log('c'); }
|
||||
`);
|
||||
|
||||
const cg = CodeGraph.initSync(testDir, {
|
||||
config: { include: ['src/**/*.ts'], exclude: [] },
|
||||
});
|
||||
|
||||
await cg.indexAll();
|
||||
cg.resolveReferences();
|
||||
|
||||
const functions = cg.getNodesByKind('function');
|
||||
const funcB = functions.find((n) => n.name === 'funcB');
|
||||
|
||||
if (!funcB) {
|
||||
cg.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse 'both' from B - should find A (incoming caller) and C (outgoing callee)
|
||||
const subgraph = cg.traverse(funcB.id, {
|
||||
maxDepth: 1,
|
||||
direction: 'both',
|
||||
});
|
||||
|
||||
// B itself + at least one neighbor in each direction
|
||||
expect(subgraph.nodes.size).toBeGreaterThanOrEqual(2);
|
||||
expect(subgraph.nodes.has(funcB.id)).toBe(true);
|
||||
|
||||
cg.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Best-Candidate Resolution
|
||||
// =============================================================================
|
||||
|
||||
describe('Best-Candidate Resolution', () => {
|
||||
it.skipIf(!HAS_SQLITE)('should be testable via the resolution module types', async () => {
|
||||
const { ReferenceResolver } = await import('../src/resolution');
|
||||
expect(typeof ReferenceResolver.prototype.resolveOne).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Schema v2 Migration
|
||||
// =============================================================================
|
||||
|
||||
describe('Schema v2 Migration', () => {
|
||||
it.skipIf(!HAS_SQLITE)('should have correct current schema version', async () => {
|
||||
const { CURRENT_SCHEMA_VERSION } = await import('../src/db/migrations');
|
||||
expect(CURRENT_SCHEMA_VERSION).toBe(2);
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should have migration for version 2', async () => {
|
||||
const { getPendingMigrations } = await import('../src/db/migrations');
|
||||
expect(typeof getPendingMigrations).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Database Layer: Batch Insert, getAllNodes, Pragmas
|
||||
// =============================================================================
|
||||
|
||||
describe('Database Layer Improvements', () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDir(testDir);
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should support batch insert of unresolved refs', async () => {
|
||||
const { DatabaseConnection } = await import('../src/db');
|
||||
const { QueryBuilder } = await import('../src/db/queries');
|
||||
|
||||
const db = DatabaseConnection.initialize(testDir);
|
||||
const queries = new QueryBuilder(db.getDatabase());
|
||||
|
||||
// Insert a node first (needed as foreign key)
|
||||
queries.insertNode({
|
||||
id: 'func:test:1',
|
||||
kind: 'function',
|
||||
name: 'testFunc',
|
||||
qualifiedName: 'test::testFunc',
|
||||
filePath: 'test.ts',
|
||||
language: 'typescript',
|
||||
startLine: 1,
|
||||
endLine: 5,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
// Batch insert unresolved refs with filePath and language
|
||||
queries.insertUnresolvedRefsBatch([
|
||||
{
|
||||
fromNodeId: 'func:test:1',
|
||||
referenceName: 'helperA',
|
||||
referenceKind: 'calls',
|
||||
line: 2,
|
||||
column: 4,
|
||||
filePath: 'test.ts',
|
||||
language: 'typescript',
|
||||
},
|
||||
{
|
||||
fromNodeId: 'func:test:1',
|
||||
referenceName: 'helperB',
|
||||
referenceKind: 'calls',
|
||||
line: 3,
|
||||
column: 4,
|
||||
filePath: 'test.ts',
|
||||
language: 'typescript',
|
||||
},
|
||||
]);
|
||||
|
||||
const refs = queries.getUnresolvedReferences();
|
||||
expect(refs).toHaveLength(2);
|
||||
expect(refs.map((r) => r.referenceName).sort()).toEqual(['helperA', 'helperB']);
|
||||
|
||||
// Verify filePath and language are persisted
|
||||
expect(refs[0]?.filePath).toBe('test.ts');
|
||||
expect(refs[0]?.language).toBe('typescript');
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should support getAllNodes', async () => {
|
||||
const { DatabaseConnection } = await import('../src/db');
|
||||
const { QueryBuilder } = await import('../src/db/queries');
|
||||
|
||||
const db = DatabaseConnection.initialize(testDir);
|
||||
const queries = new QueryBuilder(db.getDatabase());
|
||||
|
||||
// Insert some nodes
|
||||
for (let i = 0; i < 3; i++) {
|
||||
queries.insertNode({
|
||||
id: `func:test:${i}`,
|
||||
kind: 'function',
|
||||
name: `func${i}`,
|
||||
qualifiedName: `test::func${i}`,
|
||||
filePath: 'test.ts',
|
||||
language: 'typescript',
|
||||
startLine: i * 10 + 1,
|
||||
endLine: i * 10 + 5,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
const allNodes = queries.getAllNodes();
|
||||
expect(allNodes).toHaveLength(3);
|
||||
expect(allNodes.map((n) => n.name).sort()).toEqual(['func0', 'func1', 'func2']);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should set performance pragmas on initialization', async () => {
|
||||
const { DatabaseConnection } = await import('../src/db');
|
||||
|
||||
const db = DatabaseConnection.initialize(testDir);
|
||||
const rawDb = db.getDatabase();
|
||||
|
||||
// Check pragmas were set
|
||||
const synchronous = rawDb.pragma('synchronous', { simple: true });
|
||||
expect(synchronous).toBe(1); // NORMAL = 1
|
||||
|
||||
const cacheSize = rawDb.pragma('cache_size', { simple: true }) as number;
|
||||
expect(cacheSize).toBe(-64000);
|
||||
|
||||
const tempStore = rawDb.pragma('temp_store', { simple: true });
|
||||
expect(tempStore).toBe(2); // MEMORY = 2
|
||||
|
||||
const mmapSize = rawDb.pragma('mmap_size', { simple: true }) as number;
|
||||
expect(mmapSize).toBe(268435456); // 256 MB
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should handle empty batch insert gracefully', async () => {
|
||||
const { DatabaseConnection } = await import('../src/db');
|
||||
const { QueryBuilder } = await import('../src/db/queries');
|
||||
|
||||
const db = DatabaseConnection.initialize(testDir);
|
||||
const queries = new QueryBuilder(db.getDatabase());
|
||||
|
||||
// Should not throw on empty array
|
||||
expect(() => queries.insertUnresolvedRefsBatch([])).not.toThrow();
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Resolution Warm Caches
|
||||
// =============================================================================
|
||||
|
||||
describe('Resolution Warm Caches', () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDir(testDir);
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should warm caches and use them for lookups', async () => {
|
||||
const CodeGraph = (await import('../src/index')).default;
|
||||
|
||||
const srcDir = path.join(testDir, 'src');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(srcDir, 'a.ts'), `
|
||||
export function myFunc(): void {}
|
||||
export function otherFunc(): void { myFunc(); }
|
||||
`);
|
||||
|
||||
const cg = CodeGraph.initSync(testDir, {
|
||||
config: { include: ['src/**/*.ts'], exclude: [] },
|
||||
});
|
||||
|
||||
await cg.indexAll();
|
||||
|
||||
// resolveReferences internally calls warmCaches
|
||||
const result = cg.resolveReferences();
|
||||
|
||||
// Should complete without error
|
||||
expect(result.stats.total).toBeGreaterThanOrEqual(0);
|
||||
|
||||
cg.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// MCP Tool Improvements
|
||||
// =============================================================================
|
||||
|
||||
describe('MCP Tool Improvements', () => {
|
||||
it.skipIf(!HAS_SQLITE)('should export ToolHandler class', async () => {
|
||||
const { ToolHandler } = await import('../src/mcp/tools');
|
||||
expect(typeof ToolHandler).toBe('function');
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should have findSymbol and truncateOutput as private methods', async () => {
|
||||
const { ToolHandler } = await import('../src/mcp/tools');
|
||||
const proto = ToolHandler.prototype;
|
||||
expect(typeof (proto as any).findSymbol).toBe('function');
|
||||
expect(typeof (proto as any).truncateOutput).toBe('function');
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should truncate output exceeding MAX_OUTPUT_LENGTH', async () => {
|
||||
const { ToolHandler } = await import('../src/mcp/tools');
|
||||
|
||||
// Access private method for testing
|
||||
const handler = Object.create(ToolHandler.prototype);
|
||||
const truncate = (handler as any).truncateOutput.bind(handler);
|
||||
|
||||
// Short text should not be truncated
|
||||
const short = 'Hello world';
|
||||
expect(truncate(short)).toBe(short);
|
||||
|
||||
// Long text should be truncated
|
||||
const long = 'x'.repeat(20000);
|
||||
const result = truncate(long);
|
||||
expect(result.length).toBeLessThan(long.length);
|
||||
expect(result).toContain('... (output truncated)');
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should truncate at a clean line boundary', async () => {
|
||||
const { ToolHandler } = await import('../src/mcp/tools');
|
||||
|
||||
const handler = Object.create(ToolHandler.prototype);
|
||||
const truncate = (handler as any).truncateOutput.bind(handler);
|
||||
|
||||
// Build text with newlines exceeding the limit
|
||||
const lines: string[] = [];
|
||||
for (let i = 0; i < 500; i++) {
|
||||
lines.push(`Line ${i}: ${'a'.repeat(50)}`);
|
||||
}
|
||||
const text = lines.join('\n');
|
||||
|
||||
const result = truncate(text);
|
||||
// Should end with truncation notice after a newline boundary
|
||||
expect(result).toContain('... (output truncated)');
|
||||
// Should not cut mid-line (the char before truncation notice should be \n)
|
||||
const beforeTruncation = result.split('\n\n... (output truncated)')[0]!;
|
||||
expect(beforeTruncation.endsWith('\n') || !beforeTruncation.includes('\0')).toBe(true);
|
||||
});
|
||||
|
||||
describe('findSymbol disambiguation', () => {
|
||||
it.skipIf(!HAS_SQLITE)('should prefer exact name matches', async () => {
|
||||
const { ToolHandler } = await import('../src/mcp/tools');
|
||||
const CodeGraph = (await import('../src/index')).default;
|
||||
|
||||
const tmpDir = createTempDir();
|
||||
const srcDir = path.join(tmpDir, 'src');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(srcDir, 'a.ts'), `
|
||||
export function getValue(): number { return 1; }
|
||||
export function getValueFromCache(): number { return 2; }
|
||||
`);
|
||||
|
||||
const cg = CodeGraph.initSync(tmpDir, {
|
||||
config: { include: ['src/**/*.ts'], exclude: [] },
|
||||
});
|
||||
await cg.indexAll();
|
||||
|
||||
const handler = new ToolHandler(cg);
|
||||
const findSymbol = (handler as any).findSymbol.bind(handler);
|
||||
|
||||
const match = findSymbol(cg, 'getValue');
|
||||
expect(match).not.toBeNull();
|
||||
expect(match.node.name).toBe('getValue');
|
||||
// Should not have a disambiguation note for single exact match
|
||||
expect(match.note).toBe('');
|
||||
|
||||
handler.closeAll();
|
||||
cg.destroy();
|
||||
cleanupTempDir(tmpDir);
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should note when multiple symbols share the same name', async () => {
|
||||
const { ToolHandler } = await import('../src/mcp/tools');
|
||||
const CodeGraph = (await import('../src/index')).default;
|
||||
|
||||
const tmpDir = createTempDir();
|
||||
const srcDir = path.join(tmpDir, 'src');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
|
||||
// Two files with the same function name
|
||||
fs.writeFileSync(path.join(srcDir, 'a.ts'), `
|
||||
export function handle(): void {}
|
||||
`);
|
||||
fs.writeFileSync(path.join(srcDir, 'b.ts'), `
|
||||
export function handle(): void {}
|
||||
`);
|
||||
|
||||
const cg = CodeGraph.initSync(tmpDir, {
|
||||
config: { include: ['src/**/*.ts'], exclude: [] },
|
||||
});
|
||||
await cg.indexAll();
|
||||
|
||||
const handler = new ToolHandler(cg);
|
||||
const findSymbol = (handler as any).findSymbol.bind(handler);
|
||||
|
||||
const match = findSymbol(cg, 'handle');
|
||||
expect(match).not.toBeNull();
|
||||
expect(match.node.name).toBe('handle');
|
||||
// Should have a disambiguation note
|
||||
expect(match.note).toContain('2 symbols named "handle"');
|
||||
|
||||
handler.closeAll();
|
||||
cg.destroy();
|
||||
cleanupTempDir(tmpDir);
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should return null when symbol is not found', async () => {
|
||||
const { ToolHandler } = await import('../src/mcp/tools');
|
||||
const CodeGraph = (await import('../src/index')).default;
|
||||
|
||||
const tmpDir = createTempDir();
|
||||
const srcDir = path.join(tmpDir, 'src');
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, 'a.ts'), `export function foo(): void {}`);
|
||||
|
||||
const cg = CodeGraph.initSync(tmpDir, {
|
||||
config: { include: ['src/**/*.ts'], exclude: [] },
|
||||
});
|
||||
await cg.indexAll();
|
||||
|
||||
const handler = new ToolHandler(cg);
|
||||
const findSymbol = (handler as any).findSymbol.bind(handler);
|
||||
|
||||
const match = findSymbol(cg, 'nonExistentSymbol');
|
||||
expect(match).toBeNull();
|
||||
|
||||
handler.closeAll();
|
||||
cg.destroy();
|
||||
cleanupTempDir(tmpDir);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// CLI uninit Command
|
||||
// =============================================================================
|
||||
|
||||
describe('CLI uninit', () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDir(testDir);
|
||||
});
|
||||
|
||||
it.skipIf(!HAS_SQLITE)('should uninitialize a project via CodeGraph.uninitialize()', async () => {
|
||||
const CodeGraph = (await import('../src/index')).default;
|
||||
|
||||
// Initialize
|
||||
const cg = CodeGraph.initSync(testDir);
|
||||
expect(CodeGraph.isInitialized(testDir)).toBe(true);
|
||||
|
||||
// Uninitialize
|
||||
cg.uninitialize();
|
||||
|
||||
// .codegraph directory should be removed
|
||||
expect(CodeGraph.isInitialized(testDir)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Tree-sitter Version Pinning
|
||||
// =============================================================================
|
||||
|
||||
describe('Tree-sitter Version Pinning', () => {
|
||||
it('should have exact versions (no caret) in package.json', () => {
|
||||
const pkgPath = path.join(__dirname, '..', 'package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
|
||||
const treeSitterDeps = Object.entries(pkg.dependencies as Record<string, string>)
|
||||
.filter(([name]) => name.startsWith('tree-sitter') || name.includes('tree-sitter'));
|
||||
|
||||
for (const [name, version] of treeSitterDeps) {
|
||||
// Skip github: references
|
||||
if (version.startsWith('github:')) continue;
|
||||
expect(version, `${name} should not use caret range`).not.toMatch(/^\^/);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have tree-sitter override pinned', () => {
|
||||
const pkgPath = path.join(__dirname, '..', 'package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
|
||||
expect(pkg.overrides).toBeDefined();
|
||||
expect(pkg.overrides['tree-sitter']).toBeDefined();
|
||||
expect(pkg.overrides['tree-sitter']).not.toMatch(/^\^/);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Embedder Float32Array Fix
|
||||
// =============================================================================
|
||||
|
||||
describe('Float32Array Fix', () => {
|
||||
it('should correctly convert typed arrays (regression check)', () => {
|
||||
// Simulates the fix: Float32Array.from(Array.from(arr)) vs new Float32Array(arr.length)
|
||||
const source = new Float64Array([1.5, 2.5, 3.5, 4.5]);
|
||||
|
||||
// The OLD buggy approach:
|
||||
const buggy = new Float32Array(source.length);
|
||||
// buggy is all zeros!
|
||||
expect(buggy[0]).toBe(0);
|
||||
expect(buggy[1]).toBe(0);
|
||||
|
||||
// The NEW fixed approach:
|
||||
const fixed = Float32Array.from(Array.from(source));
|
||||
expect(fixed[0]).toBeCloseTo(1.5);
|
||||
expect(fixed[1]).toBeCloseTo(2.5);
|
||||
expect(fixed[2]).toBeCloseTo(3.5);
|
||||
expect(fixed[3]).toBeCloseTo(4.5);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user