| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- /**
- * 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 symbols in compact format', async () => {
- const result = await cg.buildContext('checkout', {
- format: 'markdown',
- maxNodes: 10,
- });
- const markdown = result as string;
- // Compact format uses "Related Symbols" instead of verbose "Related Files"
- // and groups symbols by file for compactness
- expect(markdown).toContain('### Entry Points');
- });
- it('should have compact output without verbose stats footer', async () => {
- const result = await cg.buildContext('payment', {
- format: 'markdown',
- });
- const markdown = result as string;
- // Compact format should NOT have verbose stats footer
- expect(markdown).not.toMatch(/\*Context:.*symbols.*relationships.*files/);
- // But should still have query
- expect(markdown).toContain('**Query:**');
- });
- });
- 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');
- }
- });
- });
- });
|