@@ -1865,8 +1865,8 @@ describe("ClineProvider", () => {
18651865 const pruned = provider [ "prunePersistedViewStates" ] ( states )
18661866
18671867 expect ( Object . keys ( pruned ) ) . toHaveLength ( 50 )
1868- expect ( pruned [ "view-54" ] ) . toBeDefined ( )
1869- expect ( pruned [ "view-5" ] ) . toBeDefined ( )
1868+ expect ( pruned [ "view-54" ] ) . toEqual ( { mode : "mode-54" , updatedAt : 54 } )
1869+ expect ( pruned [ "view-5" ] ) . toEqual ( { mode : "mode-5" , updatedAt : 5 } )
18701870 expect ( pruned [ "view-4" ] ) . toBeUndefined ( )
18711871
18721872 await provider . dispose ( )
@@ -8307,6 +8307,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
83078307 // minimal double without the full provider/services graph.
83088308 const siblingTask = {
83098309 apiConfiguration : { apiProvider : providerIdentifiers . anthropic , apiKey : "old-key" } ,
8310+ taskApiConfigName : "saved-profile" ,
83108311 updateApiConfiguration : vi . fn ( ) ,
83118312 }
83128313 vi . spyOn ( sibling , "getCurrentTask" ) . mockReturnValue (
@@ -8351,6 +8352,101 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
83518352 }
83528353 } )
83538354
8355+ it ( "does not rebuild a pinned sibling's task that runs under another profile" , async ( ) => {
8356+ // Defensive: the doubles in this suite can leave console writes pending
8357+ // at worker teardown.
8358+ vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
8359+ vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
8360+ vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
8361+ const sibling = new ClineProvider (
8362+ mockContext ,
8363+ mockOutputChannel ,
8364+ "editor" ,
8365+ new ContextProxy ( mockContext ) ,
8366+ new WebviewFocusTracker ( ) ,
8367+ )
8368+ try {
8369+ // The sibling pins the saved profile, but its active task follows a
8370+ // different sticky profile: the save must refresh the view overlay
8371+ // without switching that conversation to the saved profile's settings.
8372+ sibling [ "viewLocalState" ] . currentApiConfigName = "saved-profile"
8373+ sibling [ "viewLocalState" ] . apiConfiguration = {
8374+ apiProvider : providerIdentifiers . anthropic ,
8375+ apiKey : "old-key" ,
8376+ }
8377+ vi . spyOn ( sibling , "postStateToWebview" ) . mockResolvedValue ( undefined )
8378+
8379+ const siblingTask = {
8380+ apiConfiguration : { apiProvider : providerIdentifiers . anthropic , apiKey : "other-key" } ,
8381+ taskApiConfigName : "other-profile" ,
8382+ updateApiConfiguration : vi . fn ( ) ,
8383+ }
8384+ vi . spyOn ( sibling , "getCurrentTask" ) . mockReturnValue (
8385+ siblingTask as unknown as ReturnType < typeof sibling . getCurrentTask > ,
8386+ )
8387+ vi . spyOn ( provider , "postStateToWebview" ) . mockResolvedValue ( undefined )
8388+ // @ts -ignore - Replace providerSettingsManager with a test double.
8389+ provider . providerSettingsManager = {
8390+ saveConfig : vi . fn ( ) . mockResolvedValue ( "saved-id" ) ,
8391+ listConfig : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
8392+ setModeConfig : vi . fn ( ) ,
8393+ }
8394+
8395+ await provider . upsertProviderProfile (
8396+ "saved-profile" ,
8397+ { apiProvider : providerIdentifiers . anthropic , apiKey : "new-key" } ,
8398+ false ,
8399+ )
8400+
8401+ // The pinned view's overlay still catches up with the saved settings...
8402+ expect ( sibling [ "viewLocalState" ] . apiConfiguration ) . toEqual ( {
8403+ apiProvider : providerIdentifiers . anthropic ,
8404+ apiKey : "new-key" ,
8405+ } )
8406+ // ...but the task runs under another profile: its handler must not be
8407+ // switched to the saved profile's settings.
8408+ expect ( siblingTask . updateApiConfiguration ) . not . toHaveBeenCalled ( )
8409+ // Let queued view-local persistence settle while the console spies are
8410+ // still installed, so teardown has no pending writes.
8411+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) )
8412+ } finally {
8413+ await sibling . dispose ( )
8414+ }
8415+ } )
8416+
8417+ it ( "does not rebuild the acting view's task when it runs under another profile" , async ( ) => {
8418+ // The acting view pins the saved profile while its own task follows a
8419+ // different sticky profile: the non-activating save refreshes the view
8420+ // overlay but must leave the task's handler alone.
8421+ const task = {
8422+ apiConfiguration : { apiProvider : providerIdentifiers . anthropic , apiKey : "other-key" } ,
8423+ taskApiConfigName : "other-profile" ,
8424+ updateApiConfiguration : vi . fn ( ) ,
8425+ }
8426+ vi . spyOn ( provider , "getCurrentTask" ) . mockReturnValue (
8427+ task as unknown as ReturnType < typeof provider . getCurrentTask > ,
8428+ )
8429+ vi . spyOn ( provider , "postStateToWebview" ) . mockResolvedValue ( undefined )
8430+ provider [ "viewLocalState" ] . currentApiConfigName = "saved-profile"
8431+ // @ts -ignore - Replace providerSettingsManager with a test double.
8432+ provider . providerSettingsManager = {
8433+ saveConfig : vi . fn ( ) . mockResolvedValue ( "saved-id" ) ,
8434+ listConfig : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
8435+ setModeConfig : vi . fn ( ) ,
8436+ }
8437+
8438+ await provider . upsertProviderProfile (
8439+ "saved-profile" ,
8440+ { apiProvider : providerIdentifiers . anthropic , apiKey : "new-key" } ,
8441+ false ,
8442+ )
8443+
8444+ expect ( provider [ "viewLocalState" ] . apiConfiguration ) . toEqual ( {
8445+ apiProvider : providerIdentifiers . anthropic ,
8446+ apiKey : "new-key" ,
8447+ } )
8448+ expect ( task . updateApiConfiguration ) . not . toHaveBeenCalled ( )
8449+ } )
83548450 it ( "does not leak shared provider keys into a pinned view's api configuration" , async ( ) => {
83558451 vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
83568452 vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
0 commit comments