@@ -501,6 +501,13 @@ describe("ClineProvider", () => {
501501 let mockPostMessage : any
502502 let updateGlobalStateSpy : any
503503
504+ afterEach ( ( ) => {
505+ // Tests construct providers directly and many never dispose them; clear the
506+ // static registry so a leaked instance cannot claim stable view state ids
507+ // ahead of a later test.
508+ ClineProvider [ "activeInstances" ] = new Set ( )
509+ } )
510+
504511 beforeEach ( ( ) => {
505512 vi . clearAllMocks ( )
506513
@@ -2351,6 +2358,10 @@ describe("ClineProvider", () => {
23512358 } )
23522359 // @ts -ignore - Replace providerSettingsManager with a test double for the profile lookup.
23532360 provider . providerSettingsManager = { getProfile : getProfileSpy }
2361+ // The ownership check requires the writer to release the id before the
2362+ // next view adopts it; the durable entry it wrote stays in storage.
2363+ await writer . dispose ( )
2364+
23542365 await provider . contextProxy . setValue (
23552366 "viewStates" ,
23562367 mockContext . globalState . get < RooCodeSettings [ "viewStates" ] > ( "viewStates" ) ,
@@ -2363,7 +2374,6 @@ describe("ClineProvider", () => {
23632374 } )
23642375 expect ( getProfileSpy ) . toHaveBeenCalledWith ( { name : "my-profile" } )
23652376 expect ( logSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( "Loaded state for viewId" ) )
2366- await writer . dispose ( )
23672377 await provider . dispose ( )
23682378 } )
23692379
@@ -2491,6 +2501,10 @@ describe("ClineProvider", () => {
24912501 }
24922502 // A pre-load buffer write that was never persisted must not be merged over the load.
24932503 provider [ "viewLocalState" ] = { mode : "architect" }
2504+ // The ownership check requires the writer to release the id before the
2505+ // next view adopts it; the durable entry it wrote stays in storage.
2506+ await writer . dispose ( )
2507+
24942508 await provider . contextProxy . setValue (
24952509 "viewStates" ,
24962510 mockContext . globalState . get < RooCodeSettings [ "viewStates" ] > ( "viewStates" ) ,
@@ -2502,7 +2516,6 @@ describe("ClineProvider", () => {
25022516 apiProvider : providerIdentifiers . openrouter ,
25032517 openRouterModelId : "model-x" ,
25042518 } )
2505- await writer . dispose ( )
25062519 await provider . dispose ( )
25072520 } )
25082521
@@ -2892,6 +2905,10 @@ describe("ClineProvider", () => {
28922905 } ) ,
28932906 }
28942907 vi . spyOn ( provider , "postStateToWebview" ) . mockResolvedValue ( undefined )
2908+ // The ownership check requires the writer to release the id before the
2909+ // next view adopts it; the durable entry it wrote stays in storage.
2910+ await writer . dispose ( )
2911+
28952912 await provider . contextProxy . setValue (
28962913 "viewStates" ,
28972914 mockContext . globalState . get < RooCodeSettings [ "viewStates" ] > ( "viewStates" ) ,
@@ -2904,7 +2921,6 @@ describe("ClineProvider", () => {
29042921 // The buffer must track the activated profile so getValues() agrees with the proxy.
29052922 expect ( provider . getValues ( ) . currentApiConfigName ) . toBe ( "new-profile" )
29062923 expect ( provider . contextProxy . getValue ( "currentApiConfigName" ) ) . toBe ( "new-profile" )
2907- await writer . dispose ( )
29082924 await provider . dispose ( )
29092925 } )
29102926
@@ -6997,6 +7013,11 @@ describe("getTelemetryProperties", () => {
69977013 const { CloudService } = await import ( "@roo-code/cloud" )
69987014 const mockCloudService = {
69997015 isAuthenticated : vi . fn ( ) . mockReturnValue ( true ) ,
7016+
7017+ // Provider dispose detaches the settings listener; later tests construct
7018+ // and dispose providers against this surviving instance getter.
7019+ on : vi . fn ( ) ,
7020+ off : vi . fn ( ) ,
70007021 }
70017022
70027023 // Update the existing mock
@@ -7015,6 +7036,11 @@ describe("getTelemetryProperties", () => {
70157036 const { CloudService } = await import ( "@roo-code/cloud" )
70167037 const mockCloudService = {
70177038 isAuthenticated : vi . fn ( ) . mockReturnValue ( false ) ,
7039+
7040+ // Provider dispose detaches the settings listener; later tests construct
7041+ // and dispose providers against this surviving instance getter.
7042+ on : vi . fn ( ) ,
7043+ off : vi . fn ( ) ,
70187044 }
70197045
70207046 // Update the existing mock
@@ -7056,6 +7082,11 @@ describe("getTelemetryProperties", () => {
70567082 isAuthenticated : vi . fn ( ) . mockImplementation ( ( ) => {
70577083 throw new Error ( "Authentication check error" )
70587084 } ) ,
7085+
7086+ // Provider dispose detaches the settings listener; later tests construct
7087+ // and dispose providers against this surviving instance getter.
7088+ on : vi . fn ( ) ,
7089+ off : vi . fn ( ) ,
70597090 }
70607091
70617092 // Update the existing mock
@@ -8560,12 +8591,15 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
85608591 } ) ,
85618592 true ,
85628593 )
8563- expect ( saveConfig ) . toHaveBeenCalledWith (
8594+ // The non-active profile now goes through upsertProviderProfile so the views
8595+ // pinned to it are refreshed, not just the file on disk.
8596+ expect ( upsertSpy ) . toHaveBeenCalledWith (
85648597 "Backup Zoo" ,
85658598 expect . objectContaining ( {
85668599 zooSessionToken : "new-token" ,
85678600 zooGatewayBaseUrl : "https://www.zoocode.dev/api/gateway/v1" ,
85688601 } ) ,
8602+ false ,
85698603 )
85708604 } )
85718605
@@ -8662,4 +8696,85 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
86628696 } )
86638697 } )
86648698 } )
8699+ describe ( "profile save without activation and overlay completeness" , ( ) => {
8700+ it ( "refreshes views pinned to a profile saved without activation" , async ( ) => {
8701+ // Defensive: the doubles in this suite can leave console writes pending
8702+ // at worker teardown.
8703+ vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
8704+ vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
8705+ vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
8706+ const sibling = new ClineProvider (
8707+ mockContext ,
8708+ mockOutputChannel ,
8709+ "editor" ,
8710+ new ContextProxy ( mockContext ) ,
8711+ new WebviewFocusTracker ( ) ,
8712+ )
8713+ try {
8714+ // The sibling loaded the profile earlier: its buffer pins the name and
8715+ // carries the settings snapshot taken at that load.
8716+ sibling [ "viewLocalState" ] . currentApiConfigName = "saved-profile"
8717+ sibling [ "viewLocalState" ] . apiConfiguration = {
8718+ apiProvider : providerIdentifiers . anthropic ,
8719+ apiKey : "old-key" ,
8720+ }
8721+ vi . spyOn ( sibling , "postStateToWebview" ) . mockResolvedValue ( undefined )
8722+ vi . spyOn ( provider , "postStateToWebview" ) . mockResolvedValue ( undefined )
8723+ // @ts -ignore - Replace providerSettingsManager with a test double.
8724+ provider . providerSettingsManager = {
8725+ saveConfig : vi . fn ( ) . mockResolvedValue ( "saved-id" ) ,
8726+ listConfig : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
8727+ setModeConfig : vi . fn ( ) ,
8728+ }
8729+
8730+ await provider . upsertProviderProfile (
8731+ "saved-profile" ,
8732+ { apiProvider : providerIdentifiers . anthropic , apiKey : "new-key" } ,
8733+ false ,
8734+ )
8735+
8736+ // A save that does not activate still rewrites the profile; the pinned
8737+ // sibling must not keep serving the pre-save snapshot.
8738+ expect ( sibling [ "viewLocalState" ] . apiConfiguration ) . toEqual ( {
8739+ apiProvider : providerIdentifiers . anthropic ,
8740+ apiKey : "new-key" ,
8741+ } )
8742+ // Let queued view-local persistence settle while the console spies are
8743+ // still installed, so teardown has no pending writes.
8744+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) )
8745+ } finally {
8746+ await sibling . dispose ( )
8747+ }
8748+ } )
8749+
8750+ it ( "does not leak shared provider keys into a pinned view's api configuration" , async ( ) => {
8751+ vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } )
8752+ vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
8753+ // The shared store sits on a profile with keys the pinned profile never
8754+ // sets; spreading the overlay over the shared keys would report a mixed
8755+ // configuration (e.g. another profile's model id and region).
8756+ await provider . contextProxy . setValue ( "apiProvider" , providerIdentifiers . bedrock )
8757+ await provider . contextProxy . setValue ( "apiModelId" , "anthropic.claude-v1" )
8758+ await provider . contextProxy . setValue ( "awsRegion" , "us-east-1" )
8759+ provider [ "viewLocalState" ] . currentApiConfigName = "pinned-profile"
8760+ provider [ "viewLocalState" ] . apiConfiguration = {
8761+ apiProvider : providerIdentifiers . anthropic ,
8762+ apiKey : "k" ,
8763+ }
8764+
8765+ // The cloud doubles in this suite make getState() log its defensive catch
8766+ // branches; silence them so teardown has no pending console writes.
8767+ vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
8768+
8769+ const state = await provider . getState ( )
8770+
8771+ expect ( state . apiConfiguration . apiProvider ) . toBe ( providerIdentifiers . anthropic )
8772+ expect ( state . apiConfiguration . apiKey ) . toBe ( "k" )
8773+ expect ( state . apiConfiguration . apiModelId ) . toBeUndefined ( )
8774+ expect ( state . apiConfiguration . awsRegion ) . toBeUndefined ( )
8775+
8776+ // Let any deferred logging settle while the spies are installed.
8777+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) )
8778+ } )
8779+ } )
86658780} )
0 commit comments