forked from colbymchenry/codegraph
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontext.test.ts
More file actions
374 lines (301 loc) · 9.78 KB
/
context.test.ts
File metadata and controls
374 lines (301 loc) · 9.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
/**
* 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');
}
});
});
});