Skip to content

Commit b804995

Browse files
committed
test: add context-assembler unit tests
- Add 23 tests for assembleContext and formatContextPackage - Test issue fetching, code search, pattern detection - Test various options (includeCode, includePatterns, maxCodeResults) - Test error handling and edge cases - Test output formatting for all sections - Mark context assembler tests as done in PLAN.md
1 parent 838a30b commit b804995

File tree

2 files changed

+369
-1
lines changed

2 files changed

+369
-1
lines changed

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ Focus on quality, documentation, and developer experience before adding new feat
144144
| Task | Status | Priority |
145145
|------|--------|----------|
146146
| Fix lint warnings | ✅ Done | 🔴 High |
147-
| Context assembler tests | 🔲 Todo | 🟡 Medium |
147+
| Context assembler tests | ✅ Done | 🟡 Medium |
148148
| Integration tests for new tools | 🔲 Todo | 🟢 Low |
149149

150150
### Issue Cleanup
Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
import type { RepositoryIndexer, SearchResult } from '@lytics/dev-agent-core';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import type { ContextPackage } from '../../context-types';
4+
import { assembleContext, formatContextPackage } from '../context-assembler';
5+
6+
// Mock the GitHub fetch
7+
vi.mock('../github', () => ({
8+
fetchGitHubIssue: vi.fn(),
9+
}));
10+
11+
import { fetchGitHubIssue } from '../github';
12+
13+
const mockFetchGitHubIssue = vi.mocked(fetchGitHubIssue);
14+
15+
describe('Context Assembler', () => {
16+
const mockIssue = {
17+
number: 42,
18+
title: 'Add user authentication',
19+
body: 'We need to add JWT-based authentication to the API.\n\n## Acceptance Criteria\n- Login endpoint\n- Logout endpoint',
20+
state: 'open' as const,
21+
createdAt: '2025-01-01T00:00:00Z',
22+
updatedAt: '2025-01-02T00:00:00Z',
23+
labels: ['feature', 'security'],
24+
assignees: [],
25+
author: 'testuser',
26+
comments: [
27+
{
28+
author: 'reviewer',
29+
body: 'Consider using refresh tokens too',
30+
createdAt: '2025-01-01T12:00:00Z',
31+
},
32+
],
33+
};
34+
35+
const mockSearchResults: SearchResult[] = [
36+
{
37+
id: '1',
38+
score: 0.85,
39+
metadata: {
40+
path: 'src/auth/jwt.ts',
41+
name: 'verifyToken',
42+
type: 'function',
43+
snippet: 'export function verifyToken(token: string): boolean { ... }',
44+
file: 'src/auth/jwt.ts',
45+
startLine: 10,
46+
endLine: 20,
47+
exported: true,
48+
},
49+
},
50+
{
51+
id: '2',
52+
score: 0.72,
53+
metadata: {
54+
path: 'src/auth/types.ts',
55+
name: 'AuthConfig',
56+
type: 'interface',
57+
snippet: 'export interface AuthConfig { secret: string; }',
58+
file: 'src/auth/types.ts',
59+
startLine: 1,
60+
endLine: 5,
61+
exported: true,
62+
},
63+
},
64+
];
65+
66+
const mockIndexer = {
67+
search: vi.fn().mockResolvedValue(mockSearchResults),
68+
} as unknown as RepositoryIndexer;
69+
70+
beforeEach(() => {
71+
vi.clearAllMocks();
72+
mockFetchGitHubIssue.mockResolvedValue(mockIssue);
73+
});
74+
75+
describe('assembleContext', () => {
76+
it('should assemble a complete context package', async () => {
77+
const result = await assembleContext(42, mockIndexer, '/repo');
78+
79+
expect(result.issue.number).toBe(42);
80+
expect(result.issue.title).toBe('Add user authentication');
81+
expect(result.issue.author).toBe('testuser');
82+
expect(result.issue.labels).toEqual(['feature', 'security']);
83+
expect(result.issue.comments).toHaveLength(1);
84+
});
85+
86+
it('should include relevant code from search', async () => {
87+
const result = await assembleContext(42, mockIndexer, '/repo');
88+
89+
expect(result.relevantCode).toHaveLength(2);
90+
expect(result.relevantCode[0].file).toBe('src/auth/jwt.ts');
91+
expect(result.relevantCode[0].name).toBe('verifyToken');
92+
expect(result.relevantCode[0].relevanceScore).toBe(0.85);
93+
});
94+
95+
it('should skip code search when includeCode is false', async () => {
96+
const result = await assembleContext(42, mockIndexer, '/repo', {
97+
includeCode: false,
98+
includePatterns: false, // Also disable patterns to avoid any search calls
99+
});
100+
101+
expect(result.relevantCode).toHaveLength(0);
102+
expect(mockIndexer.search).not.toHaveBeenCalled();
103+
});
104+
105+
it('should handle null indexer gracefully', async () => {
106+
const result = await assembleContext(42, null, '/repo');
107+
108+
expect(result.relevantCode).toHaveLength(0);
109+
expect(result.metadata.codeSearchUsed).toBe(false);
110+
});
111+
112+
it('should respect maxCodeResults option', async () => {
113+
await assembleContext(42, mockIndexer, '/repo', {
114+
maxCodeResults: 5,
115+
});
116+
117+
expect(mockIndexer.search).toHaveBeenCalledWith(
118+
expect.any(String),
119+
expect.objectContaining({ limit: 5 })
120+
);
121+
});
122+
123+
it('should detect codebase patterns', async () => {
124+
// Mock search to return test files
125+
const testIndexer = {
126+
search: vi.fn().mockResolvedValue([
127+
{
128+
id: '1',
129+
score: 0.8,
130+
metadata: {
131+
path: 'src/__tests__/auth.test.ts',
132+
name: 'auth tests',
133+
type: 'file',
134+
},
135+
},
136+
]),
137+
} as unknown as RepositoryIndexer;
138+
139+
const result = await assembleContext(42, testIndexer, '/repo');
140+
141+
expect(result.codebasePatterns.testPattern).toBe('*.test.ts');
142+
expect(result.codebasePatterns.testLocation).toBe('__tests__/');
143+
});
144+
145+
it('should skip pattern detection when includePatterns is false', async () => {
146+
const result = await assembleContext(42, mockIndexer, '/repo', {
147+
includePatterns: false,
148+
});
149+
150+
expect(result.codebasePatterns).toEqual({});
151+
});
152+
153+
it('should include metadata with token estimate', async () => {
154+
const result = await assembleContext(42, mockIndexer, '/repo');
155+
156+
expect(result.metadata.generatedAt).toBeDefined();
157+
expect(result.metadata.tokensUsed).toBeGreaterThan(0);
158+
expect(result.metadata.codeSearchUsed).toBe(true);
159+
expect(result.metadata.repositoryPath).toBe('/repo');
160+
});
161+
162+
it('should handle search errors gracefully', async () => {
163+
const errorIndexer = {
164+
search: vi.fn().mockRejectedValue(new Error('Search failed')),
165+
} as unknown as RepositoryIndexer;
166+
167+
const result = await assembleContext(42, errorIndexer, '/repo');
168+
169+
// Should not throw, just return empty code
170+
expect(result.relevantCode).toHaveLength(0);
171+
});
172+
173+
it('should infer relevance reasons correctly', async () => {
174+
// Mock issue with title matching a function name
175+
mockFetchGitHubIssue.mockResolvedValueOnce({
176+
...mockIssue,
177+
title: 'Fix verifyToken function',
178+
});
179+
180+
const result = await assembleContext(42, mockIndexer, '/repo');
181+
182+
expect(result.relevantCode[0].reason).toBe('Name matches issue title');
183+
});
184+
});
185+
186+
describe('formatContextPackage', () => {
187+
const mockContext: ContextPackage = {
188+
issue: {
189+
number: 42,
190+
title: 'Add user authentication',
191+
body: 'We need JWT auth',
192+
labels: ['feature'],
193+
author: 'testuser',
194+
createdAt: '2025-01-01T00:00:00Z',
195+
updatedAt: '2025-01-02T00:00:00Z',
196+
state: 'open',
197+
comments: [
198+
{
199+
author: 'reviewer',
200+
body: 'Looks good',
201+
createdAt: '2025-01-01T12:00:00Z',
202+
},
203+
],
204+
},
205+
relevantCode: [
206+
{
207+
file: 'src/auth.ts',
208+
name: 'authenticate',
209+
type: 'function',
210+
snippet: 'function authenticate() {}',
211+
relevanceScore: 0.85,
212+
reason: 'Similar function pattern',
213+
},
214+
],
215+
codebasePatterns: {
216+
testPattern: '*.test.ts',
217+
testLocation: '__tests__/',
218+
},
219+
relatedHistory: [
220+
{
221+
type: 'pr',
222+
number: 10,
223+
title: 'Previous auth work',
224+
state: 'merged',
225+
relevanceScore: 0.7,
226+
},
227+
],
228+
metadata: {
229+
generatedAt: '2025-01-03T00:00:00Z',
230+
tokensUsed: 500,
231+
codeSearchUsed: true,
232+
historySearchUsed: true,
233+
repositoryPath: '/repo',
234+
},
235+
};
236+
237+
it('should format issue header correctly', () => {
238+
const output = formatContextPackage(mockContext);
239+
240+
expect(output).toContain('# Issue #42: Add user authentication');
241+
expect(output).toContain('**Author:** testuser');
242+
expect(output).toContain('**State:** open');
243+
expect(output).toContain('**Labels:** feature');
244+
});
245+
246+
it('should format issue description', () => {
247+
const output = formatContextPackage(mockContext);
248+
249+
expect(output).toContain('## Description');
250+
expect(output).toContain('We need JWT auth');
251+
});
252+
253+
it('should format comments section', () => {
254+
const output = formatContextPackage(mockContext);
255+
256+
expect(output).toContain('## Comments');
257+
expect(output).toContain('**reviewer**');
258+
expect(output).toContain('Looks good');
259+
});
260+
261+
it('should format relevant code section', () => {
262+
const output = formatContextPackage(mockContext);
263+
264+
expect(output).toContain('## Relevant Code');
265+
expect(output).toContain('### authenticate (function)');
266+
expect(output).toContain('**File:** `src/auth.ts`');
267+
expect(output).toContain('**Relevance:** 85%');
268+
expect(output).toContain('```typescript');
269+
expect(output).toContain('function authenticate() {}');
270+
});
271+
272+
it('should format codebase patterns section', () => {
273+
const output = formatContextPackage(mockContext);
274+
275+
expect(output).toContain('## Codebase Patterns');
276+
expect(output).toContain('**Test naming:** *.test.ts');
277+
expect(output).toContain('**Test location:** __tests__/');
278+
});
279+
280+
it('should format related history section', () => {
281+
const output = formatContextPackage(mockContext);
282+
283+
expect(output).toContain('## Related History');
284+
expect(output).toContain('**PR #10:** Previous auth work (merged)');
285+
});
286+
287+
it('should include metadata footer', () => {
288+
const output = formatContextPackage(mockContext);
289+
290+
expect(output).toContain('*Context assembled at');
291+
expect(output).toContain('~500 tokens*');
292+
});
293+
294+
it('should handle empty comments gracefully', () => {
295+
const contextNoComments: ContextPackage = {
296+
...mockContext,
297+
issue: { ...mockContext.issue, comments: [] },
298+
};
299+
300+
const output = formatContextPackage(contextNoComments);
301+
302+
expect(output).not.toContain('## Comments');
303+
});
304+
305+
it('should handle empty code results gracefully', () => {
306+
const contextNoCode: ContextPackage = {
307+
...mockContext,
308+
relevantCode: [],
309+
};
310+
311+
const output = formatContextPackage(contextNoCode);
312+
313+
expect(output).not.toContain('## Relevant Code');
314+
});
315+
316+
it('should handle empty patterns gracefully', () => {
317+
const contextNoPatterns: ContextPackage = {
318+
...mockContext,
319+
codebasePatterns: {},
320+
};
321+
322+
const output = formatContextPackage(contextNoPatterns);
323+
324+
expect(output).not.toContain('## Codebase Patterns');
325+
});
326+
327+
it('should handle empty history gracefully', () => {
328+
const contextNoHistory: ContextPackage = {
329+
...mockContext,
330+
relatedHistory: [],
331+
};
332+
333+
const output = formatContextPackage(contextNoHistory);
334+
335+
expect(output).not.toContain('## Related History');
336+
});
337+
338+
it('should handle missing description', () => {
339+
const contextNoBody: ContextPackage = {
340+
...mockContext,
341+
issue: { ...mockContext.issue, body: '' },
342+
};
343+
344+
const output = formatContextPackage(contextNoBody);
345+
346+
expect(output).toContain('_No description provided_');
347+
});
348+
349+
it('should handle issues type in history', () => {
350+
const contextWithIssue: ContextPackage = {
351+
...mockContext,
352+
relatedHistory: [
353+
{
354+
type: 'issue',
355+
number: 5,
356+
title: 'Related bug',
357+
state: 'closed',
358+
relevanceScore: 0.6,
359+
},
360+
],
361+
};
362+
363+
const output = formatContextPackage(contextWithIssue);
364+
365+
expect(output).toContain('**Issue #5:** Related bug (closed)');
366+
});
367+
});
368+
});

0 commit comments

Comments
 (0)