@@ -500,6 +500,13 @@ describe("ClineProvider", () => {
500500 let mockPostMessage : any
501501 let updateGlobalStateSpy : any
502502
503+ afterEach ( ( ) => {
504+ // Tests construct providers directly and many never dispose them; clear the
505+ // static registry so a leaked instance cannot claim stable view state ids
506+ // ahead of a later test.
507+ ClineProvider [ "activeInstances" ] = new Set ( )
508+ } )
509+
503510 beforeEach ( ( ) => {
504511 vi . clearAllMocks ( )
505512
@@ -2129,6 +2136,10 @@ describe("ClineProvider", () => {
21292136 } )
21302137 // @ts -ignore - Replace providerSettingsManager with a test double for the profile lookup.
21312138 provider . providerSettingsManager = { getProfile : getProfileSpy }
2139+ // The ownership check requires the writer to release the id before the
2140+ // next view adopts it; the durable entry it wrote stays in storage.
2141+ await writer . dispose ( )
2142+
21322143 await provider . contextProxy . setValue (
21332144 "viewStates" ,
21342145 mockContext . globalState . get < RooCodeSettings [ "viewStates" ] > ( "viewStates" ) ,
@@ -2141,7 +2152,6 @@ describe("ClineProvider", () => {
21412152 } )
21422153 expect ( getProfileSpy ) . toHaveBeenCalledWith ( { name : "my-profile" } )
21432154 expect ( logSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( "Loaded state for viewId" ) )
2144- await writer . dispose ( )
21452155 await provider . dispose ( )
21462156 } )
21472157
@@ -2269,6 +2279,10 @@ describe("ClineProvider", () => {
22692279 }
22702280 // A pre-load buffer write that was never persisted must not be merged over the load.
22712281 provider [ "viewLocalState" ] = { mode : "architect" }
2282+ // The ownership check requires the writer to release the id before the
2283+ // next view adopts it; the durable entry it wrote stays in storage.
2284+ await writer . dispose ( )
2285+
22722286 await provider . contextProxy . setValue (
22732287 "viewStates" ,
22742288 mockContext . globalState . get < RooCodeSettings [ "viewStates" ] > ( "viewStates" ) ,
@@ -2280,7 +2294,6 @@ describe("ClineProvider", () => {
22802294 apiProvider : providerIdentifiers . openrouter ,
22812295 openRouterModelId : "model-x" ,
22822296 } )
2283- await writer . dispose ( )
22842297 await provider . dispose ( )
22852298 } )
22862299
@@ -2671,6 +2684,10 @@ describe("ClineProvider", () => {
26712684 } ) ,
26722685 }
26732686 vi . spyOn ( provider , "postStateToWebview" ) . mockResolvedValue ( undefined )
2687+ // The ownership check requires the writer to release the id before the
2688+ // next view adopts it; the durable entry it wrote stays in storage.
2689+ await writer . dispose ( )
2690+
26742691 await provider . contextProxy . setValue (
26752692 "viewStates" ,
26762693 mockContext . globalState . get < RooCodeSettings [ "viewStates" ] > ( "viewStates" ) ,
@@ -2683,7 +2700,6 @@ describe("ClineProvider", () => {
26832700 // The buffer must track the activated profile so getValues() agrees with the proxy.
26842701 expect ( provider . getValues ( ) . currentApiConfigName ) . toBe ( "new-profile" )
26852702 expect ( provider . contextProxy . getValue ( "currentApiConfigName" ) ) . toBe ( "new-profile" )
2686- await writer . dispose ( )
26872703 await provider . dispose ( )
26882704 } )
26892705
@@ -6563,6 +6579,11 @@ describe("getTelemetryProperties", () => {
65636579 const { CloudService } = await import ( "@roo-code/cloud" )
65646580 const mockCloudService = {
65656581 isAuthenticated : vi . fn ( ) . mockReturnValue ( true ) ,
6582+
6583+ // Provider dispose detaches the settings listener; later tests construct
6584+ // and dispose providers against this surviving instance getter.
6585+ on : vi . fn ( ) ,
6586+ off : vi . fn ( ) ,
65666587 }
65676588
65686589 // Update the existing mock
@@ -6581,6 +6602,11 @@ describe("getTelemetryProperties", () => {
65816602 const { CloudService } = await import ( "@roo-code/cloud" )
65826603 const mockCloudService = {
65836604 isAuthenticated : vi . fn ( ) . mockReturnValue ( false ) ,
6605+
6606+ // Provider dispose detaches the settings listener; later tests construct
6607+ // and dispose providers against this surviving instance getter.
6608+ on : vi . fn ( ) ,
6609+ off : vi . fn ( ) ,
65846610 }
65856611
65866612 // Update the existing mock
@@ -6622,6 +6648,11 @@ describe("getTelemetryProperties", () => {
66226648 isAuthenticated : vi . fn ( ) . mockImplementation ( ( ) => {
66236649 throw new Error ( "Authentication check error" )
66246650 } ) ,
6651+
6652+ // Provider dispose detaches the settings listener; later tests construct
6653+ // and dispose providers against this surviving instance getter.
6654+ on : vi . fn ( ) ,
6655+ off : vi . fn ( ) ,
66256656 }
66266657
66276658 // Update the existing mock
@@ -8126,12 +8157,15 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
81268157 } ) ,
81278158 true ,
81288159 )
8129- expect ( saveConfig ) . toHaveBeenCalledWith (
8160+ // The non-active profile now goes through upsertProviderProfile so the views
8161+ // pinned to it are refreshed, not just the file on disk.
8162+ expect ( upsertSpy ) . toHaveBeenCalledWith (
81308163 "Backup Zoo" ,
81318164 expect . objectContaining ( {
81328165 zooSessionToken : "new-token" ,
81338166 zooGatewayBaseUrl : "https://www.zoocode.dev/api/gateway/v1" ,
81348167 } ) ,
8168+ false ,
81358169 )
81368170 } )
81378171
@@ -8228,4 +8262,80 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
82288262 } )
82298263 } )
82308264 } )
8265+
8266+ describe ( "profile save without activation and overlay completeness" , ( ) => {
8267+ it ( "refreshes views pinned to a profile saved without activation" , async ( ) => {
8268+ // Defensive: the doubles in this suite can leave console writes pending
8269+ // at worker teardown.
8270+ vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
8271+ vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
8272+ vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
8273+ const sibling = new ClineProvider (
8274+ mockContext ,
8275+ mockOutputChannel ,
8276+ "editor" ,
8277+ new ContextProxy ( mockContext ) ,
8278+ new WebviewFocusTracker ( ) ,
8279+ )
8280+ try {
8281+ // The sibling loaded the profile earlier: its buffer pins the name and
8282+ // carries the settings snapshot taken at that load.
8283+ sibling [ "viewLocalState" ] . currentApiConfigName = "saved-profile"
8284+ sibling [ "viewLocalState" ] . apiConfiguration = {
8285+ apiProvider : providerIdentifiers . anthropic ,
8286+ apiKey : "old-key" ,
8287+ }
8288+ vi . spyOn ( sibling , "postStateToWebview" ) . mockResolvedValue ( undefined )
8289+ vi . spyOn ( provider , "postStateToWebview" ) . mockResolvedValue ( undefined )
8290+ // @ts -ignore - Replace providerSettingsManager with a test double.
8291+ provider . providerSettingsManager = {
8292+ saveConfig : vi . fn ( ) . mockResolvedValue ( "saved-id" ) ,
8293+ listConfig : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
8294+ setModeConfig : vi . fn ( ) ,
8295+ }
8296+
8297+ await provider . upsertProviderProfile (
8298+ "saved-profile" ,
8299+ { apiProvider : providerIdentifiers . anthropic , apiKey : "new-key" } ,
8300+ false ,
8301+ )
8302+
8303+ // A save that does not activate still rewrites the profile; the pinned
8304+ // sibling must not keep serving the pre-save snapshot.
8305+ expect ( sibling [ "viewLocalState" ] . apiConfiguration ) . toEqual ( {
8306+ apiProvider : providerIdentifiers . anthropic ,
8307+ apiKey : "new-key" ,
8308+ } )
8309+ } finally {
8310+ await sibling . dispose ( )
8311+ }
8312+ } )
8313+
8314+ it ( "does not leak shared provider keys into a pinned view's api configuration" , async ( ) => {
8315+ vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
8316+ vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
8317+ // The shared store sits on a profile with keys the pinned profile never
8318+ // sets; spreading the overlay over the shared keys would report a mixed
8319+ // configuration (e.g. another profile's model id and region).
8320+ await provider . contextProxy . setValue ( "apiProvider" , providerIdentifiers . bedrock )
8321+ await provider . contextProxy . setValue ( "apiModelId" , "anthropic.claude-v1" )
8322+ await provider . contextProxy . setValue ( "awsRegion" , "us-east-1" )
8323+ provider [ "viewLocalState" ] . currentApiConfigName = "pinned-profile"
8324+ provider [ "viewLocalState" ] . apiConfiguration = {
8325+ apiProvider : providerIdentifiers . anthropic ,
8326+ apiKey : "k" ,
8327+ }
8328+
8329+ // The cloud doubles in this suite make getState() log its defensive catch
8330+ // branches; silence them so teardown has no pending console writes.
8331+ vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
8332+
8333+ const state = await provider . getState ( )
8334+
8335+ expect ( state . apiConfiguration . apiProvider ) . toBe ( providerIdentifiers . anthropic )
8336+ expect ( state . apiConfiguration . apiKey ) . toBe ( "k" )
8337+ expect ( state . apiConfiguration . apiModelId ) . toBeUndefined ( )
8338+ expect ( state . apiConfiguration . awsRegion ) . toBeUndefined ( )
8339+ } )
8340+ } )
82318341} )
0 commit comments