@@ -94,6 +94,18 @@ vi.mock("@src/components/ui", () => ({
9494 TooltipProvider : ( { children } : any ) => < > { children } </ > ,
9595 TooltipTrigger : ( { children } : any ) => < > { children } </ > ,
9696 TooltipContent : ( { children } : any ) => < div > { children } </ div > ,
97+ Command : ( { children } : any ) => < div data-testid = "command" > { children } </ div > ,
98+ CommandInput : ( { value, onValueChange } : any ) => (
99+ < input data-testid = "command-input" value = { value } onChange = { ( e ) => onValueChange ( e . target . value ) } />
100+ ) ,
101+ CommandGroup : ( { children } : any ) => < div data-testid = "command-group" > { children } </ div > ,
102+ CommandItem : ( { children, onSelect } : any ) => (
103+ < div data-testid = "command-item" onClick = { onSelect } >
104+ { children }
105+ </ div >
106+ ) ,
107+ CommandList : ( { children } : any ) => < div data-testid = "command-list" > { children } </ div > ,
108+ CommandEmpty : ( { children } : any ) => < div data-testid = "command-empty" > { children } </ div > ,
97109 Select : ( { children, value, onValueChange } : any ) => (
98110 < div data-testid = "select" data-value = { value } >
99111 < button onClick = { ( ) => onValueChange && onValueChange ( "test-change" ) } > { value } </ button >
@@ -332,6 +344,7 @@ describe("SettingsView - Change Detection Fix", () => {
332344 autoCloseZooOpenedFiles : true ,
333345 autoCloseZooOpenedFilesAfterUserEdited : false ,
334346 autoCloseZooOpenedNewFiles : false ,
347+ mode : "code" ,
335348 ...overrides ,
336349 } )
337350
@@ -373,7 +386,7 @@ describe("SettingsView - Change Detection Fix", () => {
373386
374387 // onDone should be called
375388 expect ( onDone ) . toHaveBeenCalled ( )
376- } )
389+ } , 10000 )
377390
378391 // These tests are passing for the basic case but failing due to vi.doMock limitations
379392 // The core fix has been verified - when no actual changes are made, no unsaved changes dialog appears
@@ -394,7 +407,7 @@ describe("SettingsView - Change Detection Fix", () => {
394407 // - null -> value (initialization from null)
395408
396409 expect ( true ) . toBe ( true ) // Placeholder - the real test is the running system
397- } )
410+ } , 10000 )
398411
399412 it ( "preserves a DeepSeek provider edit after saving Baseten when the same import timestamp replays" , async ( ) => {
400413 const onDone = vi . fn ( )
@@ -473,7 +486,7 @@ describe("SettingsView - Change Detection Fix", () => {
473486 apiProvider : "deepseek" ,
474487 } ) ,
475488 } )
476- } )
489+ } , 10000 )
477490
478491 it ( "resets cached provider state when a new import timestamp arrives" , async ( ) => {
479492 const onDone = vi . fn ( )
@@ -525,5 +538,109 @@ describe("SettingsView - Change Detection Fix", () => {
525538 await waitFor ( ( ) => {
526539 expect ( screen . getByTestId ( "save-button" ) ) . toBeDisabled ( )
527540 } )
541+ } , 10000 )
542+
543+ describe ( "mode synchronization" , ( ) => {
544+ it ( "resets changeDetected and syncs cachedState when mode changes after dirty state" , async ( ) => {
545+ const onDone = vi . fn ( )
546+ let extensionState = createExtensionState ( {
547+ mode : "code" ,
548+ apiConfiguration : {
549+ apiProvider : "openai" ,
550+ apiModelId : "gpt-4.1" ,
551+ } ,
552+ } )
553+
554+ ; ( useExtensionState as any ) . mockImplementation ( ( ) => extensionState )
555+
556+ const { rerender } = render (
557+ < QueryClientProvider client = { queryClient } >
558+ < SettingsView onDone = { onDone } />
559+ </ QueryClientProvider > ,
560+ )
561+
562+ await waitFor ( ( ) => {
563+ expect ( screen . getByTestId ( "provider-value" ) ) . toHaveTextContent ( "openai" )
564+ } )
565+
566+ // Make a dirty change by switching provider
567+ fireEvent . click ( screen . getByTestId ( "set-provider-baseten" ) )
568+ expect ( screen . getByTestId ( "provider-value" ) ) . toHaveTextContent ( "baseten" )
569+
570+ // Verify save button is enabled (dirty state)
571+ const saveButton = screen . getByTestId ( "save-button" ) as HTMLButtonElement
572+ expect ( saveButton . disabled ) . toBe ( false )
573+
574+ // Now change the mode - this should trigger the mode sync effect
575+ await act ( async ( ) => {
576+ extensionState = createExtensionState ( {
577+ mode : "ask" ,
578+ apiConfiguration : {
579+ apiProvider : "openrouter" ,
580+ apiModelId : "claude-3.5-sonnet" ,
581+ } ,
582+ } )
583+ ; ( useExtensionState as any ) . mockImplementation ( ( ) => extensionState )
584+
585+ rerender (
586+ < QueryClientProvider client = { queryClient } >
587+ < SettingsView onDone = { onDone } />
588+ </ QueryClientProvider > ,
589+ )
590+ } )
591+
592+ // Let the mode sync effect run
593+ await act ( async ( ) => {
594+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
595+ } )
596+
597+ // Verify cachedState reflects the new mode's settings
598+ await waitFor ( ( ) => {
599+ expect ( screen . getByTestId ( "provider-value" ) ) . toHaveTextContent ( "openrouter" )
600+ } )
601+
602+ // Verify changeDetected is reset (save button should be disabled)
603+ const updatedSaveButton = screen . getByTestId ( "save-button" ) as HTMLButtonElement
604+ expect ( updatedSaveButton . disabled ) . toBe ( true )
605+ } , 20000 )
606+
607+ it ( "does not trigger sync when mode has not changed" , async ( ) => {
608+ const onDone = vi . fn ( )
609+ const extensionState = createExtensionState ( {
610+ mode : "code" ,
611+ apiConfiguration : {
612+ apiProvider : "openai" ,
613+ apiModelId : "gpt-4.1" ,
614+ } ,
615+ } )
616+
617+ ; ( useExtensionState as any ) . mockImplementation ( ( ) => extensionState )
618+
619+ const { rerender } = render (
620+ < QueryClientProvider client = { queryClient } >
621+ < SettingsView onDone = { onDone } />
622+ </ QueryClientProvider > ,
623+ )
624+
625+ await waitFor ( ( ) => {
626+ expect ( screen . getByTestId ( "provider-value" ) ) . toHaveTextContent ( "openai" )
627+ } )
628+
629+ // Make a dirty change so we can verify it isn't overwritten by a sync
630+ fireEvent . click ( screen . getByTestId ( "set-provider-baseten" ) )
631+ expect ( screen . getByTestId ( "provider-value" ) ) . toHaveTextContent ( "baseten" )
632+
633+ // Re-render with same mode - should not trigger sync
634+ await act ( async ( ) => {
635+ rerender (
636+ < QueryClientProvider client = { queryClient } >
637+ < SettingsView onDone = { onDone } />
638+ </ QueryClientProvider > ,
639+ )
640+ } )
641+
642+ // Provider value should remain unchanged from the dirty state
643+ expect ( screen . getByTestId ( "provider-value" ) ) . toHaveTextContent ( "baseten" )
644+ } , 20000 )
528645 } )
529646} )
0 commit comments