context.test.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. /**
  2. * Context Builder Tests
  3. *
  4. * Tests for the context building functionality.
  5. */
  6. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  7. import * as fs from 'fs';
  8. import * as path from 'path';
  9. import * as os from 'os';
  10. import CodeGraph from '../src/index';
  11. describe('Context Builder', () => {
  12. let testDir: string;
  13. let cg: CodeGraph;
  14. beforeEach(async () => {
  15. testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-test-'));
  16. // Create a sample codebase
  17. const srcDir = path.join(testDir, 'src');
  18. fs.mkdirSync(srcDir);
  19. // Create a payment service file
  20. fs.writeFileSync(
  21. path.join(srcDir, 'payment.ts'),
  22. `/**
  23. * Payment Service
  24. * Handles payment processing logic.
  25. */
  26. export interface PaymentResult {
  27. success: boolean;
  28. transactionId: string;
  29. amount: number;
  30. }
  31. export class PaymentService {
  32. private apiKey: string;
  33. constructor(apiKey: string) {
  34. this.apiKey = apiKey;
  35. }
  36. /**
  37. * Process a payment for the given amount
  38. */
  39. async processPayment(amount: number): Promise<PaymentResult> {
  40. // Validate amount
  41. if (amount <= 0) {
  42. throw new Error('Invalid amount');
  43. }
  44. // Process payment
  45. const transactionId = this.generateTransactionId();
  46. return {
  47. success: true,
  48. transactionId,
  49. amount,
  50. };
  51. }
  52. private generateTransactionId(): string {
  53. return 'txn_' + Math.random().toString(36).substring(2);
  54. }
  55. }
  56. export function createPaymentService(apiKey: string): PaymentService {
  57. return new PaymentService(apiKey);
  58. }
  59. `
  60. );
  61. // Create a checkout controller file
  62. fs.writeFileSync(
  63. path.join(srcDir, 'checkout.ts'),
  64. `/**
  65. * Checkout Controller
  66. * Handles the checkout flow.
  67. */
  68. import { PaymentService, PaymentResult } from './payment';
  69. export interface CartItem {
  70. id: string;
  71. name: string;
  72. price: number;
  73. quantity: number;
  74. }
  75. export class CheckoutController {
  76. private paymentService: PaymentService;
  77. constructor(paymentService: PaymentService) {
  78. this.paymentService = paymentService;
  79. }
  80. /**
  81. * Process checkout for the given cart
  82. */
  83. async processCheckout(cart: CartItem[]): Promise<PaymentResult> {
  84. const total = this.calculateTotal(cart);
  85. if (total === 0) {
  86. throw new Error('Cart is empty');
  87. }
  88. return this.paymentService.processPayment(total);
  89. }
  90. /**
  91. * Calculate the total price of the cart
  92. */
  93. calculateTotal(cart: CartItem[]): number {
  94. return cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  95. }
  96. }
  97. `
  98. );
  99. // Create a utilities file
  100. fs.writeFileSync(
  101. path.join(srcDir, 'utils.ts'),
  102. `/**
  103. * Utility functions
  104. */
  105. export function formatCurrency(amount: number): string {
  106. return '$' + amount.toFixed(2);
  107. }
  108. export function validateEmail(email: string): boolean {
  109. return email.includes('@');
  110. }
  111. `
  112. );
  113. // Initialize CodeGraph
  114. cg = CodeGraph.initSync(testDir, {
  115. config: {
  116. include: ['**/*.ts'],
  117. exclude: [],
  118. },
  119. });
  120. // Index the codebase
  121. await cg.indexAll();
  122. });
  123. afterEach(() => {
  124. if (cg) {
  125. cg.destroy();
  126. }
  127. if (fs.existsSync(testDir)) {
  128. fs.rmSync(testDir, { recursive: true, force: true });
  129. }
  130. });
  131. describe('getCode()', () => {
  132. it('should extract code for a node', async () => {
  133. // Find the PaymentService class
  134. const nodes = cg.getNodesByKind('class');
  135. const paymentService = nodes.find((n) => n.name === 'PaymentService');
  136. expect(paymentService).toBeDefined();
  137. const code = await cg.getCode(paymentService!.id);
  138. expect(code).not.toBeNull();
  139. expect(code).toContain('class PaymentService');
  140. expect(code).toContain('processPayment');
  141. });
  142. it('should return null for non-existent node', async () => {
  143. const code = await cg.getCode('non-existent-id');
  144. expect(code).toBeNull();
  145. });
  146. });
  147. describe('findRelevantContext()', () => {
  148. it('should find relevant nodes for a query', async () => {
  149. // Use simple query that matches symbol names (FTS5 treats spaces as AND)
  150. const result = await cg.findRelevantContext('PaymentService');
  151. expect(result.nodes.size).toBeGreaterThan(0);
  152. // Should find payment-related nodes
  153. const nodeNames = Array.from(result.nodes.values()).map((n) => n.name);
  154. expect(
  155. nodeNames.some(
  156. (name) =>
  157. name.toLowerCase().includes('payment') ||
  158. name.toLowerCase().includes('checkout')
  159. )
  160. ).toBe(true);
  161. });
  162. it('should include edges in the result', async () => {
  163. const result = await cg.findRelevantContext('checkout', {
  164. traversalDepth: 2,
  165. });
  166. // Should have some edges from traversal
  167. expect(result.edges).toBeDefined();
  168. });
  169. it('should respect maxNodes option', async () => {
  170. const result = await cg.findRelevantContext('function', {
  171. maxNodes: 5,
  172. });
  173. expect(result.nodes.size).toBeLessThanOrEqual(5);
  174. });
  175. });
  176. describe('buildContext()', () => {
  177. it('should build context with markdown format', async () => {
  178. const result = await cg.buildContext('Fix checkout error', {
  179. format: 'markdown',
  180. maxCodeBlocks: 3,
  181. });
  182. expect(typeof result).toBe('string');
  183. const markdown = result as string;
  184. // Should contain markdown structure
  185. expect(markdown).toContain('## Code Context');
  186. expect(markdown).toContain('**Query:** Fix checkout error');
  187. });
  188. it('should build context with JSON format', async () => {
  189. const result = await cg.buildContext('payment processing', {
  190. format: 'json',
  191. });
  192. expect(typeof result).toBe('string');
  193. const parsed = JSON.parse(result as string);
  194. expect(parsed.query).toBe('payment processing');
  195. expect(parsed.nodes).toBeDefined();
  196. expect(Array.isArray(parsed.nodes)).toBe(true);
  197. });
  198. it('should accept object input with title and description', async () => {
  199. const result = await cg.buildContext(
  200. {
  201. title: 'Checkout bug',
  202. description: 'Cart total calculation is wrong',
  203. },
  204. { format: 'markdown' }
  205. );
  206. expect(typeof result).toBe('string');
  207. expect(result).toContain('Checkout bug: Cart total calculation is wrong');
  208. });
  209. it('should include code blocks when requested', async () => {
  210. const result = await cg.buildContext('PaymentService', {
  211. format: 'markdown',
  212. includeCode: true,
  213. maxCodeBlocks: 2,
  214. });
  215. const markdown = result as string;
  216. // Should contain code blocks
  217. expect(markdown).toContain('### Code');
  218. expect(markdown).toContain('```typescript');
  219. });
  220. it('should exclude code blocks when requested', async () => {
  221. const result = await cg.buildContext('payment', {
  222. format: 'markdown',
  223. includeCode: false,
  224. });
  225. const markdown = result as string;
  226. // Should not contain code section
  227. expect(markdown).not.toContain('### Code');
  228. });
  229. it('should include related symbols in compact format', async () => {
  230. const result = await cg.buildContext('checkout', {
  231. format: 'markdown',
  232. maxNodes: 10,
  233. });
  234. const markdown = result as string;
  235. // Compact format uses "Related Symbols" instead of verbose "Related Files"
  236. // and groups symbols by file for compactness
  237. expect(markdown).toContain('### Entry Points');
  238. });
  239. it('should have compact output without verbose stats footer', async () => {
  240. const result = await cg.buildContext('payment', {
  241. format: 'markdown',
  242. });
  243. const markdown = result as string;
  244. // Compact format should NOT have verbose stats footer
  245. expect(markdown).not.toMatch(/\*Context:.*symbols.*relationships.*files/);
  246. // But should still have query
  247. expect(markdown).toContain('**Query:**');
  248. });
  249. });
  250. describe('Context structure', () => {
  251. it('should find entry points from search', async () => {
  252. const result = await cg.buildContext('PaymentService', {
  253. format: 'json',
  254. });
  255. const parsed = JSON.parse(result as string);
  256. expect(parsed.entryPoints).toBeDefined();
  257. expect(parsed.entryPoints.length).toBeGreaterThan(0);
  258. });
  259. it('should traverse graph from entry points', async () => {
  260. const result = await cg.buildContext('CheckoutController', {
  261. format: 'json',
  262. traversalDepth: 2,
  263. });
  264. const parsed = JSON.parse(result as string);
  265. // Should have found related nodes through traversal
  266. const nodeNames = parsed.nodes.map((n: { name: string }) => n.name);
  267. // CheckoutController calls PaymentService, so both should be present
  268. expect(
  269. nodeNames.some((name: string) => name.includes('Checkout'))
  270. ).toBe(true);
  271. });
  272. });
  273. describe('Edge cases', () => {
  274. it('should handle empty query', async () => {
  275. const result = await cg.buildContext('', { format: 'markdown' });
  276. expect(typeof result).toBe('string');
  277. });
  278. it('should handle query with no matches', async () => {
  279. const result = await cg.buildContext('xyznonexistent123', {
  280. format: 'json',
  281. });
  282. const parsed = JSON.parse(result as string);
  283. // Should return empty or minimal results
  284. expect(parsed.nodes).toBeDefined();
  285. });
  286. it('should truncate long code blocks', async () => {
  287. const result = await cg.buildContext('PaymentService', {
  288. format: 'markdown',
  289. maxCodeBlockSize: 100,
  290. includeCode: true,
  291. });
  292. const markdown = result as string;
  293. // Long code blocks should be truncated
  294. if (markdown.includes('```typescript')) {
  295. // If there's a code block, check for truncation marker if content was long
  296. // This test validates the truncation logic works
  297. expect(typeof markdown).toBe('string');
  298. }
  299. });
  300. });
  301. });