Skip to content

Commit bfb8eff

Browse files
committed
Fix code_search context cases
1 parent 54fdc08 commit bfb8eff

File tree

2 files changed

+217
-48
lines changed

2 files changed

+217
-48
lines changed

sdk/src/__tests__/code-search.test.ts

Lines changed: 152 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ function createRgJsonMatch(
3737
})
3838
}
3939

40+
// Helper to create ripgrep JSON context output (for -A, -B, -C flags)
41+
function createRgJsonContext(
42+
filePath: string,
43+
lineNumber: number,
44+
lineText: string,
45+
): string {
46+
return JSON.stringify({
47+
type: 'context',
48+
data: {
49+
path: { text: filePath },
50+
lines: { text: lineText },
51+
line_number: lineNumber,
52+
},
53+
})
54+
}
55+
4056
describe('codeSearch', () => {
4157
let mockSpawn: ReturnType<typeof mock>
4258
let mockProcess: ReturnType<typeof createMockChildProcess>
@@ -87,10 +103,14 @@ describe('codeSearch', () => {
87103
flags: '-A 2',
88104
})
89105

90-
// Ripgrep JSON output - only match events are processed
106+
// Ripgrep JSON output with -A 2 includes match + 2 context lines after
91107
const output = [
92108
createRgJsonMatch('test.ts', 1, 'import { env } from "./config"'),
109+
createRgJsonContext('test.ts', 2, 'const apiUrl = env.API_URL'),
110+
createRgJsonContext('test.ts', 3, 'const apiKey = env.API_KEY'),
93111
createRgJsonMatch('other.ts', 5, 'import env from "process"'),
112+
createRgJsonContext('other.ts', 6, 'const nodeEnv = env.NODE_ENV'),
113+
createRgJsonContext('other.ts', 7, 'const port = env.PORT'),
94114
].join('\n')
95115

96116
mockProcess.stdout.emit('data', Buffer.from(output))
@@ -100,12 +120,15 @@ describe('codeSearch', () => {
100120
expect(result[0].type).toBe('json')
101121
const value = result[0].value as any
102122

103-
// Should contain both files
104-
expect(value.stdout).toContain('test.ts:')
105-
expect(value.stdout).toContain('other.ts:')
123+
// Should contain match lines
124+
expect(value.stdout).toContain('import { env } from "./config"')
125+
expect(value.stdout).toContain('import env from "process"')
106126

107-
// Should not include the entire file content
108-
expect(value.stdout.length).toBeLessThan(1000)
127+
// Should contain context lines (this is the bug - they're currently missing)
128+
expect(value.stdout).toContain('const apiUrl = env.API_URL')
129+
expect(value.stdout).toContain('const apiKey = env.API_KEY')
130+
expect(value.stdout).toContain('const nodeEnv = env.NODE_ENV')
131+
expect(value.stdout).toContain('const port = env.PORT')
109132
})
110133

111134
it('should correctly parse output with -B flag (before context)', async () => {
@@ -115,8 +138,13 @@ describe('codeSearch', () => {
115138
flags: '-B 2',
116139
})
117140

141+
// Ripgrep JSON output with -B 2 includes 2 context lines before + match
118142
const output = [
143+
createRgJsonContext('app.ts', 1, 'import React from "react"'),
144+
createRgJsonContext('app.ts', 2, ''),
119145
createRgJsonMatch('app.ts', 3, 'export const main = () => {}'),
146+
createRgJsonContext('utils.ts', 8, 'function validateInput(x: string) {'),
147+
createRgJsonContext('utils.ts', 9, ' return x.length > 0'),
120148
createRgJsonMatch('utils.ts', 10, 'export function helper() {}'),
121149
].join('\n')
122150

@@ -126,8 +154,14 @@ describe('codeSearch', () => {
126154
const result = await searchPromise
127155
const value = result[0].value as any
128156

129-
expect(value.stdout).toContain('app.ts:')
130-
expect(value.stdout).toContain('utils.ts:')
157+
// Should contain match lines
158+
expect(value.stdout).toContain('export const main = () => {}')
159+
expect(value.stdout).toContain('export function helper() {}')
160+
161+
// Should contain before context lines
162+
expect(value.stdout).toContain('import React from "react"')
163+
expect(value.stdout).toContain('function validateInput(x: string) {')
164+
expect(value.stdout).toContain('return x.length > 0')
131165
})
132166

133167
it('should correctly parse output with -C flag (context before and after)', async () => {
@@ -137,16 +171,73 @@ describe('codeSearch', () => {
137171
flags: '-C 1',
138172
})
139173

140-
const output = createRgJsonMatch('code.ts', 6, ' // TODO: implement this')
174+
// Ripgrep JSON output with -C 1 includes 1 line before + match + 1 line after
175+
const output = [
176+
createRgJsonContext('code.ts', 5, 'function processData() {'),
177+
createRgJsonMatch('code.ts', 6, ' // TODO: implement this'),
178+
createRgJsonContext('code.ts', 7, ' return null'),
179+
].join('\n')
180+
181+
mockProcess.stdout.emit('data', Buffer.from(output))
182+
mockProcess.emit('close', 0)
183+
184+
const result = await searchPromise
185+
const value = result[0].value as any
186+
187+
// Should contain match line
188+
expect(value.stdout).toContain('TODO: implement this')
189+
190+
// Should contain context lines before and after
191+
expect(value.stdout).toContain('function processData() {')
192+
expect(value.stdout).toContain('return null')
193+
})
194+
195+
it('should handle -A flag with multiple matches in same file', async () => {
196+
const searchPromise = codeSearch({
197+
projectPath: '/test/project',
198+
pattern: 'import',
199+
flags: '-A 1',
200+
})
201+
202+
const output = [
203+
createRgJsonMatch('file.ts', 1, 'import foo from "foo"'),
204+
createRgJsonContext('file.ts', 2, 'import bar from "bar"'),
205+
createRgJsonMatch('file.ts', 3, 'import baz from "baz"'),
206+
createRgJsonContext('file.ts', 4, ''),
207+
].join('\n')
208+
209+
mockProcess.stdout.emit('data', Buffer.from(output))
210+
mockProcess.emit('close', 0)
211+
212+
const result = await searchPromise
213+
const value = result[0].value as any
214+
215+
// Should contain all matches
216+
expect(value.stdout).toContain('import foo from "foo"')
217+
expect(value.stdout).toContain('import baz from "baz"')
218+
219+
// Context line appears as both context and match
220+
expect(value.stdout).toContain('import bar from "bar"')
221+
})
222+
223+
it('should handle -B flag at start of file', async () => {
224+
const searchPromise = codeSearch({
225+
projectPath: '/test/project',
226+
pattern: 'import',
227+
flags: '-B 2',
228+
})
229+
230+
// First line match has no before context
231+
const output = createRgJsonMatch('file.ts', 1, 'import foo from "foo"')
141232

142233
mockProcess.stdout.emit('data', Buffer.from(output))
143234
mockProcess.emit('close', 0)
144235

145236
const result = await searchPromise
146237
const value = result[0].value as any
147238

148-
expect(value.stdout).toContain('code.ts:')
149-
expect(value.stdout).toContain('TODO')
239+
// Should still work with match at file start
240+
expect(value.stdout).toContain('import foo from "foo"')
150241
})
151242

152243
it('should skip separator lines between result groups', async () => {
@@ -261,9 +352,13 @@ describe('codeSearch', () => {
261352

262353
const output = [
263354
createRgJsonMatch('file.ts', 1, 'test 1'),
355+
createRgJsonContext('file.ts', 2, 'context 1'),
264356
createRgJsonMatch('file.ts', 5, 'test 2'),
357+
createRgJsonContext('file.ts', 6, 'context 2'),
265358
createRgJsonMatch('file.ts', 10, 'test 3'),
359+
createRgJsonContext('file.ts', 11, 'context 3'),
266360
createRgJsonMatch('file.ts', 15, 'test 4'),
361+
createRgJsonContext('file.ts', 16, 'context 4'),
267362
].join('\n')
268363

269364
mockProcess.stdout.emit('data', Buffer.from(output))
@@ -272,11 +367,19 @@ describe('codeSearch', () => {
272367
const result = await searchPromise
273368
const value = result[0].value as any
274369

275-
// Should be limited to 2 results per file
370+
// Should be limited to 2 match results per file (context lines don't count toward limit)
276371
// Count how many 'test' matches are in the output
277372
const testMatches = (value.stdout.match(/test \d/g) || []).length
278373
expect(testMatches).toBeLessThanOrEqual(2)
279374
expect(value.stdout).toContain('Results limited')
375+
376+
// Should still include context lines for the matches that are shown
377+
if (value.stdout.includes('test 1')) {
378+
expect(value.stdout).toContain('context 1')
379+
}
380+
if (value.stdout.includes('test 2')) {
381+
expect(value.stdout).toContain('context 2')
382+
}
280383
})
281384

282385
it('should respect globalMaxResults with context lines', async () => {
@@ -289,9 +392,13 @@ describe('codeSearch', () => {
289392

290393
const output = [
291394
createRgJsonMatch('file1.ts', 1, 'test 1'),
395+
createRgJsonContext('file1.ts', 2, 'context 1'),
292396
createRgJsonMatch('file1.ts', 5, 'test 2'),
397+
createRgJsonContext('file1.ts', 6, 'context 2'),
293398
createRgJsonMatch('file2.ts', 1, 'test 3'),
399+
createRgJsonContext('file2.ts', 2, 'context 3'),
294400
createRgJsonMatch('file2.ts', 5, 'test 4'),
401+
createRgJsonContext('file2.ts', 6, 'context 4'),
295402
].join('\n')
296403

297404
mockProcess.stdout.emit('data', Buffer.from(output))
@@ -300,7 +407,7 @@ describe('codeSearch', () => {
300407
const result = await searchPromise
301408
const value = result[0].value as any
302409

303-
// Should be limited globally to 3 results
410+
// Should be limited globally to 3 match results (context lines don't count)
304411
const matches = (value.stdout.match(/test \d/g) || []).length
305412
expect(matches).toBeLessThanOrEqual(3)
306413
// Check for either 'Global limit' message or truncation indicator
@@ -309,6 +416,38 @@ describe('codeSearch', () => {
309416
value.stdout.includes('Results limited')
310417
expect(hasLimitMessage).toBe(true)
311418
})
419+
420+
it('should not count context lines toward maxResults limit', async () => {
421+
const searchPromise = codeSearch({
422+
projectPath: '/test/project',
423+
pattern: 'match',
424+
flags: '-A 2 -B 2',
425+
maxResults: 1,
426+
})
427+
428+
const output = [
429+
createRgJsonContext('file.ts', 1, 'context before 1'),
430+
createRgJsonContext('file.ts', 2, 'context before 2'),
431+
createRgJsonMatch('file.ts', 3, 'match line'),
432+
createRgJsonContext('file.ts', 4, 'context after 1'),
433+
createRgJsonContext('file.ts', 5, 'context after 2'),
434+
].join('\n')
435+
436+
mockProcess.stdout.emit('data', Buffer.from(output))
437+
mockProcess.emit('close', 0)
438+
439+
const result = await searchPromise
440+
const value = result[0].value as any
441+
442+
// Should include the match
443+
expect(value.stdout).toContain('match line')
444+
445+
// Should include all context lines even though maxResults is 1
446+
expect(value.stdout).toContain('context before 1')
447+
expect(value.stdout).toContain('context before 2')
448+
expect(value.stdout).toContain('context after 1')
449+
expect(value.stdout).toContain('context after 2')
450+
})
312451
})
313452

314453
describe('malformed output handling', () => {

0 commit comments

Comments
 (0)