1
0

context.test.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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. fs.writeFileSync(
  114. path.join(srcDir, 'callback_ops.c'),
  115. `union CallbackOps { int (*run)(int); };
  116. `
  117. );
  118. // Initialize CodeGraph
  119. cg = CodeGraph.initSync(testDir, {
  120. config: {
  121. include: ['**/*.ts', '**/*.c'],
  122. exclude: [],
  123. },
  124. });
  125. // Index the codebase
  126. await cg.indexAll();
  127. });
  128. afterEach(() => {
  129. if (cg) {
  130. cg.destroy();
  131. }
  132. if (fs.existsSync(testDir)) {
  133. fs.rmSync(testDir, { recursive: true, force: true });
  134. }
  135. });
  136. describe('getCode()', () => {
  137. it('should extract code for a node', async () => {
  138. // Find the PaymentService class
  139. const nodes = cg.getNodesByKind('class');
  140. const paymentService = nodes.find((n) => n.name === 'PaymentService');
  141. expect(paymentService).toBeDefined();
  142. const code = await cg.getCode(paymentService!.id);
  143. expect(code).not.toBeNull();
  144. expect(code).toContain('class PaymentService');
  145. expect(code).toContain('processPayment');
  146. });
  147. it('should return null for non-existent node', async () => {
  148. const code = await cg.getCode('non-existent-id');
  149. expect(code).toBeNull();
  150. });
  151. });
  152. describe('findRelevantContext()', () => {
  153. it('should find relevant nodes for a query', async () => {
  154. // Use simple query that matches symbol names (FTS5 treats spaces as AND)
  155. const result = await cg.findRelevantContext('PaymentService');
  156. expect(result.nodes.size).toBeGreaterThan(0);
  157. // Should find payment-related nodes
  158. const nodeNames = Array.from(result.nodes.values()).map((n) => n.name);
  159. expect(
  160. nodeNames.some(
  161. (name) =>
  162. name.toLowerCase().includes('payment') ||
  163. name.toLowerCase().includes('checkout')
  164. )
  165. ).toBe(true);
  166. });
  167. it('includes union definitions in the default context search', async () => {
  168. const result = await cg.findRelevantContext('CallbackOps');
  169. const union = [...result.nodes.values()].find(
  170. (node) => node.kind === 'union' && node.name === 'CallbackOps'
  171. );
  172. expect(union).toBeDefined();
  173. });
  174. it('should include edges in the result', async () => {
  175. const result = await cg.findRelevantContext('checkout', {
  176. traversalDepth: 2,
  177. });
  178. // Should have some edges from traversal
  179. expect(result.edges).toBeDefined();
  180. });
  181. it('should respect maxNodes option', async () => {
  182. const result = await cg.findRelevantContext('function', {
  183. maxNodes: 5,
  184. });
  185. expect(result.nodes.size).toBeLessThanOrEqual(5);
  186. });
  187. });
  188. describe('buildContext()', () => {
  189. it('should build context with markdown format', async () => {
  190. const result = await cg.buildContext('Fix checkout error', {
  191. format: 'markdown',
  192. maxCodeBlocks: 3,
  193. });
  194. expect(typeof result).toBe('string');
  195. const markdown = result as string;
  196. // Should contain markdown structure
  197. expect(markdown).toContain('## Code Context');
  198. expect(markdown).toContain('**Query:** Fix checkout error');
  199. });
  200. it('should build context with JSON format', async () => {
  201. const result = await cg.buildContext('payment processing', {
  202. format: 'json',
  203. });
  204. expect(typeof result).toBe('string');
  205. const parsed = JSON.parse(result as string);
  206. expect(parsed.query).toBe('payment processing');
  207. expect(parsed.nodes).toBeDefined();
  208. expect(Array.isArray(parsed.nodes)).toBe(true);
  209. });
  210. it('should accept object input with title and description', async () => {
  211. const result = await cg.buildContext(
  212. {
  213. title: 'Checkout bug',
  214. description: 'Cart total calculation is wrong',
  215. },
  216. { format: 'markdown' }
  217. );
  218. expect(typeof result).toBe('string');
  219. expect(result).toContain('Checkout bug: Cart total calculation is wrong');
  220. });
  221. it('should include code blocks when requested', async () => {
  222. const result = await cg.buildContext('PaymentService', {
  223. format: 'markdown',
  224. includeCode: true,
  225. maxCodeBlocks: 2,
  226. });
  227. const markdown = result as string;
  228. // Should contain code blocks
  229. expect(markdown).toContain('### Code');
  230. expect(markdown).toContain('```typescript');
  231. });
  232. it('should exclude code blocks when requested', async () => {
  233. const result = await cg.buildContext('payment', {
  234. format: 'markdown',
  235. includeCode: false,
  236. });
  237. const markdown = result as string;
  238. // Should not contain code section
  239. expect(markdown).not.toContain('### Code');
  240. });
  241. it('should include related symbols in compact format', async () => {
  242. const result = await cg.buildContext('checkout', {
  243. format: 'markdown',
  244. maxNodes: 10,
  245. });
  246. const markdown = result as string;
  247. // Compact format uses "Related Symbols" instead of verbose "Related Files"
  248. // and groups symbols by file for compactness
  249. expect(markdown).toContain('### Entry Points');
  250. });
  251. it('should have compact output without verbose stats footer', async () => {
  252. const result = await cg.buildContext('payment', {
  253. format: 'markdown',
  254. });
  255. const markdown = result as string;
  256. // Compact format should NOT have verbose stats footer
  257. expect(markdown).not.toMatch(/\*Context:.*symbols.*relationships.*files/);
  258. // But should still have query
  259. expect(markdown).toContain('**Query:**');
  260. });
  261. });
  262. describe('Context structure', () => {
  263. it('should find entry points from search', async () => {
  264. const result = await cg.buildContext('PaymentService', {
  265. format: 'json',
  266. });
  267. const parsed = JSON.parse(result as string);
  268. expect(parsed.entryPoints).toBeDefined();
  269. expect(parsed.entryPoints.length).toBeGreaterThan(0);
  270. });
  271. it('should traverse graph from entry points', async () => {
  272. const result = await cg.buildContext('CheckoutController', {
  273. format: 'json',
  274. traversalDepth: 2,
  275. });
  276. const parsed = JSON.parse(result as string);
  277. // Should have found related nodes through traversal
  278. const nodeNames = parsed.nodes.map((n: { name: string }) => n.name);
  279. // CheckoutController calls PaymentService, so both should be present
  280. expect(
  281. nodeNames.some((name: string) => name.includes('Checkout'))
  282. ).toBe(true);
  283. });
  284. });
  285. describe('Edge cases', () => {
  286. it('should handle empty query', async () => {
  287. const result = await cg.buildContext('', { format: 'markdown' });
  288. expect(typeof result).toBe('string');
  289. });
  290. it('should handle query with no matches', async () => {
  291. const result = await cg.buildContext('xyznonexistent123', {
  292. format: 'json',
  293. });
  294. const parsed = JSON.parse(result as string);
  295. // Should return empty or minimal results
  296. expect(parsed.nodes).toBeDefined();
  297. });
  298. it('should truncate long code blocks', async () => {
  299. const result = await cg.buildContext('PaymentService', {
  300. format: 'markdown',
  301. maxCodeBlockSize: 100,
  302. includeCode: true,
  303. });
  304. const markdown = result as string;
  305. // Long code blocks should be truncated
  306. if (markdown.includes('```typescript')) {
  307. // If there's a code block, check for truncation marker if content was long
  308. // This test validates the truncation logic works
  309. expect(typeof markdown).toBe('string');
  310. }
  311. });
  312. });
  313. });