@@ -25,7 +25,12 @@ import {
2525 type LoadedTrustedFolders ,
2626} from '../../config/trustedFolders.js' ;
2727import * as trustedFolders from '../../config/trustedFolders.js' ;
28- import { coreEvents , ExitCodes , isHeadlessMode } from '@google/gemini-cli-core' ;
28+ import {
29+ coreEvents ,
30+ ExitCodes ,
31+ isHeadlessMode ,
32+ FolderTrustDiscoveryService ,
33+ } from '@google/gemini-cli-core' ;
2934import { MessageType } from '../types.js' ;
3035
3136const mockedCwd = vi . hoisted ( ( ) => vi . fn ( ) . mockReturnValue ( '/mock/cwd' ) ) ;
@@ -366,7 +371,7 @@ describe('useFolderTrust', () => {
366371 } ) ;
367372
368373 describe ( 'headless mode' , ( ) => {
369- it ( 'should force trust and hide dialog in headless mode ' , async ( ) => {
374+ it ( 'should propagate false to onTrustChange, hide dialog, and show warning when folder is untrusted ' , async ( ) => {
370375 vi . mocked ( isHeadlessMode ) . mockReturnValue ( true ) ;
371376 isWorkspaceTrustedSpy . mockReturnValue ( {
372377 isTrusted : false ,
@@ -378,7 +383,8 @@ describe('useFolderTrust', () => {
378383 ) ;
379384
380385 expect ( result . current . isFolderTrustDialogOpen ) . toBe ( false ) ;
381- expect ( onTrustChange ) . toHaveBeenCalledWith ( true ) ;
386+ expect ( result . current . isTrusted ) . toBe ( false ) ;
387+ expect ( onTrustChange ) . toHaveBeenCalledWith ( false ) ;
382388 expect ( addItem ) . toHaveBeenCalledWith (
383389 expect . objectContaining ( {
384390 type : MessageType . INFO ,
@@ -387,5 +393,255 @@ describe('useFolderTrust', () => {
387393 expect . any ( Number ) ,
388394 ) ;
389395 } ) ;
396+
397+ it ( 'should propagate true to onTrustChange, hide dialog, and not show warning when folder is trusted' , async ( ) => {
398+ vi . mocked ( isHeadlessMode ) . mockReturnValue ( true ) ;
399+ isWorkspaceTrustedSpy . mockReturnValue ( {
400+ isTrusted : true ,
401+ source : 'file' ,
402+ } ) ;
403+
404+ const { result } = await renderHook ( ( ) =>
405+ useFolderTrust ( mockSettings , onTrustChange , addItem ) ,
406+ ) ;
407+
408+ expect ( result . current . isFolderTrustDialogOpen ) . toBe ( false ) ;
409+ expect ( result . current . isTrusted ) . toBe ( true ) ;
410+ expect ( onTrustChange ) . toHaveBeenCalledWith ( true ) ;
411+ expect ( addItem ) . not . toHaveBeenCalled ( ) ;
412+ } ) ;
413+
414+ it ( 'should propagate undefined to onTrustChange and hide dialog when folder trust is undefined' , async ( ) => {
415+ vi . mocked ( isHeadlessMode ) . mockReturnValue ( true ) ;
416+ isWorkspaceTrustedSpy . mockReturnValue ( {
417+ isTrusted : undefined ,
418+ source : undefined ,
419+ } ) ;
420+
421+ const { result } = await renderHook ( ( ) =>
422+ useFolderTrust ( mockSettings , onTrustChange , addItem ) ,
423+ ) ;
424+
425+ expect ( result . current . isFolderTrustDialogOpen ) . toBe ( false ) ;
426+ expect ( result . current . isTrusted ) . toBeUndefined ( ) ;
427+ expect ( onTrustChange ) . toHaveBeenCalledWith ( undefined ) ;
428+ expect ( addItem ) . not . toHaveBeenCalled ( ) ;
429+ } ) ;
430+ } ) ;
431+
432+ describe ( 'callback stability' , ( ) => {
433+ it ( 'should not re-run effect or trigger onTrustChange again when callback references change' , async ( ) => {
434+ isWorkspaceTrustedSpy . mockReturnValue ( {
435+ isTrusted : true ,
436+ source : 'file' ,
437+ } ) ;
438+
439+ const initialOnTrustChange = vi . fn ( ) ;
440+ const initialAddItem = vi . fn ( ) ;
441+
442+ const { rerender } = await renderHook (
443+ ( { onTrustChangeCb, addItemCb } ) =>
444+ useFolderTrust ( mockSettings , onTrustChangeCb , addItemCb ) ,
445+ {
446+ initialProps : {
447+ onTrustChangeCb : initialOnTrustChange ,
448+ addItemCb : initialAddItem ,
449+ } ,
450+ } ,
451+ ) ;
452+
453+ expect ( initialOnTrustChange ) . toHaveBeenCalledTimes ( 1 ) ;
454+ expect ( initialOnTrustChange ) . toHaveBeenCalledWith ( true ) ;
455+
456+ const newOnTrustChange = vi . fn ( ) ;
457+ const newAddItem = vi . fn ( ) ;
458+
459+ rerender ( {
460+ onTrustChangeCb : newOnTrustChange ,
461+ addItemCb : newAddItem ,
462+ } ) ;
463+
464+ expect ( newOnTrustChange ) . not . toHaveBeenCalled ( ) ;
465+ expect ( initialOnTrustChange ) . toHaveBeenCalledTimes ( 1 ) ;
466+ } ) ;
467+
468+ it ( 'should not re-trigger FolderTrustDiscoveryService.discover when callback references change' , async ( ) => {
469+ isWorkspaceTrustedSpy . mockReturnValue ( {
470+ isTrusted : false ,
471+ source : 'file' ,
472+ } ) ;
473+ const discoverSpy = vi . spyOn ( FolderTrustDiscoveryService , 'discover' ) ;
474+ discoverSpy . mockClear ( ) ;
475+
476+ const initialOnTrustChange = vi . fn ( ) ;
477+ const initialAddItem = vi . fn ( ) ;
478+
479+ const { rerender } = await renderHook (
480+ ( { onTrustChangeCb, addItemCb } ) =>
481+ useFolderTrust ( mockSettings , onTrustChangeCb , addItemCb ) ,
482+ {
483+ initialProps : {
484+ onTrustChangeCb : initialOnTrustChange ,
485+ addItemCb : initialAddItem ,
486+ } ,
487+ } ,
488+ ) ;
489+
490+ expect ( discoverSpy ) . toHaveBeenCalledTimes ( 1 ) ;
491+
492+ const newOnTrustChange = vi . fn ( ) ;
493+ const newAddItem = vi . fn ( ) ;
494+
495+ rerender ( {
496+ onTrustChangeCb : newOnTrustChange ,
497+ addItemCb : newAddItem ,
498+ } ) ;
499+
500+ expect ( discoverSpy ) . toHaveBeenCalledTimes ( 1 ) ;
501+ } ) ;
502+
503+ it ( 'should use updated onTrustChange callback in handleFolderTrustSelect when callback reference changes' , async ( ) => {
504+ isWorkspaceTrustedSpy . mockReturnValue ( {
505+ isTrusted : undefined ,
506+ source : undefined ,
507+ } ) ;
508+
509+ const initialOnTrustChange = vi . fn ( ) ;
510+ const initialAddItem = vi . fn ( ) ;
511+
512+ const { result, rerender } = await renderHook (
513+ ( { onTrustChangeCb, addItemCb } ) =>
514+ useFolderTrust ( mockSettings , onTrustChangeCb , addItemCb ) ,
515+ {
516+ initialProps : {
517+ onTrustChangeCb : initialOnTrustChange ,
518+ addItemCb : initialAddItem ,
519+ } ,
520+ } ,
521+ ) ;
522+
523+ expect ( initialOnTrustChange ) . toHaveBeenCalledWith ( undefined ) ;
524+
525+ const newOnTrustChange = vi . fn ( ) ;
526+ const newAddItem = vi . fn ( ) ;
527+
528+ rerender ( {
529+ onTrustChangeCb : newOnTrustChange ,
530+ addItemCb : newAddItem ,
531+ } ) ;
532+
533+ await act ( async ( ) => {
534+ await result . current . handleFolderTrustSelect (
535+ FolderTrustChoice . TRUST_FOLDER ,
536+ ) ;
537+ } ) ;
538+
539+ expect ( newOnTrustChange ) . toHaveBeenCalledWith ( true ) ;
540+ expect ( initialOnTrustChange ) . not . toHaveBeenCalledWith ( true ) ;
541+ } ) ;
542+
543+ it ( 'should not re-run discovery effect when unrelated settings change' , async ( ) => {
544+ isWorkspaceTrustedSpy . mockReturnValue ( {
545+ isTrusted : true ,
546+ source : 'file' ,
547+ } ) ;
548+
549+ const onTrustChangeCb = vi . fn ( ) ;
550+ const addItemCb = vi . fn ( ) ;
551+
552+ const initialSettings = {
553+ merged : {
554+ security : {
555+ folderTrust : {
556+ enabled : true ,
557+ } ,
558+ } ,
559+ ui : {
560+ theme : 'default' ,
561+ } ,
562+ } ,
563+ setValue : vi . fn ( ) ,
564+ } as unknown as LoadedSettings ;
565+
566+ const { rerender } = await renderHook (
567+ ( { settings } ) => useFolderTrust ( settings , onTrustChangeCb , addItemCb ) ,
568+ {
569+ initialProps : {
570+ settings : initialSettings ,
571+ } ,
572+ } ,
573+ ) ;
574+
575+ expect ( onTrustChangeCb ) . toHaveBeenCalledTimes ( 1 ) ;
576+
577+ const updatedSettings = {
578+ merged : {
579+ security : {
580+ folderTrust : {
581+ enabled : true ,
582+ } ,
583+ } ,
584+ ui : {
585+ theme : 'dark' ,
586+ } ,
587+ } ,
588+ setValue : vi . fn ( ) ,
589+ } as unknown as LoadedSettings ;
590+
591+ rerender ( {
592+ settings : updatedSettings ,
593+ } ) ;
594+
595+ expect ( onTrustChangeCb ) . toHaveBeenCalledTimes ( 1 ) ;
596+ } ) ;
597+
598+ it ( 'should re-run discovery effect when folderTrust setting changes' , async ( ) => {
599+ isWorkspaceTrustedSpy . mockReturnValue ( {
600+ isTrusted : true ,
601+ source : 'file' ,
602+ } ) ;
603+
604+ const onTrustChangeCb = vi . fn ( ) ;
605+ const addItemCb = vi . fn ( ) ;
606+
607+ const initialSettings = {
608+ merged : {
609+ security : {
610+ folderTrust : {
611+ enabled : true ,
612+ } ,
613+ } ,
614+ } ,
615+ setValue : vi . fn ( ) ,
616+ } as unknown as LoadedSettings ;
617+
618+ const { rerender } = await renderHook (
619+ ( { settings } ) => useFolderTrust ( settings , onTrustChangeCb , addItemCb ) ,
620+ {
621+ initialProps : {
622+ settings : initialSettings ,
623+ } ,
624+ } ,
625+ ) ;
626+
627+ expect ( onTrustChangeCb ) . toHaveBeenCalledTimes ( 1 ) ;
628+
629+ const updatedSettings = {
630+ merged : {
631+ security : {
632+ folderTrust : {
633+ enabled : false ,
634+ } ,
635+ } ,
636+ } ,
637+ setValue : vi . fn ( ) ,
638+ } as unknown as LoadedSettings ;
639+
640+ rerender ( {
641+ settings : updatedSettings ,
642+ } ) ;
643+
644+ expect ( onTrustChangeCb ) . toHaveBeenCalledTimes ( 2 ) ;
645+ } ) ;
390646 } ) ;
391647} ) ;
0 commit comments