@@ -11,10 +11,7 @@ import {
1111 KEYWORD_INDEX_FILENAME ,
1212 VECTOR_DB_DIRNAME
1313} from '../src/constants/codebase-context.js' ;
14- import {
15- CONTEXT_RESOURCE_URI ,
16- buildProjectContextResourceUri
17- } from '../src/resources/uri.js' ;
14+ import { CONTEXT_RESOURCE_URI , buildProjectContextResourceUri } from '../src/resources/uri.js' ;
1815
1916interface SearchResultRow {
2017 summary : string ;
@@ -47,6 +44,10 @@ const searchMocks = vi.hoisted(() => ({
4744 search : vi . fn ( )
4845} ) ) ;
4946
47+ const indexerMocks = vi . hoisted ( ( ) => ( {
48+ index : vi . fn ( )
49+ } ) ) ;
50+
5051const watcherMocks = vi . hoisted ( ( ) => ( {
5152 start : vi . fn ( )
5253} ) ) ;
@@ -72,6 +73,7 @@ vi.mock('../src/core/indexer.js', () => {
7273 }
7374
7475 async index ( ) {
76+ indexerMocks . index ( ) ;
7577 return {
7678 totalFiles : 0 ,
7779 indexedFiles : 0 ,
@@ -172,6 +174,7 @@ describe('multi-project routing', () => {
172174 beforeEach ( async ( ) => {
173175 vi . resetModules ( ) ;
174176 searchMocks . search . mockReset ( ) ;
177+ indexerMocks . index . mockReset ( ) ;
175178 watcherMocks . start . mockReset ( ) ;
176179
177180 originalArgv = [ ...process . argv ] ;
@@ -258,6 +261,75 @@ describe('multi-project routing', () => {
258261 }
259262 } ) ;
260263
264+ it ( 'ignores invalid client roots instead of registering or creating them' , async ( ) => {
265+ delete process . env . CODEBASE_ROOT ;
266+ delete process . argv [ 2 ] ;
267+
268+ const missingRoot = path . join ( os . tmpdir ( ) , `cc-missing-root-${ Date . now ( ) } ` ) ;
269+ const { server, refreshKnownRootsFromClient } = await import ( '../src/index.js' ) ;
270+ const typedServer = server as unknown as TestServer & {
271+ listRoots : ( ) => Promise < { roots : Array < { uri : string ; name ?: string } > } > ;
272+ } ;
273+ const originalListRoots = typedServer . listRoots . bind ( typedServer ) ;
274+ const toolHandler = typedServer . _requestHandlers . get ( 'tools/call' ) ;
275+ const resourceHandler = typedServer . _requestHandlers . get ( 'resources/read' ) ;
276+ if ( ! toolHandler || ! resourceHandler ) throw new Error ( 'required handlers not registered' ) ;
277+
278+ typedServer . listRoots = vi . fn ( ) . mockResolvedValue ( {
279+ roots : [ { uri : pathToFileURL ( missingRoot ) . href , name : 'Missing' } ]
280+ } ) ;
281+
282+ try {
283+ await refreshKnownRootsFromClient ( ) ;
284+
285+ const response = await callTool ( toolHandler , 90 , 'search_codebase' , { query : 'feature' } ) ;
286+ const payload = parsePayload ( response ) as {
287+ status : string ;
288+ errorCode : string ;
289+ } ;
290+
291+ expect ( response . isError ) . toBe ( true ) ;
292+ expect ( payload . status ) . toBe ( 'selection_required' ) ;
293+ expect ( payload . errorCode ) . toBe ( 'selection_required' ) ;
294+ await expect ( fs . stat ( missingRoot ) ) . rejects . toThrow ( ) ;
295+
296+ const resourceResponse = ( await resourceHandler ( {
297+ jsonrpc : '2.0' ,
298+ id : 91 ,
299+ method : 'resources/read' ,
300+ params : { uri : CONTEXT_RESOURCE_URI }
301+ } ) ) as ResourceReadResponse ;
302+
303+ expect ( resourceResponse . contents [ 0 ] ?. text ) . not . toContain ( missingRoot ) ;
304+ } finally {
305+ typedServer . listRoots = originalListRoots ;
306+ }
307+ } ) ;
308+
309+ it ( 'does not eagerly index every announced root during background refresh' , async ( ) => {
310+ delete process . env . CODEBASE_ROOT ;
311+ delete process . argv [ 2 ] ;
312+
313+ const unindexedRoot = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'cc-unindexed-root-' ) ) ;
314+ const { server, refreshKnownRootsFromClient } = await import ( '../src/index.js' ) ;
315+ const typedServer = server as unknown as TestServer & {
316+ listRoots : ( ) => Promise < { roots : Array < { uri : string ; name ?: string } > } > ;
317+ } ;
318+ const originalListRoots = typedServer . listRoots . bind ( typedServer ) ;
319+
320+ typedServer . listRoots = vi . fn ( ) . mockResolvedValue ( {
321+ roots : [ { uri : pathToFileURL ( unindexedRoot ) . href , name : 'Unindexed' } ]
322+ } ) ;
323+
324+ try {
325+ await refreshKnownRootsFromClient ( ) ;
326+ expect ( indexerMocks . index ) . not . toHaveBeenCalled ( ) ;
327+ } finally {
328+ typedServer . listRoots = originalListRoots ;
329+ await fs . rm ( unindexedRoot , { recursive : true , force : true } ) ;
330+ }
331+ } ) ;
332+
261333 it ( 'supports explicit project routing without bootstrap roots when the client does not expose roots' , async ( ) => {
262334 delete process . env . CODEBASE_ROOT ;
263335 delete process . argv [ 2 ] ;
@@ -337,7 +409,9 @@ describe('multi-project routing', () => {
337409 expect ( payload . status ) . toBe ( 'success' ) ;
338410 expect ( payload . project . rootPath ) . toBe ( primaryRoot ) ;
339411 expect ( payload . project . project ) . toBe ( primaryRoot ) ;
340- expect ( watcherMocks . start ) . toHaveBeenCalledWith ( expect . objectContaining ( { rootPath : primaryRoot } ) ) ;
412+ expect ( watcherMocks . start ) . toHaveBeenCalledWith (
413+ expect . objectContaining ( { rootPath : primaryRoot } )
414+ ) ;
341415 } ) ;
342416
343417 it ( 'explicit project starts a watcher and makes that project active' , async ( ) => {
@@ -371,7 +445,8 @@ describe('multi-project routing', () => {
371445 query : 'feature' ,
372446 project : secondaryRoot
373447 } ) ;
374- const selectedProject = ( parsePayload ( selection ) as { project : { project : string } } ) . project . project ;
448+ const selectedProject = ( parsePayload ( selection ) as { project : { project : string } } ) . project
449+ . project ;
375450
376451 const response = await callTool ( handler , 7 , 'search_codebase' , { query : 'feature' } ) ;
377452 const payload = parsePayload ( response ) as {
@@ -383,13 +458,9 @@ describe('multi-project routing', () => {
383458 expect ( payload . status ) . toBe ( 'success' ) ;
384459 expect ( payload . project . project ) . toBe ( selectedProject ) ;
385460 expect ( payload . project . rootPath ) . toBe ( secondaryRoot ) ;
386- expect ( searchMocks . search ) . toHaveBeenCalledWith (
387- secondaryRoot ,
388- 'feature' ,
389- 5 ,
390- undefined ,
391- { profile : 'explore' }
392- ) ;
461+ expect ( searchMocks . search ) . toHaveBeenCalledWith ( secondaryRoot , 'feature' , 5 , undefined , {
462+ profile : 'explore'
463+ } ) ;
393464 expect ( payload . results [ 0 ] ?. file ) . toContain ( 'feature.ts' ) ;
394465 } ) ;
395466
@@ -501,7 +572,9 @@ describe('multi-project routing', () => {
501572 } ) ) as ResourceReadResponse ;
502573
503574 expect ( response . contents [ 0 ] ?. text ) . toContain ( '# Codebase Workspace' ) ;
504- expect ( response . contents [ 0 ] ?. text ) . toContain ( 'client-announced roots as the workspace boundary' ) ;
575+ expect ( response . contents [ 0 ] ?. text ) . toContain (
576+ 'client-announced roots as the workspace boundary'
577+ ) ;
505578 expect ( response . contents [ 0 ] ?. text ) . toContain ( 'codebase://context/project/' ) ;
506579 expect ( response . contents [ 0 ] ?. text ) . toContain ( 'retry tool calls with `project`' ) ;
507580 expect ( response . contents [ 0 ] ?. text ) . toContain ( 'apps/dashboard' ) ;
@@ -547,9 +620,7 @@ describe('multi-project routing', () => {
547620 params : { uri : buildProjectContextResourceUri ( payload . project . project ) }
548621 } ) ) as ResourceReadResponse ;
549622
550- expect ( response . contents [ 0 ] ?. uri ) . toBe (
551- buildProjectContextResourceUri ( payload . project . project )
552- ) ;
623+ expect ( response . contents [ 0 ] ?. uri ) . toBe ( buildProjectContextResourceUri ( payload . project . project ) ) ;
553624 expect ( response . contents [ 0 ] ?. text ) . toContain ( '# Codebase Intelligence' ) ;
554625 } ) ;
555626
0 commit comments