This commit is contained in:
Colby McHenry
2026-01-18 16:25:00 -06:00
parent 08ccabb5a9
commit cc6e7a5c89
57 changed files with 23315 additions and 1 deletions
+369
View File
@@ -0,0 +1,369 @@
/**
* Context Builder Tests
*
* Tests for the context building functionality.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src/index';
describe('Context Builder', () => {
let testDir: string;
let cg: CodeGraph;
beforeEach(async () => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-test-'));
// Create a sample codebase
const srcDir = path.join(testDir, 'src');
fs.mkdirSync(srcDir);
// Create a payment service file
fs.writeFileSync(
path.join(srcDir, 'payment.ts'),
`/**
* Payment Service
* Handles payment processing logic.
*/
export interface PaymentResult {
success: boolean;
transactionId: string;
amount: number;
}
export class PaymentService {
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
/**
* Process a payment for the given amount
*/
async processPayment(amount: number): Promise<PaymentResult> {
// Validate amount
if (amount <= 0) {
throw new Error('Invalid amount');
}
// Process payment
const transactionId = this.generateTransactionId();
return {
success: true,
transactionId,
amount,
};
}
private generateTransactionId(): string {
return 'txn_' + Math.random().toString(36).substring(2);
}
}
export function createPaymentService(apiKey: string): PaymentService {
return new PaymentService(apiKey);
}
`
);
// Create a checkout controller file
fs.writeFileSync(
path.join(srcDir, 'checkout.ts'),
`/**
* Checkout Controller
* Handles the checkout flow.
*/
import { PaymentService, PaymentResult } from './payment';
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
export class CheckoutController {
private paymentService: PaymentService;
constructor(paymentService: PaymentService) {
this.paymentService = paymentService;
}
/**
* Process checkout for the given cart
*/
async processCheckout(cart: CartItem[]): Promise<PaymentResult> {
const total = this.calculateTotal(cart);
if (total === 0) {
throw new Error('Cart is empty');
}
return this.paymentService.processPayment(total);
}
/**
* Calculate the total price of the cart
*/
calculateTotal(cart: CartItem[]): number {
return cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
}
`
);
// Create a utilities file
fs.writeFileSync(
path.join(srcDir, 'utils.ts'),
`/**
* Utility functions
*/
export function formatCurrency(amount: number): string {
return '$' + amount.toFixed(2);
}
export function validateEmail(email: string): boolean {
return email.includes('@');
}
`
);
// Initialize CodeGraph
cg = CodeGraph.initSync(testDir, {
config: {
include: ['**/*.ts'],
exclude: [],
},
});
// Index the codebase
await cg.indexAll();
});
afterEach(() => {
if (cg) {
cg.destroy();
}
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
describe('getCode()', () => {
it('should extract code for a node', async () => {
// Find the PaymentService class
const nodes = cg.getNodesByKind('class');
const paymentService = nodes.find((n) => n.name === 'PaymentService');
expect(paymentService).toBeDefined();
const code = await cg.getCode(paymentService!.id);
expect(code).not.toBeNull();
expect(code).toContain('class PaymentService');
expect(code).toContain('processPayment');
});
it('should return null for non-existent node', async () => {
const code = await cg.getCode('non-existent-id');
expect(code).toBeNull();
});
});
describe('findRelevantContext()', () => {
it('should find relevant nodes for a query', async () => {
// Use simple query that matches symbol names (FTS5 treats spaces as AND)
const result = await cg.findRelevantContext('PaymentService');
expect(result.nodes.size).toBeGreaterThan(0);
// Should find payment-related nodes
const nodeNames = Array.from(result.nodes.values()).map((n) => n.name);
expect(
nodeNames.some(
(name) =>
name.toLowerCase().includes('payment') ||
name.toLowerCase().includes('checkout')
)
).toBe(true);
});
it('should include edges in the result', async () => {
const result = await cg.findRelevantContext('checkout', {
traversalDepth: 2,
});
// Should have some edges from traversal
expect(result.edges).toBeDefined();
});
it('should respect maxNodes option', async () => {
const result = await cg.findRelevantContext('function', {
maxNodes: 5,
});
expect(result.nodes.size).toBeLessThanOrEqual(5);
});
});
describe('buildContext()', () => {
it('should build context with markdown format', async () => {
const result = await cg.buildContext('Fix checkout error', {
format: 'markdown',
maxCodeBlocks: 3,
});
expect(typeof result).toBe('string');
const markdown = result as string;
// Should contain markdown structure
expect(markdown).toContain('## Code Context');
expect(markdown).toContain('**Query:** Fix checkout error');
});
it('should build context with JSON format', async () => {
const result = await cg.buildContext('payment processing', {
format: 'json',
});
expect(typeof result).toBe('string');
const parsed = JSON.parse(result as string);
expect(parsed.query).toBe('payment processing');
expect(parsed.nodes).toBeDefined();
expect(Array.isArray(parsed.nodes)).toBe(true);
});
it('should accept object input with title and description', async () => {
const result = await cg.buildContext(
{
title: 'Checkout bug',
description: 'Cart total calculation is wrong',
},
{ format: 'markdown' }
);
expect(typeof result).toBe('string');
expect(result).toContain('Checkout bug: Cart total calculation is wrong');
});
it('should include code blocks when requested', async () => {
const result = await cg.buildContext('PaymentService', {
format: 'markdown',
includeCode: true,
maxCodeBlocks: 2,
});
const markdown = result as string;
// Should contain code blocks
expect(markdown).toContain('### Code');
expect(markdown).toContain('```typescript');
});
it('should exclude code blocks when requested', async () => {
const result = await cg.buildContext('payment', {
format: 'markdown',
includeCode: false,
});
const markdown = result as string;
// Should not contain code section
expect(markdown).not.toContain('### Code');
});
it('should include related files', async () => {
const result = await cg.buildContext('checkout', {
format: 'markdown',
});
const markdown = result as string;
expect(markdown).toContain('### Related Files');
});
it('should include stats in the output', async () => {
const result = await cg.buildContext('payment', {
format: 'markdown',
});
const markdown = result as string;
// Should have stats footer
expect(markdown).toMatch(/\*Context:.*symbols.*relationships.*files/);
});
});
describe('Context structure', () => {
it('should find entry points from search', async () => {
const result = await cg.buildContext('PaymentService', {
format: 'json',
});
const parsed = JSON.parse(result as string);
expect(parsed.entryPoints).toBeDefined();
expect(parsed.entryPoints.length).toBeGreaterThan(0);
});
it('should traverse graph from entry points', async () => {
const result = await cg.buildContext('CheckoutController', {
format: 'json',
traversalDepth: 2,
});
const parsed = JSON.parse(result as string);
// Should have found related nodes through traversal
const nodeNames = parsed.nodes.map((n: { name: string }) => n.name);
// CheckoutController calls PaymentService, so both should be present
expect(
nodeNames.some((name: string) => name.includes('Checkout'))
).toBe(true);
});
});
describe('Edge cases', () => {
it('should handle empty query', async () => {
const result = await cg.buildContext('', { format: 'markdown' });
expect(typeof result).toBe('string');
});
it('should handle query with no matches', async () => {
const result = await cg.buildContext('xyznonexistent123', {
format: 'json',
});
const parsed = JSON.parse(result as string);
// Should return empty or minimal results
expect(parsed.nodes).toBeDefined();
});
it('should truncate long code blocks', async () => {
const result = await cg.buildContext('PaymentService', {
format: 'markdown',
maxCodeBlockSize: 100,
includeCode: true,
});
const markdown = result as string;
// Long code blocks should be truncated
if (markdown.includes('```typescript')) {
// If there's a code block, check for truncation marker if content was long
// This test validates the truncation logic works
expect(typeof markdown).toBe('string');
}
});
});
});
+668
View File
@@ -0,0 +1,668 @@
/**
* Extraction Tests
*
* Tests for the tree-sitter extraction system.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { extractFromSource } from '../src/extraction';
import { detectLanguage, isLanguageSupported, getSupportedLanguages } from '../src/extraction/grammars';
// Create a temporary directory for each test
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'));
}
// Clean up temporary directory
function cleanupTempDir(dir: string): void {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
describe('Language Detection', () => {
it('should detect TypeScript files', () => {
expect(detectLanguage('src/index.ts')).toBe('typescript');
expect(detectLanguage('components/Button.tsx')).toBe('tsx');
});
it('should detect JavaScript files', () => {
expect(detectLanguage('index.js')).toBe('javascript');
expect(detectLanguage('App.jsx')).toBe('jsx');
expect(detectLanguage('config.mjs')).toBe('javascript');
});
it('should detect Python files', () => {
expect(detectLanguage('main.py')).toBe('python');
});
it('should detect Go files', () => {
expect(detectLanguage('main.go')).toBe('go');
});
it('should detect Rust files', () => {
expect(detectLanguage('lib.rs')).toBe('rust');
});
it('should detect Java files', () => {
expect(detectLanguage('Main.java')).toBe('java');
});
it('should detect C files', () => {
expect(detectLanguage('main.c')).toBe('c');
expect(detectLanguage('utils.h')).toBe('c');
});
it('should detect C++ files', () => {
expect(detectLanguage('main.cpp')).toBe('cpp');
expect(detectLanguage('class.hpp')).toBe('cpp');
});
it('should detect C# files', () => {
expect(detectLanguage('Program.cs')).toBe('csharp');
});
it('should detect PHP files', () => {
expect(detectLanguage('index.php')).toBe('php');
});
it('should detect Ruby files', () => {
expect(detectLanguage('app.rb')).toBe('ruby');
});
it('should detect Swift files', () => {
expect(detectLanguage('ViewController.swift')).toBe('swift');
});
it('should detect Kotlin files', () => {
expect(detectLanguage('MainActivity.kt')).toBe('kotlin');
expect(detectLanguage('build.gradle.kts')).toBe('kotlin');
});
it('should return unknown for unsupported extensions', () => {
expect(detectLanguage('styles.css')).toBe('unknown');
expect(detectLanguage('data.json')).toBe('unknown');
});
});
describe('Language Support', () => {
it('should report supported languages', () => {
expect(isLanguageSupported('typescript')).toBe(true);
expect(isLanguageSupported('python')).toBe(true);
expect(isLanguageSupported('go')).toBe(true);
expect(isLanguageSupported('unknown')).toBe(false);
});
it('should list all supported languages', () => {
const languages = getSupportedLanguages();
expect(languages).toContain('typescript');
expect(languages).toContain('javascript');
expect(languages).toContain('python');
expect(languages).toContain('go');
expect(languages).toContain('rust');
expect(languages).toContain('java');
expect(languages).toContain('csharp');
expect(languages).toContain('php');
expect(languages).toContain('ruby');
expect(languages).toContain('swift');
expect(languages).toContain('kotlin');
});
});
describe('TypeScript Extraction', () => {
it('should extract function declarations', () => {
const code = `
export function processPayment(amount: number): Promise<Receipt> {
return stripe.charge(amount);
}
`;
const result = extractFromSource('payment.ts', code);
expect(result.nodes).toHaveLength(1);
expect(result.nodes[0]).toMatchObject({
kind: 'function',
name: 'processPayment',
language: 'typescript',
isExported: true,
});
expect(result.nodes[0]?.signature).toContain('amount: number');
});
it('should extract class declarations', () => {
const code = `
export class PaymentService {
private stripe: StripeClient;
constructor(apiKey: string) {
this.stripe = new StripeClient(apiKey);
}
async charge(amount: number): Promise<Receipt> {
return this.stripe.charge(amount);
}
}
`;
const result = extractFromSource('service.ts', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
const methodNodes = result.nodes.filter((n) => n.kind === 'method');
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('PaymentService');
expect(classNode?.isExported).toBe(true);
expect(methodNodes.length).toBeGreaterThanOrEqual(1);
const chargeMethod = methodNodes.find((m) => m.name === 'charge');
expect(chargeMethod).toBeDefined();
});
it('should extract interfaces', () => {
const code = `
export interface User {
id: string;
name: string;
email: string;
}
`;
const result = extractFromSource('types.ts', code);
expect(result.nodes).toHaveLength(1);
expect(result.nodes[0]).toMatchObject({
kind: 'interface',
name: 'User',
isExported: true,
});
});
it('should track function calls', () => {
const code = `
function main() {
const result = processData();
console.log(result);
}
`;
const result = extractFromSource('main.ts', code);
expect(result.unresolvedReferences.length).toBeGreaterThan(0);
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
expect(calls.some((c) => c.referenceName === 'processData')).toBe(true);
});
});
describe('Python Extraction', () => {
it('should extract function definitions', () => {
const code = `
def calculate_total(items: list, tax_rate: float) -> float:
"""Calculate total with tax."""
subtotal = sum(item.price for item in items)
return subtotal * (1 + tax_rate)
`;
const result = extractFromSource('calc.py', code);
expect(result.nodes).toHaveLength(1);
expect(result.nodes[0]).toMatchObject({
kind: 'function',
name: 'calculate_total',
language: 'python',
});
});
it('should extract class definitions', () => {
const code = `
class UserService:
"""Service for managing users."""
def __init__(self, db):
self.db = db
def get_user(self, user_id: str) -> User:
return self.db.find_user(user_id)
`;
const result = extractFromSource('service.py', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('UserService');
});
});
describe('Go Extraction', () => {
it('should extract function declarations', () => {
const code = `
package main
func ProcessOrder(order Order) (Receipt, error) {
// Process the order
return Receipt{}, nil
}
`;
const result = extractFromSource('main.go', code);
const funcNode = result.nodes.find((n) => n.kind === 'function');
expect(funcNode).toBeDefined();
expect(funcNode?.name).toBe('ProcessOrder');
});
it('should extract method declarations', () => {
const code = `
package main
type Service struct {
db *Database
}
func (s *Service) GetUser(id string) (*User, error) {
return s.db.FindUser(id)
}
`;
const result = extractFromSource('service.go', code);
const methodNode = result.nodes.find((n) => n.kind === 'method');
expect(methodNode).toBeDefined();
expect(methodNode?.name).toBe('GetUser');
});
});
describe('Rust Extraction', () => {
it('should extract function declarations', () => {
const code = `
pub fn process_data(input: &str) -> Result<Output, Error> {
// Process data
Ok(Output::new())
}
`;
const result = extractFromSource('lib.rs', code);
const funcNode = result.nodes.find((n) => n.kind === 'function');
expect(funcNode).toBeDefined();
expect(funcNode?.name).toBe('process_data');
expect(funcNode?.visibility).toBe('public');
});
it('should extract struct declarations', () => {
const code = `
pub struct User {
pub id: String,
pub name: String,
email: String,
}
`;
const result = extractFromSource('models.rs', code);
const structNode = result.nodes.find((n) => n.kind === 'struct');
expect(structNode).toBeDefined();
expect(structNode?.name).toBe('User');
});
it('should extract trait declarations', () => {
const code = `
pub trait Repository {
fn find(&self, id: &str) -> Option<Entity>;
fn save(&mut self, entity: Entity) -> Result<(), Error>;
}
`;
const result = extractFromSource('traits.rs', code);
const traitNode = result.nodes.find((n) => n.kind === 'trait');
expect(traitNode).toBeDefined();
expect(traitNode?.name).toBe('Repository');
});
});
describe('Java Extraction', () => {
it('should extract class declarations', () => {
const code = `
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
public User getUser(String id) {
return repository.findById(id);
}
}
`;
const result = extractFromSource('UserService.java', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('UserService');
expect(classNode?.visibility).toBe('public');
});
it('should extract method declarations', () => {
const code = `
public class Calculator {
public static int add(int a, int b) {
return a + b;
}
}
`;
const result = extractFromSource('Calculator.java', code);
const methodNode = result.nodes.find((n) => n.kind === 'method' && n.name === 'add');
expect(methodNode).toBeDefined();
expect(methodNode?.isStatic).toBe(true);
});
});
describe('C# Extraction', () => {
it('should extract class declarations', () => {
const code = `
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public async Task<Order> GetOrderAsync(string id)
{
return await _repository.FindByIdAsync(id);
}
}
`;
const result = extractFromSource('OrderService.cs', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('OrderService');
expect(classNode?.visibility).toBe('public');
});
});
describe('PHP Extraction', () => {
it('should extract class declarations', () => {
const code = `<?php
class UserController
{
private UserService $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function show(string $id): User
{
return $this->userService->find($id);
}
}
`;
const result = extractFromSource('UserController.php', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('UserController');
});
});
describe('Swift Extraction', () => {
it('should extract class declarations', () => {
const code = `
public class NetworkManager {
private let session: URLSession
public init(session: URLSession = .shared) {
self.session = session
}
public func fetchData(from url: URL) async throws -> Data {
let (data, _) = try await session.data(from: url)
return data
}
}
`;
const result = extractFromSource('NetworkManager.swift', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('NetworkManager');
});
it('should extract function declarations', () => {
const code = `
func calculateSum(_ numbers: [Int]) -> Int {
return numbers.reduce(0, +)
}
public func formatCurrency(amount: Double) -> String {
return String(format: "$%.2f", amount)
}
`;
const result = extractFromSource('utils.swift', code);
const functions = result.nodes.filter((n) => n.kind === 'function');
expect(functions.length).toBeGreaterThanOrEqual(1);
});
it('should extract struct declarations', () => {
const code = `
public struct User {
let id: UUID
var name: String
var email: String
func displayName() -> String {
return name
}
}
`;
const result = extractFromSource('User.swift', code);
const structNode = result.nodes.find((n) => n.kind === 'struct');
expect(structNode).toBeDefined();
expect(structNode?.name).toBe('User');
});
it('should extract protocol declarations', () => {
const code = `
public protocol Repository {
associatedtype Entity
func find(id: String) async throws -> Entity?
func save(_ entity: Entity) async throws
}
`;
const result = extractFromSource('Repository.swift', code);
const protocolNode = result.nodes.find((n) => n.kind === 'interface');
expect(protocolNode).toBeDefined();
expect(protocolNode?.name).toBe('Repository');
});
});
describe('Kotlin Extraction', () => {
it('should extract class declarations', () => {
const code = `
class UserRepository(private val database: Database) {
fun findById(id: String): User? {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
suspend fun save(user: User) {
database.insert(user)
}
}
`;
const result = extractFromSource('UserRepository.kt', code);
const classNode = result.nodes.find((n) => n.kind === 'class');
expect(classNode).toBeDefined();
expect(classNode?.name).toBe('UserRepository');
});
it('should extract function declarations', () => {
const code = `
fun calculateTotal(items: List<Item>): Double {
return items.sumOf { it.price }
}
suspend fun fetchUserData(userId: String): User {
return api.getUser(userId)
}
`;
const result = extractFromSource('utils.kt', code);
const functions = result.nodes.filter((n) => n.kind === 'function');
expect(functions.length).toBeGreaterThanOrEqual(1);
});
it('should detect suspend functions as async', () => {
const code = `
suspend fun loadData(): List<String> {
delay(1000)
return listOf("a", "b", "c")
}
`;
const result = extractFromSource('loader.kt', code);
const funcNode = result.nodes.find((n) => n.kind === 'function');
expect(funcNode).toBeDefined();
expect(funcNode?.isAsync).toBe(true);
});
});
describe('Full Indexing', () => {
let tempDir: string;
beforeEach(() => {
tempDir = createTempDir();
});
afterEach(() => {
cleanupTempDir(tempDir);
});
it('should index a TypeScript file', async () => {
// Create test file
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir);
fs.writeFileSync(
path.join(srcDir, 'utils.ts'),
`
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
`
);
// Initialize and index
const cg = CodeGraph.initSync(tempDir);
const result = await cg.indexAll();
expect(result.success).toBe(true);
expect(result.filesIndexed).toBe(1);
expect(result.nodesCreated).toBeGreaterThanOrEqual(2);
// Check nodes were stored
const nodes = cg.getNodesInFile('src/utils.ts');
expect(nodes.length).toBeGreaterThanOrEqual(2);
const addFunc = nodes.find((n) => n.name === 'add');
expect(addFunc).toBeDefined();
expect(addFunc?.kind).toBe('function');
cg.close();
});
it('should index multiple files', async () => {
// Create test files
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir);
fs.writeFileSync(
path.join(srcDir, 'math.ts'),
`export function add(a: number, b: number) { return a + b; }`
);
fs.writeFileSync(
path.join(srcDir, 'string.ts'),
`export function capitalize(s: string) { return s.toUpperCase(); }`
);
// Initialize and index
const cg = CodeGraph.initSync(tempDir);
const result = await cg.indexAll();
expect(result.success).toBe(true);
expect(result.filesIndexed).toBe(2);
const files = cg.getFiles();
expect(files.length).toBe(2);
cg.close();
});
it('should track file hashes for incremental updates', async () => {
// Create initial file
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir);
fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 1;`);
// Initialize and index
const cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
// Check file is tracked
const file = cg.getFile('src/main.ts');
expect(file).toBeDefined();
expect(file?.contentHash).toBeDefined();
// Modify file
fs.writeFileSync(path.join(srcDir, 'main.ts'), `export const x = 2;`);
// Check for changes
const changes = cg.getChangedFiles();
expect(changes.modified).toContain('src/main.ts');
cg.close();
});
it('should sync and detect changes', async () => {
// Create initial file
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir);
fs.writeFileSync(
path.join(srcDir, 'main.ts'),
`export function original() { return 1; }`
);
// Initialize and index
const cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
const initialNodes = cg.getNodesInFile('src/main.ts');
expect(initialNodes.some((n) => n.name === 'original')).toBe(true);
// Modify file
fs.writeFileSync(
path.join(srcDir, 'main.ts'),
`export function updated() { return 2; }`
);
// Sync
const syncResult = await cg.sync();
expect(syncResult.filesModified).toBe(1);
// Check nodes were updated
const updatedNodes = cg.getNodesInFile('src/main.ts');
expect(updatedNodes.some((n) => n.name === 'updated')).toBe(true);
expect(updatedNodes.some((n) => n.name === 'original')).toBe(false);
cg.close();
});
});
+383
View File
@@ -0,0 +1,383 @@
/**
* Foundation Tests
*
* Tests for the CodeGraph foundation layer.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { DEFAULT_CONFIG, Node, Edge } from '../src/types';
import { loadConfig, saveConfig } from '../src/config';
import { isInitialized, getCodeGraphDir, validateDirectory } from '../src/directory';
import { DatabaseConnection, getDatabasePath } from '../src/db';
// Create a temporary directory for each test
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'));
}
// Clean up temporary directory
function cleanupTempDir(dir: string): void {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
describe('CodeGraph Foundation', () => {
let tempDir: string;
beforeEach(() => {
tempDir = createTempDir();
});
afterEach(() => {
cleanupTempDir(tempDir);
});
describe('Initialization', () => {
it('should initialize a new project', () => {
const cg = CodeGraph.initSync(tempDir);
expect(CodeGraph.isInitialized(tempDir)).toBe(true);
expect(fs.existsSync(getCodeGraphDir(tempDir))).toBe(true);
expect(fs.existsSync(getDatabasePath(tempDir))).toBe(true);
cg.close();
});
it('should create .gitignore in .codegraph directory', () => {
const cg = CodeGraph.initSync(tempDir);
const gitignorePath = path.join(getCodeGraphDir(tempDir), '.gitignore');
expect(fs.existsSync(gitignorePath)).toBe(true);
const content = fs.readFileSync(gitignorePath, 'utf-8');
expect(content).toContain('*.db');
cg.close();
});
it('should create config.json with defaults', () => {
const cg = CodeGraph.initSync(tempDir);
const configPath = path.join(getCodeGraphDir(tempDir), 'config.json');
expect(fs.existsSync(configPath)).toBe(true);
const config = cg.getConfig();
expect(config.version).toBe(DEFAULT_CONFIG.version);
expect(config.include).toEqual(DEFAULT_CONFIG.include);
expect(config.exclude).toEqual(DEFAULT_CONFIG.exclude);
cg.close();
});
it('should throw if already initialized', () => {
const cg = CodeGraph.initSync(tempDir);
cg.close();
expect(() => CodeGraph.initSync(tempDir)).toThrow(/already initialized/i);
});
it('should accept custom config options', () => {
const cg = CodeGraph.initSync(tempDir, {
config: {
maxFileSize: 500000,
extractDocstrings: false,
},
});
const config = cg.getConfig();
expect(config.maxFileSize).toBe(500000);
expect(config.extractDocstrings).toBe(false);
cg.close();
});
});
describe('Opening Projects', () => {
it('should open an existing project', () => {
// First initialize
const cg1 = CodeGraph.initSync(tempDir);
cg1.close();
// Then open
const cg2 = CodeGraph.openSync(tempDir);
expect(cg2.getProjectRoot()).toBe(path.resolve(tempDir));
cg2.close();
});
it('should throw if not initialized', () => {
expect(() => CodeGraph.openSync(tempDir)).toThrow(/not initialized/i);
});
it('should preserve configuration across open/close', () => {
const cg1 = CodeGraph.initSync(tempDir, {
config: { maxFileSize: 123456 },
});
cg1.close();
const cg2 = CodeGraph.openSync(tempDir);
expect(cg2.getConfig().maxFileSize).toBe(123456);
cg2.close();
});
});
describe('Static Methods', () => {
it('isInitialized should return false for new directory', () => {
expect(CodeGraph.isInitialized(tempDir)).toBe(false);
});
it('isInitialized should return true after init', () => {
const cg = CodeGraph.initSync(tempDir);
expect(CodeGraph.isInitialized(tempDir)).toBe(true);
cg.close();
});
});
describe('Database', () => {
it('should create database with correct schema', () => {
const cg = CodeGraph.initSync(tempDir);
// Check that we can get stats (requires tables to exist)
const stats = cg.getStats();
expect(stats.nodeCount).toBe(0);
expect(stats.edgeCount).toBe(0);
expect(stats.fileCount).toBe(0);
cg.close();
});
it('should return correct database size', () => {
const cg = CodeGraph.initSync(tempDir);
const stats = cg.getStats();
// Database should have some size (at least the schema)
expect(stats.dbSizeBytes).toBeGreaterThan(0);
cg.close();
});
it('should support optimize operation', () => {
const cg = CodeGraph.initSync(tempDir);
// Should not throw
expect(() => cg.optimize()).not.toThrow();
cg.close();
});
it('should support clear operation', () => {
const cg = CodeGraph.initSync(tempDir);
// Should not throw
expect(() => cg.clear()).not.toThrow();
const stats = cg.getStats();
expect(stats.nodeCount).toBe(0);
cg.close();
});
});
describe('Configuration', () => {
it('should load and merge config with defaults', () => {
const cg = CodeGraph.initSync(tempDir);
cg.close();
const config = loadConfig(tempDir);
expect(config.version).toBe(DEFAULT_CONFIG.version);
expect(config.rootDir).toBe(path.resolve(tempDir));
});
it('should update configuration', () => {
const cg = CodeGraph.initSync(tempDir);
cg.updateConfig({ maxFileSize: 999999 });
expect(cg.getConfig().maxFileSize).toBe(999999);
cg.close();
// Verify persistence
const config = loadConfig(tempDir);
expect(config.maxFileSize).toBe(999999);
});
});
describe('Directory Management', () => {
it('should validate directory structure', () => {
const cg = CodeGraph.initSync(tempDir);
cg.close();
const validation = validateDirectory(tempDir);
expect(validation.valid).toBe(true);
expect(validation.errors).toHaveLength(0);
});
it('should detect invalid directory', () => {
const validation = validateDirectory(tempDir);
expect(validation.valid).toBe(false);
expect(validation.errors.length).toBeGreaterThan(0);
});
});
describe('Uninitialize', () => {
it('should remove .codegraph directory', () => {
const cg = CodeGraph.initSync(tempDir);
cg.uninitialize();
expect(fs.existsSync(getCodeGraphDir(tempDir))).toBe(false);
expect(CodeGraph.isInitialized(tempDir)).toBe(false);
});
});
describe('Close/Destroy', () => {
it('should close database but keep .codegraph directory', () => {
const cg = CodeGraph.initSync(tempDir);
cg.destroy(); // destroy is alias for close
expect(fs.existsSync(getCodeGraphDir(tempDir))).toBe(true);
expect(CodeGraph.isInitialized(tempDir)).toBe(true);
});
});
describe('Graph Query Methods', () => {
it('should throw "Node not found" for non-existent nodes', () => {
const cg = CodeGraph.initSync(tempDir);
// getContext throws for non-existent nodes
expect(() => cg.getContext('non-existent')).toThrow(/not found/i);
cg.close();
});
it('should return empty results for non-existent nodes', () => {
const cg = CodeGraph.initSync(tempDir);
// These methods return empty results instead of throwing
const traverseResult = cg.traverse('non-existent');
expect(traverseResult.nodes.size).toBe(0);
const callGraph = cg.getCallGraph('non-existent');
expect(callGraph.nodes.size).toBe(0);
const typeHierarchy = cg.getTypeHierarchy('non-existent');
expect(typeHierarchy.nodes.size).toBe(0);
const usages = cg.findUsages('non-existent');
expect(usages.length).toBe(0);
cg.close();
});
it('should require embedding initialization for semantic search', async () => {
const cg = CodeGraph.initSync(tempDir);
// Semantic search requires embeddings to be initialized first
await expect(cg.semanticSearch('test')).rejects.toThrow(/not initialized/i);
await expect(cg.findSimilar('test')).rejects.toThrow(/not initialized/i);
// Check embedding status
expect(cg.isEmbeddingsInitialized()).toBe(false);
cg.close();
});
});
});
describe('Database Connection', () => {
let tempDir: string;
beforeEach(() => {
tempDir = createTempDir();
});
afterEach(() => {
cleanupTempDir(tempDir);
});
it('should initialize new database', () => {
const dbPath = path.join(tempDir, 'test.db');
const db = DatabaseConnection.initialize(dbPath);
expect(db.isOpen()).toBe(true);
expect(fs.existsSync(dbPath)).toBe(true);
db.close();
});
it('should get schema version', () => {
const dbPath = path.join(tempDir, 'test.db');
const db = DatabaseConnection.initialize(dbPath);
const version = db.getSchemaVersion();
expect(version).not.toBeNull();
expect(version?.version).toBe(1);
db.close();
});
it('should support transactions', () => {
const dbPath = path.join(tempDir, 'test.db');
const db = DatabaseConnection.initialize(dbPath);
const result = db.transaction(() => {
return 42;
});
expect(result).toBe(42);
db.close();
});
it('should throw when opening non-existent database', () => {
const dbPath = path.join(tempDir, 'nonexistent.db');
expect(() => DatabaseConnection.open(dbPath)).toThrow(/not found/i);
});
});
describe('Query Builder', () => {
let tempDir: string;
let cg: CodeGraph;
beforeEach(() => {
tempDir = createTempDir();
cg = CodeGraph.initSync(tempDir);
});
afterEach(() => {
cg.close();
cleanupTempDir(tempDir);
});
it('should return null for non-existent node', () => {
const node = cg.getNode('nonexistent');
expect(node).toBeNull();
});
it('should return empty array for nodes in non-existent file', () => {
const nodes = cg.getNodesInFile('nonexistent.ts');
expect(nodes).toEqual([]);
});
it('should return empty array for edges from non-existent node', () => {
const edges = cg.getOutgoingEdges('nonexistent');
expect(edges).toEqual([]);
});
it('should return null for non-existent file', () => {
const file = cg.getFile('nonexistent.ts');
expect(file).toBeNull();
});
it('should return empty array for files when none tracked', () => {
const files = cg.getFiles();
expect(files).toEqual([]);
});
});
+435
View File
@@ -0,0 +1,435 @@
/**
* Graph Query Tests
*
* Tests for graph traversal and query functionality.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src/index';
import { Node, Edge } from '../src/types';
describe('Graph Queries', () => {
let testDir: string;
let cg: CodeGraph;
beforeEach(async () => {
// Create temp directory
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-graph-test-'));
// Create test files with relationships
const srcDir = path.join(testDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
// Create base class
fs.writeFileSync(
path.join(srcDir, 'base.ts'),
`
export class BaseClass {
protected value: number;
constructor(value: number) {
this.value = value;
}
getValue(): number {
return this.value;
}
}
export interface Printable {
print(): void;
}
`
);
// Create derived class
fs.writeFileSync(
path.join(srcDir, 'derived.ts'),
`
import { BaseClass, Printable } from './base';
export class DerivedClass extends BaseClass implements Printable {
private name: string;
constructor(value: number, name: string) {
super(value);
this.name = name;
}
print(): void {
console.log(this.getName(), this.getValue());
}
getName(): string {
return this.name;
}
}
`
);
// Create utility functions
fs.writeFileSync(
path.join(srcDir, 'utils.ts'),
`
export function formatValue(value: number): string {
return value.toFixed(2);
}
export function processValue(value: number): number {
const formatted = formatValue(value);
return parseFloat(formatted);
}
export function doubleValue(value: number): number {
return value * 2;
}
// Unused function (dead code)
function unusedHelper(): void {
console.log('never called');
}
`
);
// Create main file that uses everything
fs.writeFileSync(
path.join(srcDir, 'main.ts'),
`
import { DerivedClass } from './derived';
import { processValue, doubleValue } from './utils';
function main(): void {
const obj = new DerivedClass(10, 'test');
obj.print();
const result = processValue(doubleValue(obj.getValue()));
console.log(result);
}
export { main };
`
);
// Initialize and index
cg = CodeGraph.initSync(testDir, {
config: {
include: ['src/**/*.ts'],
exclude: [],
},
});
await cg.indexAll();
cg.resolveReferences();
});
afterEach(() => {
if (cg) {
cg.destroy();
}
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
describe('traverse()', () => {
it('should traverse graph from a starting node', () => {
const nodes = cg.getNodesByKind('function');
const mainFunc = nodes.find((n) => n.name === 'main');
if (!mainFunc) {
console.log('main function not found, skipping test');
return;
}
const subgraph = cg.traverse(mainFunc.id, {
maxDepth: 2,
direction: 'outgoing',
});
expect(subgraph.nodes.size).toBeGreaterThan(0);
expect(subgraph.roots).toContain(mainFunc.id);
});
it('should respect maxDepth option', () => {
const nodes = cg.getNodesByKind('function');
const mainFunc = nodes.find((n) => n.name === 'main');
if (!mainFunc) {
return;
}
const shallow = cg.traverse(mainFunc.id, { maxDepth: 1 });
const deep = cg.traverse(mainFunc.id, { maxDepth: 3 });
expect(deep.nodes.size).toBeGreaterThanOrEqual(shallow.nodes.size);
});
it('should support incoming direction', () => {
const nodes = cg.getNodesByKind('function');
const formatValue = nodes.find((n) => n.name === 'formatValue');
if (!formatValue) {
return;
}
const subgraph = cg.traverse(formatValue.id, {
maxDepth: 2,
direction: 'incoming',
});
expect(subgraph.nodes.size).toBeGreaterThan(0);
});
});
describe('getContext()', () => {
it('should return context for a node', () => {
const nodes = cg.getNodesByKind('class');
const derivedClass = nodes.find((n) => n.name === 'DerivedClass');
if (!derivedClass) {
console.log('DerivedClass not found, skipping test');
return;
}
const context = cg.getContext(derivedClass.id);
expect(context.focal).toBeDefined();
expect(context.focal.id).toBe(derivedClass.id);
expect(context.ancestors).toBeDefined();
expect(context.children).toBeDefined();
expect(context.incomingRefs).toBeDefined();
expect(context.outgoingRefs).toBeDefined();
});
it('should throw for non-existent node', () => {
expect(() => cg.getContext('non-existent-id')).toThrow('Node not found');
});
});
describe('getCallGraph()', () => {
it('should return call graph for a function', () => {
const nodes = cg.getNodesByKind('function');
const processValue = nodes.find((n) => n.name === 'processValue');
if (!processValue) {
console.log('processValue not found, skipping test');
return;
}
const callGraph = cg.getCallGraph(processValue.id, 2);
expect(callGraph.nodes.size).toBeGreaterThan(0);
expect(callGraph.nodes.has(processValue.id)).toBe(true);
});
});
describe('getTypeHierarchy()', () => {
it('should return type hierarchy for a class', () => {
const nodes = cg.getNodesByKind('class');
const derivedClass = nodes.find((n) => n.name === 'DerivedClass');
if (!derivedClass) {
return;
}
const hierarchy = cg.getTypeHierarchy(derivedClass.id);
expect(hierarchy.nodes.size).toBeGreaterThan(0);
expect(hierarchy.nodes.has(derivedClass.id)).toBe(true);
});
it('should return empty subgraph for non-existent node', () => {
const hierarchy = cg.getTypeHierarchy('non-existent-id');
expect(hierarchy.nodes.size).toBe(0);
expect(hierarchy.edges.length).toBe(0);
});
});
describe('findUsages()', () => {
it('should find usages of a symbol', () => {
const nodes = cg.getNodesByKind('class');
const baseClass = nodes.find((n) => n.name === 'BaseClass');
if (!baseClass) {
return;
}
const usages = cg.findUsages(baseClass.id);
// Should find at least the extends relationship
expect(usages).toBeDefined();
expect(Array.isArray(usages)).toBe(true);
});
});
describe('getCallers() and getCallees()', () => {
it('should get callers of a function', () => {
const nodes = cg.getNodesByKind('function');
const formatValue = nodes.find((n) => n.name === 'formatValue');
if (!formatValue) {
return;
}
const callers = cg.getCallers(formatValue.id);
// processValue calls formatValue
expect(Array.isArray(callers)).toBe(true);
});
it('should get callees of a function', () => {
const nodes = cg.getNodesByKind('function');
const processValue = nodes.find((n) => n.name === 'processValue');
if (!processValue) {
return;
}
const callees = cg.getCallees(processValue.id);
expect(Array.isArray(callees)).toBe(true);
});
});
describe('getImpactRadius()', () => {
it('should calculate impact radius', () => {
const nodes = cg.getNodesByKind('function');
const formatValue = nodes.find((n) => n.name === 'formatValue');
if (!formatValue) {
return;
}
const impact = cg.getImpactRadius(formatValue.id, 3);
expect(impact.nodes.size).toBeGreaterThan(0);
expect(impact.nodes.has(formatValue.id)).toBe(true);
});
});
describe('findPath()', () => {
it('should find path between connected nodes', () => {
const stats = cg.getStats();
if (stats.nodeCount < 2) {
return;
}
const functions = cg.getNodesByKind('function');
if (functions.length < 2) {
return;
}
// Try to find any path
const processValue = functions.find((n) => n.name === 'processValue');
const formatValue = functions.find((n) => n.name === 'formatValue');
if (processValue && formatValue) {
const path = cg.findPath(processValue.id, formatValue.id);
// Path might exist or might not depending on edge direction
expect(path === null || Array.isArray(path)).toBe(true);
}
});
it('should return null for disconnected nodes', () => {
// Create two nodes that definitely don't have a path
const path = cg.findPath('non-existent-1', 'non-existent-2');
expect(path).toBeNull();
});
});
describe('getAncestors() and getChildren()', () => {
it('should get ancestors of a node', () => {
const methods = cg.getNodesByKind('method');
const printMethod = methods.find((n) => n.name === 'print');
if (!printMethod) {
return;
}
const ancestors = cg.getAncestors(printMethod.id);
// Should have class and file as ancestors
expect(Array.isArray(ancestors)).toBe(true);
});
it('should get children of a node', () => {
const classes = cg.getNodesByKind('class');
const derivedClass = classes.find((n) => n.name === 'DerivedClass');
if (!derivedClass) {
return;
}
const children = cg.getChildren(derivedClass.id);
// Should have methods as children
expect(Array.isArray(children)).toBe(true);
});
});
describe('File dependency analysis', () => {
it('should get file dependencies', () => {
const deps = cg.getFileDependencies('src/main.ts');
expect(Array.isArray(deps)).toBe(true);
});
it('should get file dependents', () => {
const dependents = cg.getFileDependents('src/utils.ts');
expect(Array.isArray(dependents)).toBe(true);
});
});
describe('findCircularDependencies()', () => {
it('should detect circular dependencies', () => {
const cycles = cg.findCircularDependencies();
// Our test files don't have circular deps
expect(Array.isArray(cycles)).toBe(true);
});
});
describe('findDeadCode()', () => {
it('should find dead code', () => {
const deadCode = cg.findDeadCode(['function']);
expect(Array.isArray(deadCode)).toBe(true);
// unusedHelper should be detected
const hasUnused = deadCode.some((n) => n.name === 'unusedHelper');
// Note: This depends on extraction properly detecting function scope
expect(deadCode.length).toBeGreaterThanOrEqual(0);
});
});
describe('getNodeMetrics()', () => {
it('should return metrics for a node', () => {
const functions = cg.getNodesByKind('function');
const func = functions[0];
if (!func) {
return;
}
const metrics = cg.getNodeMetrics(func.id);
expect(metrics).toHaveProperty('incomingEdgeCount');
expect(metrics).toHaveProperty('outgoingEdgeCount');
expect(metrics).toHaveProperty('callCount');
expect(metrics).toHaveProperty('callerCount');
expect(metrics).toHaveProperty('childCount');
expect(metrics).toHaveProperty('depth');
expect(typeof metrics.incomingEdgeCount).toBe('number');
expect(typeof metrics.outgoingEdgeCount).toBe('number');
});
});
});
+487
View File
@@ -0,0 +1,487 @@
/**
* Resolution Module Tests
*
* Tests for Phase 3: Reference Resolution
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { Node, UnresolvedReference } from '../src/types';
import { ReferenceResolver, createResolver, ResolutionContext } from '../src/resolution';
import { matchReference } from '../src/resolution/name-matcher';
import { resolveImportPath, extractImportMappings } from '../src/resolution/import-resolver';
import { detectFrameworks, getAllFrameworkResolvers } from '../src/resolution/frameworks';
import { QueryBuilder } from '../src/db/queries';
import { DatabaseConnection } from '../src/db';
describe('Resolution Module', () => {
let tempDir: string;
let cg: CodeGraph;
beforeEach(() => {
// Create temp directory
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-resolution-test-'));
});
afterEach(() => {
// Clean up
if (cg) {
cg.destroy();
} else if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true });
}
});
describe('Name Matcher', () => {
it('should match exact name references', () => {
// Create a mock context
const mockNodes: Node[] = [
{
id: 'func:test.ts:myFunction:10',
kind: 'function',
name: 'myFunction',
qualifiedName: 'test.ts::myFunction',
filePath: 'test.ts',
language: 'typescript',
startLine: 10,
endLine: 20,
startColumn: 0,
endColumn: 0,
updatedAt: Date.now(),
},
];
const context: ResolutionContext = {
getNodesInFile: () => mockNodes,
getNodesByName: (name) => mockNodes.filter((n) => n.name === name),
getNodesByQualifiedName: () => [],
getNodesByKind: () => [],
fileExists: () => true,
readFile: () => null,
getProjectRoot: () => '/test',
getAllFiles: () => ['test.ts'],
};
const ref = {
fromNodeId: 'caller:main.ts:caller:5',
referenceName: 'myFunction',
referenceKind: 'calls' as const,
line: 5,
column: 10,
filePath: 'main.ts',
language: 'typescript' as const,
};
const result = matchReference(ref, context);
expect(result).not.toBeNull();
expect(result?.targetNodeId).toBe('func:test.ts:myFunction:10');
expect(result?.resolvedBy).toBe('exact-match');
});
it('should match qualified name references', () => {
const mockClassNode: Node = {
id: 'class:user.ts:User:5',
kind: 'class',
name: 'User',
qualifiedName: 'user.ts::User',
filePath: 'user.ts',
language: 'typescript',
startLine: 5,
endLine: 30,
startColumn: 0,
endColumn: 0,
updatedAt: Date.now(),
};
const mockMethodNode: Node = {
id: 'method:user.ts:User.save:15',
kind: 'method',
name: 'save',
qualifiedName: 'user.ts::User::save',
filePath: 'user.ts',
language: 'typescript',
startLine: 15,
endLine: 25,
startColumn: 0,
endColumn: 0,
updatedAt: Date.now(),
};
const context: ResolutionContext = {
getNodesInFile: (fp) => fp === 'user.ts' ? [mockClassNode, mockMethodNode] : [],
getNodesByName: (name) => {
if (name === 'User') return [mockClassNode];
if (name === 'save') return [mockMethodNode];
return [];
},
getNodesByQualifiedName: (qn) => {
if (qn === 'user.ts::User::save') return [mockMethodNode];
return [];
},
getNodesByKind: () => [],
fileExists: () => true,
readFile: () => null,
getProjectRoot: () => '/test',
getAllFiles: () => ['user.ts'],
};
const ref = {
fromNodeId: 'caller:main.ts:main:5',
referenceName: 'User.save',
referenceKind: 'calls' as const,
line: 5,
column: 10,
filePath: 'main.ts',
language: 'typescript' as const,
};
const result = matchReference(ref, context);
expect(result).not.toBeNull();
expect(result?.targetNodeId).toBe('method:user.ts:User.save:15');
});
});
describe('Import Resolver', () => {
it('should resolve relative import paths', () => {
const context: ResolutionContext = {
getNodesInFile: () => [],
getNodesByName: () => [],
getNodesByQualifiedName: () => [],
getNodesByKind: () => [],
fileExists: (p) => p === 'src/components/utils.ts' || p === 'src/components/utils/index.ts',
readFile: () => null,
getProjectRoot: () => '',
getAllFiles: () => ['src/components/utils.ts', 'src/components/utils/index.ts'],
};
const result = resolveImportPath(
'./utils',
'src/components/Button.ts',
'typescript',
context
);
expect(result).toBe('src/components/utils.ts');
});
it('should resolve parent directory imports', () => {
const context: ResolutionContext = {
getNodesInFile: () => [],
getNodesByName: () => [],
getNodesByQualifiedName: () => [],
getNodesByKind: () => [],
fileExists: (p) => p === 'src/helpers.ts' || p === 'src/helpers/index.ts',
readFile: () => null,
getProjectRoot: () => '',
getAllFiles: () => ['src/helpers.ts', 'src/helpers/index.ts'],
};
const result = resolveImportPath(
'../helpers',
'src/components/Button.ts',
'typescript',
context
);
expect(result).toBe('src/helpers.ts');
});
it('should extract JS/TS import mappings', () => {
const content = `
import { foo } from './foo';
import bar from '../bar';
import * as utils from './utils';
import { baz, qux } from './baz';
`;
const mappings = extractImportMappings(
'src/index.ts',
content,
'typescript'
);
expect(mappings.length).toBeGreaterThan(0);
expect(mappings.some((m) => m.localName === 'foo')).toBe(true);
expect(mappings.some((m) => m.localName === 'bar')).toBe(true);
});
it('should extract Python import mappings', () => {
const content = `
from utils import helper
from .models import User
import os
from ..services import auth_service
`;
const mappings = extractImportMappings(
'src/main.py',
content,
'python'
);
expect(mappings.length).toBeGreaterThan(0);
expect(mappings.some((m) => m.localName === 'helper')).toBe(true);
expect(mappings.some((m) => m.localName === 'User')).toBe(true);
});
});
describe('Framework Detection', () => {
it('should detect React framework', () => {
const context: ResolutionContext = {
getNodesInFile: () => [],
getNodesByName: () => [],
getNodesByQualifiedName: () => [],
getNodesByKind: () => [],
fileExists: () => false,
readFile: (p) => {
if (p === 'package.json') {
return JSON.stringify({
dependencies: { react: '^18.0.0' },
});
}
return null;
},
getProjectRoot: () => '/test',
getAllFiles: () => ['package.json', 'src/App.tsx'],
};
const frameworks = detectFrameworks(context);
expect(frameworks.some((f) => f.name === 'react')).toBe(true);
});
it('should detect Express framework', () => {
const context: ResolutionContext = {
getNodesInFile: () => [],
getNodesByName: () => [],
getNodesByQualifiedName: () => [],
getNodesByKind: () => [],
fileExists: () => false,
readFile: (p) => {
if (p === 'package.json') {
return JSON.stringify({
dependencies: { express: '^4.18.0' },
});
}
return null;
},
getProjectRoot: () => '/test',
getAllFiles: () => ['package.json', 'src/app.js'],
};
const frameworks = detectFrameworks(context);
expect(frameworks.some((f) => f.name === 'express')).toBe(true);
});
it('should detect Laravel framework', () => {
const context: ResolutionContext = {
getNodesInFile: () => [],
getNodesByName: () => [],
getNodesByQualifiedName: () => [],
getNodesByKind: () => [],
fileExists: (p) => p === 'artisan',
readFile: () => null,
getProjectRoot: () => '/test',
getAllFiles: () => ['artisan', 'app/Http/Kernel.php'],
};
const frameworks = detectFrameworks(context);
expect(frameworks.some((f) => f.name === 'laravel')).toBe(true);
});
it('should return all framework resolvers', () => {
const resolvers = getAllFrameworkResolvers();
expect(resolvers.length).toBeGreaterThan(0);
expect(resolvers.some((r) => r.name === 'react')).toBe(true);
expect(resolvers.some((r) => r.name === 'express')).toBe(true);
expect(resolvers.some((r) => r.name === 'laravel')).toBe(true);
});
});
describe('React Framework Resolver', () => {
it('should resolve React component references', () => {
const mockNodes: Node[] = [
{
id: 'component:src/Button.tsx:Button:5',
kind: 'component',
name: 'Button',
qualifiedName: 'src/Button.tsx::Button',
filePath: 'src/Button.tsx',
language: 'tsx',
startLine: 5,
endLine: 20,
startColumn: 0,
endColumn: 0,
updatedAt: Date.now(),
},
];
const context: ResolutionContext = {
getNodesInFile: (fp) => (fp === 'src/Button.tsx' ? mockNodes : []),
getNodesByName: () => mockNodes,
getNodesByQualifiedName: () => [],
getNodesByKind: () => [],
fileExists: () => false,
readFile: (p) => {
if (p === 'package.json') {
return JSON.stringify({ dependencies: { react: '^18.0.0' } });
}
return null;
},
getProjectRoot: () => '/test',
getAllFiles: () => ['package.json', 'src/Button.tsx', 'src/App.tsx'],
};
const frameworks = detectFrameworks(context);
const reactResolver = frameworks.find((f) => f.name === 'react');
expect(reactResolver).toBeDefined();
const ref = {
fromNodeId: 'component:src/App.tsx:App:1',
referenceName: 'Button',
referenceKind: 'renders' as const,
line: 10,
column: 5,
filePath: 'src/App.tsx',
language: 'typescript' as const,
};
const result = reactResolver!.resolve(ref, context);
expect(result).not.toBeNull();
expect(result?.targetNodeId).toBe('component:src/Button.tsx:Button:5');
});
it('should resolve custom hook references', () => {
const mockNodes: Node[] = [
{
id: 'hook:src/hooks/useAuth.ts:useAuth:1',
kind: 'function',
name: 'useAuth',
qualifiedName: 'src/hooks/useAuth.ts::useAuth',
filePath: 'src/hooks/useAuth.ts',
language: 'typescript',
startLine: 1,
endLine: 20,
startColumn: 0,
endColumn: 0,
updatedAt: Date.now(),
},
];
const context: ResolutionContext = {
getNodesInFile: (fp) => (fp.includes('useAuth') ? mockNodes : []),
getNodesByName: () => mockNodes,
getNodesByQualifiedName: () => [],
getNodesByKind: () => [],
fileExists: () => false,
readFile: (p) => {
if (p === 'package.json') {
return JSON.stringify({ dependencies: { react: '^18.0.0' } });
}
return null;
},
getProjectRoot: () => '/test',
getAllFiles: () => ['package.json', 'src/hooks/useAuth.ts'],
};
const frameworks = detectFrameworks(context);
const reactResolver = frameworks.find((f) => f.name === 'react');
const ref = {
fromNodeId: 'component:src/App.tsx:App:1',
referenceName: 'useAuth',
referenceKind: 'calls' as const,
line: 5,
column: 10,
filePath: 'src/App.tsx',
language: 'typescript' as const,
};
const result = reactResolver!.resolve(ref, context);
expect(result).not.toBeNull();
expect(result?.targetNodeId).toBe('hook:src/hooks/useAuth.ts:useAuth:1');
});
});
describe('Integration Tests', () => {
it('should create resolver from CodeGraph instance', async () => {
// Create a simple TypeScript project
fs.writeFileSync(
path.join(tempDir, 'package.json'),
JSON.stringify({ name: 'test', dependencies: { react: '^18.0.0' } })
);
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir);
// Create utility file
fs.writeFileSync(
path.join(srcDir, 'utils.ts'),
`export function formatDate(date: Date): string {
return date.toISOString();
}
export function parseDate(str: string): Date {
return new Date(str);
}`
);
// Create main file that uses utils
fs.writeFileSync(
path.join(srcDir, 'main.ts'),
`import { formatDate, parseDate } from './utils';
function processDate(input: string): string {
const date = parseDate(input);
return formatDate(date);
}`
);
// Initialize and index
cg = await CodeGraph.init(tempDir, { index: true });
// Check that resolver detected React framework
const frameworks = cg.getDetectedFrameworks();
expect(frameworks).toContain('react');
// Get stats to verify indexing worked
const stats = cg.getStats();
expect(stats.fileCount).toBe(2);
expect(stats.nodeCount).toBeGreaterThan(0);
});
it('should resolve references after indexing', async () => {
// Create a project with references
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(
path.join(srcDir, 'helper.ts'),
`export function helperFunction(): void {
console.log('helper');
}`
);
fs.writeFileSync(
path.join(srcDir, 'main.ts'),
`import { helperFunction } from './helper';
function main(): void {
helperFunction();
}`
);
cg = await CodeGraph.init(tempDir, { index: true });
// Run reference resolution
const result = cg.resolveReferences();
// Should have attempted resolution
expect(result.stats.total).toBeGreaterThanOrEqual(0);
});
});
});
+393
View File
@@ -0,0 +1,393 @@
/**
* Sync Module Tests
*
* Tests for git hooks installation and sync functionality.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src/index';
describe('Sync Module', () => {
describe('Git Hooks', () => {
let testDir: string;
let cg: CodeGraph;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-test-'));
// Create a sample source file
const srcDir = path.join(testDir, 'src');
fs.mkdirSync(srcDir);
fs.writeFileSync(
path.join(srcDir, 'index.ts'),
`export function hello() { return 'world'; }`
);
// Initialize CodeGraph
cg = CodeGraph.initSync(testDir, {
config: {
include: ['**/*.ts'],
exclude: [],
},
});
});
afterEach(() => {
if (cg) {
cg.destroy();
}
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
describe('isGitRepository()', () => {
it('should return false for non-git directory', () => {
expect(cg.isGitRepository()).toBe(false);
});
it('should return true for git directory', () => {
// Initialize git
fs.mkdirSync(path.join(testDir, '.git'));
expect(cg.isGitRepository()).toBe(true);
});
});
describe('isGitHookInstalled()', () => {
it('should return false when no hook is installed', () => {
// Initialize git
fs.mkdirSync(path.join(testDir, '.git'));
expect(cg.isGitHookInstalled()).toBe(false);
});
it('should return false for non-codegraph hook', () => {
// Initialize git with a custom hook
const hooksDir = path.join(testDir, '.git', 'hooks');
fs.mkdirSync(path.join(testDir, '.git'));
fs.mkdirSync(hooksDir);
fs.writeFileSync(
path.join(hooksDir, 'post-commit'),
'#!/bin/sh\necho "custom hook"'
);
expect(cg.isGitHookInstalled()).toBe(false);
});
it('should return true when codegraph hook is installed', () => {
// Initialize git
fs.mkdirSync(path.join(testDir, '.git'));
// Install hook
cg.installGitHooks();
expect(cg.isGitHookInstalled()).toBe(true);
});
});
describe('installGitHooks()', () => {
it('should fail if not a git repository', () => {
const result = cg.installGitHooks();
expect(result.success).toBe(false);
expect(result.message).toContain('Not a git repository');
});
it('should install hook in git repository', () => {
// Initialize git
fs.mkdirSync(path.join(testDir, '.git'));
const result = cg.installGitHooks();
expect(result.success).toBe(true);
expect(result.message).toContain('installed');
// Verify hook file exists
const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
expect(fs.existsSync(hookPath)).toBe(true);
// Verify hook content contains marker
const content = fs.readFileSync(hookPath, 'utf-8');
expect(content).toContain('CodeGraph auto-sync hook');
expect(content).toContain('codegraph sync');
});
it('should create hooks directory if missing', () => {
// Initialize git without hooks directory
fs.mkdirSync(path.join(testDir, '.git'));
const result = cg.installGitHooks();
expect(result.success).toBe(true);
expect(fs.existsSync(path.join(testDir, '.git', 'hooks'))).toBe(true);
});
it('should backup existing non-codegraph hook', () => {
// Initialize git with a custom hook
const hooksDir = path.join(testDir, '.git', 'hooks');
fs.mkdirSync(path.join(testDir, '.git'));
fs.mkdirSync(hooksDir);
const customHookContent = '#!/bin/sh\necho "custom hook"';
fs.writeFileSync(
path.join(hooksDir, 'post-commit'),
customHookContent
);
const result = cg.installGitHooks();
expect(result.success).toBe(true);
expect(result.previousHookBackedUp).toBe(true);
// Verify backup exists
const backupPath = path.join(hooksDir, 'post-commit.codegraph-backup');
expect(fs.existsSync(backupPath)).toBe(true);
expect(fs.readFileSync(backupPath, 'utf-8')).toBe(customHookContent);
});
it('should update existing codegraph hook without backup', () => {
// Initialize git
fs.mkdirSync(path.join(testDir, '.git'));
// Install hook first time
cg.installGitHooks();
// Install again (update)
const result = cg.installGitHooks();
expect(result.success).toBe(true);
expect(result.message).toContain('updated');
expect(result.previousHookBackedUp).toBeUndefined();
});
it('should make hook executable', () => {
// Initialize git
fs.mkdirSync(path.join(testDir, '.git'));
cg.installGitHooks();
const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
const stats = fs.statSync(hookPath);
// Check executable bit (at least for owner)
expect(stats.mode & 0o100).toBeTruthy();
});
});
describe('removeGitHooks()', () => {
it('should succeed if no hook exists', () => {
// Initialize git
fs.mkdirSync(path.join(testDir, '.git'));
const result = cg.removeGitHooks();
expect(result.success).toBe(true);
expect(result.message).toContain('No post-commit hook found');
});
it('should not remove non-codegraph hook', () => {
// Initialize git with a custom hook
const hooksDir = path.join(testDir, '.git', 'hooks');
fs.mkdirSync(path.join(testDir, '.git'));
fs.mkdirSync(hooksDir);
fs.writeFileSync(
path.join(hooksDir, 'post-commit'),
'#!/bin/sh\necho "custom hook"'
);
const result = cg.removeGitHooks();
expect(result.success).toBe(false);
expect(result.message).toContain('not installed by CodeGraph');
// Verify hook still exists
expect(fs.existsSync(path.join(hooksDir, 'post-commit'))).toBe(true);
});
it('should remove codegraph hook', () => {
// Initialize git
fs.mkdirSync(path.join(testDir, '.git'));
// Install then remove
cg.installGitHooks();
const result = cg.removeGitHooks();
expect(result.success).toBe(true);
expect(result.message).toContain('removed');
// Verify hook is gone
const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
expect(fs.existsSync(hookPath)).toBe(false);
});
it('should restore backup when removing', () => {
// Initialize git with a custom hook
const hooksDir = path.join(testDir, '.git', 'hooks');
fs.mkdirSync(path.join(testDir, '.git'));
fs.mkdirSync(hooksDir);
const customHookContent = '#!/bin/sh\necho "custom hook"';
fs.writeFileSync(
path.join(hooksDir, 'post-commit'),
customHookContent
);
// Install (backs up custom hook) then remove
cg.installGitHooks();
const result = cg.removeGitHooks();
expect(result.success).toBe(true);
expect(result.restoredFromBackup).toBe(true);
// Verify original hook is restored
const hookPath = path.join(hooksDir, 'post-commit');
expect(fs.existsSync(hookPath)).toBe(true);
expect(fs.readFileSync(hookPath, 'utf-8')).toBe(customHookContent);
// Verify backup is gone
const backupPath = path.join(hooksDir, 'post-commit.codegraph-backup');
expect(fs.existsSync(backupPath)).toBe(false);
});
});
});
describe('Sync Functionality', () => {
let testDir: string;
let cg: CodeGraph;
beforeEach(async () => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-func-'));
// Create initial source files
const srcDir = path.join(testDir, 'src');
fs.mkdirSync(srcDir);
fs.writeFileSync(
path.join(srcDir, 'index.ts'),
`export function hello() { return 'world'; }`
);
// Initialize and index
cg = CodeGraph.initSync(testDir, {
config: {
include: ['**/*.ts'],
exclude: [],
},
});
await cg.indexAll();
});
afterEach(() => {
if (cg) {
cg.destroy();
}
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
describe('getChangedFiles()', () => {
it('should detect added files', () => {
// Add a new file
fs.writeFileSync(
path.join(testDir, 'src', 'new.ts'),
`export function newFunc() { return 42; }`
);
const changes = cg.getChangedFiles();
expect(changes.added).toContain('src/new.ts');
expect(changes.modified).toHaveLength(0);
expect(changes.removed).toHaveLength(0);
});
it('should detect modified files', () => {
// Modify existing file
fs.writeFileSync(
path.join(testDir, 'src', 'index.ts'),
`export function hello() { return 'modified'; }`
);
const changes = cg.getChangedFiles();
expect(changes.added).toHaveLength(0);
expect(changes.modified).toContain('src/index.ts');
expect(changes.removed).toHaveLength(0);
});
it('should detect removed files', () => {
// Remove file
fs.unlinkSync(path.join(testDir, 'src', 'index.ts'));
const changes = cg.getChangedFiles();
expect(changes.added).toHaveLength(0);
expect(changes.modified).toHaveLength(0);
expect(changes.removed).toContain('src/index.ts');
});
});
describe('sync()', () => {
it('should reindex added files', async () => {
// Add a new file
fs.writeFileSync(
path.join(testDir, 'src', 'new.ts'),
`export function newFunc() { return 42; }`
);
const result = await cg.sync();
expect(result.filesAdded).toBe(1);
expect(result.filesModified).toBe(0);
expect(result.filesRemoved).toBe(0);
// Verify new function is in the graph
const nodes = cg.searchNodes('newFunc');
expect(nodes.length).toBeGreaterThan(0);
});
it('should reindex modified files', async () => {
// Modify existing file
fs.writeFileSync(
path.join(testDir, 'src', 'index.ts'),
`export function goodbye() { return 'farewell'; }`
);
const result = await cg.sync();
expect(result.filesModified).toBe(1);
// Verify new function is in the graph
const nodes = cg.searchNodes('goodbye');
expect(nodes.length).toBeGreaterThan(0);
// Verify old function is gone
const oldNodes = cg.searchNodes('hello');
expect(oldNodes.length).toBe(0);
});
it('should remove nodes from deleted files', async () => {
// Remove file
fs.unlinkSync(path.join(testDir, 'src', 'index.ts'));
const result = await cg.sync();
expect(result.filesRemoved).toBe(1);
// Verify function is gone
const nodes = cg.searchNodes('hello');
expect(nodes.length).toBe(0);
});
it('should report no changes when nothing changed', async () => {
const result = await cg.sync();
expect(result.filesAdded).toBe(0);
expect(result.filesModified).toBe(0);
expect(result.filesRemoved).toBe(0);
expect(result.filesChecked).toBeGreaterThan(0);
});
});
});
});
+302
View File
@@ -0,0 +1,302 @@
/**
* Vector Embedding Tests
*
* Tests for vector embedding and semantic search functionality.
* Note: Full embedding tests require the model to be downloaded,
* which can take time on first run.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src/index';
import { TextEmbedder } from '../src/vectors/embedder';
import { VectorSearchManager, createVectorSearch } from '../src/vectors/search';
import { DatabaseConnection } from '../src/db';
describe('Vector Embeddings', () => {
describe('TextEmbedder', () => {
describe('createNodeText', () => {
it('should create text representation from node', () => {
const node = {
name: 'processPayment',
kind: 'function',
qualifiedName: 'PaymentService.processPayment',
signature: '(amount: number) => Promise<Receipt>',
docstring: 'Process a payment and return a receipt.',
filePath: 'src/services/payment.ts',
};
const text = TextEmbedder.createNodeText(node);
expect(text).toContain('function: processPayment');
expect(text).toContain('path: PaymentService.processPayment');
expect(text).toContain('file: src/services/payment.ts');
expect(text).toContain('signature: (amount: number) => Promise<Receipt>');
expect(text).toContain('documentation: Process a payment');
});
it('should handle minimal node data', () => {
const node = {
name: 'helper',
kind: 'function',
filePath: 'src/utils.ts',
};
const text = TextEmbedder.createNodeText(node);
expect(text).toContain('function: helper');
expect(text).toContain('file: src/utils.ts');
expect(text).not.toContain('signature:');
expect(text).not.toContain('documentation:');
});
});
describe('cosineSimilarity', () => {
it('should compute similarity between identical vectors', () => {
const vec = new Float32Array([0.1, 0.2, 0.3, 0.4, 0.5]);
const similarity = TextEmbedder.cosineSimilarity(vec, vec);
expect(similarity).toBeCloseTo(1.0, 5);
});
it('should compute similarity between orthogonal vectors', () => {
const vec1 = new Float32Array([1, 0, 0]);
const vec2 = new Float32Array([0, 1, 0]);
const similarity = TextEmbedder.cosineSimilarity(vec1, vec2);
expect(similarity).toBeCloseTo(0.0, 5);
});
it('should compute similarity between opposite vectors', () => {
const vec1 = new Float32Array([1, 0, 0]);
const vec2 = new Float32Array([-1, 0, 0]);
const similarity = TextEmbedder.cosineSimilarity(vec1, vec2);
expect(similarity).toBeCloseTo(-1.0, 5);
});
it('should throw for vectors of different dimensions', () => {
const vec1 = new Float32Array([1, 2, 3]);
const vec2 = new Float32Array([1, 2]);
expect(() => TextEmbedder.cosineSimilarity(vec1, vec2)).toThrow(
'Embeddings must have the same dimension'
);
});
it('should handle zero vectors', () => {
const vec1 = new Float32Array([0, 0, 0]);
const vec2 = new Float32Array([1, 2, 3]);
const similarity = TextEmbedder.cosineSimilarity(vec1, vec2);
expect(similarity).toBe(0);
});
});
});
describe('VectorSearchManager', () => {
let tempDir: string;
let db: DatabaseConnection;
let searchManager: VectorSearchManager;
const TEST_DIMENSION = 3; // Use small dimension for tests
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-vector-test-'));
const dbPath = path.join(tempDir, 'test.db');
db = DatabaseConnection.initialize(dbPath);
searchManager = createVectorSearch(db.getDb(), TEST_DIMENSION);
});
afterEach(() => {
db.close();
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it('should store and retrieve vectors', async () => {
await searchManager.initialize();
const embedding = new Float32Array([0.1, 0.2, 0.3]);
searchManager.storeVector('node1', embedding, 'test-model');
const retrieved = searchManager.getVector('node1');
expect(retrieved).not.toBeNull();
expect(retrieved?.length).toBe(3);
expect(retrieved?.[0]).toBeCloseTo(0.1, 5);
});
it('should return null for non-existent vectors', async () => {
await searchManager.initialize();
const retrieved = searchManager.getVector('non-existent');
expect(retrieved).toBeNull();
});
it('should check if vector exists', async () => {
await searchManager.initialize();
const embedding = new Float32Array([0.1, 0.2, 0.3]);
searchManager.storeVector('node1', embedding, 'test-model');
expect(searchManager.hasVector('node1')).toBe(true);
expect(searchManager.hasVector('node2')).toBe(false);
});
it('should delete vectors', async () => {
await searchManager.initialize();
const embedding = new Float32Array([0.1, 0.2, 0.3]);
searchManager.storeVector('node1', embedding, 'test-model');
expect(searchManager.hasVector('node1')).toBe(true);
searchManager.deleteVector('node1');
expect(searchManager.hasVector('node1')).toBe(false);
});
it('should count vectors', async () => {
await searchManager.initialize();
expect(searchManager.getVectorCount()).toBe(0);
searchManager.storeVector('node1', new Float32Array([0.1, 0.2, 0.3]), 'test');
searchManager.storeVector('node2', new Float32Array([0.4, 0.5, 0.6]), 'test');
expect(searchManager.getVectorCount()).toBe(2);
});
it('should clear all vectors', async () => {
await searchManager.initialize();
searchManager.storeVector('node1', new Float32Array([0.1, 0.2, 0.3]), 'test');
searchManager.storeVector('node2', new Float32Array([0.4, 0.5, 0.6]), 'test');
expect(searchManager.getVectorCount()).toBe(2);
searchManager.clear();
expect(searchManager.getVectorCount()).toBe(0);
});
it('should perform brute-force similarity search', async () => {
await searchManager.initialize();
// Store some test vectors
searchManager.storeVector('node1', new Float32Array([1, 0, 0]), 'test');
searchManager.storeVector('node2', new Float32Array([0.9, 0.1, 0]), 'test');
searchManager.storeVector('node3', new Float32Array([0, 1, 0]), 'test');
// Search for similar to [1, 0, 0]
const query = new Float32Array([1, 0, 0]);
const results = searchManager.search(query, { limit: 3 });
expect(results.length).toBe(3);
expect(results[0].nodeId).toBe('node1'); // Most similar
expect(results[0].score).toBeCloseTo(1.0, 5);
expect(results[1].nodeId).toBe('node2'); // Second most similar
});
it('should respect minScore in search', async () => {
await searchManager.initialize();
searchManager.storeVector('node1', new Float32Array([1, 0, 0]), 'test');
searchManager.storeVector('node2', new Float32Array([0, 1, 0]), 'test');
const query = new Float32Array([1, 0, 0]);
const results = searchManager.search(query, { limit: 10, minScore: 0.5 });
// Only node1 should match with score >= 0.5
expect(results.length).toBe(1);
expect(results[0].nodeId).toBe('node1');
});
it('should store vectors in batch', async () => {
await searchManager.initialize();
// Use normalized 3-dimensional vectors
const entries = [
{ nodeId: 'node1', embedding: new Float32Array([1.0, 0.0, 0.0]) },
{ nodeId: 'node2', embedding: new Float32Array([0.0, 1.0, 0.0]) },
{ nodeId: 'node3', embedding: new Float32Array([0.0, 0.0, 1.0]) },
];
searchManager.storeVectorBatch(entries, 'test-model');
expect(searchManager.getVectorCount()).toBe(3);
expect(searchManager.hasVector('node1')).toBe(true);
expect(searchManager.hasVector('node2')).toBe(true);
expect(searchManager.hasVector('node3')).toBe(true);
});
it('should get indexed node IDs', async () => {
await searchManager.initialize();
searchManager.storeVector('node1', new Float32Array([0.1, 0.2, 0.3]), 'test');
searchManager.storeVector('node2', new Float32Array([0.4, 0.5, 0.6]), 'test');
const ids = searchManager.getIndexedNodeIds();
expect(ids).toContain('node1');
expect(ids).toContain('node2');
expect(ids.length).toBe(2);
});
});
describe('CodeGraph Embedding Integration', () => {
let testDir: string;
let cg: CodeGraph;
beforeEach(() => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-embed-integration-'));
// Create a simple test file
fs.writeFileSync(
path.join(testDir, 'test.ts'),
`
export function processData(input: string): string {
return input.toUpperCase();
}
`
);
cg = CodeGraph.initSync(testDir, {
config: {
include: ['**/*.ts'],
exclude: [],
},
});
});
afterEach(() => {
if (cg) {
cg.destroy();
}
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
it('should report embeddings not initialized', () => {
expect(cg.isEmbeddingsInitialized()).toBe(false);
});
it('should return null embedding stats when not initialized', () => {
const stats = cg.getEmbeddingStats();
expect(stats).toBeNull();
});
it('should throw when calling semanticSearch without initialization', async () => {
await expect(cg.semanticSearch('test')).rejects.toThrow(/not initialized/i);
});
it('should throw when calling findSimilar without initialization', async () => {
await expect(cg.findSimilar('test-id')).rejects.toThrow(/not initialized/i);
});
});
});