/** * 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 { // 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 { 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'); } }); }); });