@@ -6,6 +6,19 @@ import type { ProviderSettings } from "@roo-code/types"
66
77import { ProviderSettingsManager , ProviderProfiles , SyncCloudProfilesResult } from "../ProviderSettingsManager"
88
9+ // Mock getWorkspacePath
10+ import { getWorkspacePath } from "../../../utils/path"
11+ vi . mock ( "../../../utils/path" , ( ) => ( {
12+ getWorkspacePath : vi . fn ( ( ) => "/test/workspace" ) ,
13+ } ) )
14+
15+ // Mock vscode module
16+ vi . mock ( "vscode" , ( ) => ( {
17+ workspace : {
18+ workspaceFolders : [ { uri : { fsPath : "/test/workspace" } } ] ,
19+ } ,
20+ } ) )
21+
922// Mock VSCode ExtensionContext
1023const mockSecrets = {
1124 get : vi . fn ( ) ,
@@ -458,7 +471,10 @@ describe("ProviderSettingsManager", () => {
458471 } ,
459472 }
460473
461- expect ( mockSecrets . store . mock . calls [ 0 ] [ 0 ] ) . toEqual ( "roo_cline_config_api_config" )
474+ // Should use workspace-specific key (hash of /test/workspace)
475+ const crypto = await import ( "crypto" )
476+ const workspaceHash = crypto . createHash ( "sha256" ) . update ( "/test/workspace" ) . digest ( "hex" ) . substring ( 0 , 8 )
477+ expect ( mockSecrets . store . mock . calls [ 0 ] [ 0 ] ) . toEqual ( `roo_cline_config_ws_${ workspaceHash } ` )
462478 expect ( storedConfig ) . toEqual ( expectedConfig )
463479 } )
464480
@@ -508,7 +524,10 @@ describe("ProviderSettingsManager", () => {
508524 } ,
509525 }
510526
511- expect ( mockSecrets . store . mock . calls [ 0 ] [ 0 ] ) . toEqual ( "roo_cline_config_api_config" )
527+ // Should use workspace-specific key
528+ const crypto = await import ( "crypto" )
529+ const workspaceHash = crypto . createHash ( "sha256" ) . update ( "/test/workspace" ) . digest ( "hex" ) . substring ( 0 , 8 )
530+ expect ( mockSecrets . store . mock . calls [ 0 ] [ 0 ] ) . toEqual ( `roo_cline_config_ws_${ workspaceHash } ` )
512531 expect ( storedConfig ) . toEqual ( expectedConfig )
513532 } )
514533
@@ -551,8 +570,10 @@ describe("ProviderSettingsManager", () => {
551570 }
552571
553572 const storedConfig = JSON . parse ( mockSecrets . store . mock . calls [ mockSecrets . store . mock . calls . length - 1 ] [ 1 ] )
573+ const crypto = await import ( "crypto" )
574+ const workspaceHash = crypto . createHash ( "sha256" ) . update ( "/test/workspace" ) . digest ( "hex" ) . substring ( 0 , 8 )
554575 expect ( mockSecrets . store . mock . calls [ mockSecrets . store . mock . calls . length - 1 ] [ 0 ] ) . toEqual (
555- "roo_cline_config_api_config" ,
576+ `roo_cline_config_ws_ ${ workspaceHash } ` ,
556577 )
557578 expect ( storedConfig ) . toEqual ( expectedConfig )
558579 } )
@@ -757,7 +778,11 @@ describe("ProviderSettingsManager", () => {
757778
758779 await providerSettingsManager . resetAllConfigs ( )
759780
760- // Should have called delete with the correct config key
781+ // Should have called delete with the workspace-specific key
782+ const crypto = await import ( "crypto" )
783+ const workspaceHash = crypto . createHash ( "sha256" ) . update ( "/test/workspace" ) . digest ( "hex" ) . substring ( 0 , 8 )
784+ expect ( mockSecrets . delete ) . toHaveBeenCalledWith ( `roo_cline_config_ws_${ workspaceHash } ` )
785+ // Should also try to delete the global key for backward compatibility
761786 expect ( mockSecrets . delete ) . toHaveBeenCalledWith ( "roo_cline_config_api_config" )
762787 } )
763788 } )
@@ -1236,4 +1261,159 @@ describe("ProviderSettingsManager", () => {
12361261 expect ( result . activeProfileId ) . toBe ( "local-id" )
12371262 } )
12381263 } )
1264+
1265+ describe ( "Workspace-specific storage" , ( ) => {
1266+ it ( "should use workspace-specific key when workspace is available" , async ( ) => {
1267+ vi . mocked ( getWorkspacePath ) . mockReturnValue ( "/test/workspace" )
1268+ const manager = new ProviderSettingsManager ( mockContext )
1269+
1270+ mockSecrets . get . mockResolvedValue ( null )
1271+
1272+ const config : ProviderSettings = {
1273+ apiProvider : "anthropic" ,
1274+ apiKey : "test-key" ,
1275+ }
1276+
1277+ await manager . saveConfig ( "test" , config )
1278+
1279+ const crypto = await import ( "crypto" )
1280+ const workspaceHash = crypto . createHash ( "sha256" ) . update ( "/test/workspace" ) . digest ( "hex" ) . substring ( 0 , 8 )
1281+ expect ( mockSecrets . store ) . toHaveBeenCalledWith ( `roo_cline_config_ws_${ workspaceHash } ` , expect . any ( String ) )
1282+ } )
1283+
1284+ it ( "should fall back to global key when no workspace is available" , async ( ) => {
1285+ vi . mocked ( getWorkspacePath ) . mockReturnValue ( "" )
1286+ const manager = new ProviderSettingsManager ( mockContext )
1287+
1288+ mockSecrets . get . mockResolvedValue ( null )
1289+
1290+ const config : ProviderSettings = {
1291+ apiProvider : "anthropic" ,
1292+ apiKey : "test-key" ,
1293+ }
1294+
1295+ await manager . saveConfig ( "test" , config )
1296+
1297+ expect ( mockSecrets . store ) . toHaveBeenCalledWith ( "roo_cline_config_api_config" , expect . any ( String ) )
1298+ } )
1299+
1300+ it ( "should migrate from global to workspace-specific storage" , async ( ) => {
1301+ vi . mocked ( getWorkspacePath ) . mockReturnValue ( "/test/workspace" )
1302+
1303+ const globalConfig = {
1304+ currentApiConfigName : "global-config" ,
1305+ apiConfigs : {
1306+ "global-config" : {
1307+ apiProvider : "anthropic" ,
1308+ apiKey : "global-key" ,
1309+ id : "global-id" ,
1310+ } ,
1311+ } ,
1312+ }
1313+
1314+ const crypto = await import ( "crypto" )
1315+ const workspaceHash = crypto . createHash ( "sha256" ) . update ( "/test/workspace" ) . digest ( "hex" ) . substring ( 0 , 8 )
1316+ const workspaceKey = `roo_cline_config_ws_${ workspaceHash } `
1317+
1318+ // Set up the mock to properly simulate migration behavior
1319+ let storedConfig : string | undefined
1320+ mockSecrets . get . mockImplementation ( ( key ) => {
1321+ if ( key === workspaceKey ) {
1322+ // Return stored config if it was migrated
1323+ return Promise . resolve ( storedConfig || null )
1324+ } else if ( key === "roo_cline_config_api_config" ) {
1325+ // Return global config for migration
1326+ return Promise . resolve ( JSON . stringify ( globalConfig ) )
1327+ }
1328+ return Promise . resolve ( null )
1329+ } )
1330+
1331+ mockSecrets . store . mockImplementation ( ( key , value ) => {
1332+ if ( key === workspaceKey ) {
1333+ storedConfig = value
1334+ }
1335+ return Promise . resolve ( )
1336+ } )
1337+
1338+ const manager = new ProviderSettingsManager ( mockContext )
1339+ // Wait for initialization to complete (which triggers migration)
1340+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) )
1341+
1342+ const configs = await manager . listConfig ( )
1343+
1344+ // Should have migrated the global config
1345+ expect ( configs ) . toEqual ( [
1346+ { name : "global-config" , id : "global-id" , apiProvider : "anthropic" , modelId : undefined } ,
1347+ ] )
1348+
1349+ // Should have saved to workspace-specific key
1350+ expect ( mockSecrets . store ) . toHaveBeenCalledWith ( workspaceKey , JSON . stringify ( globalConfig ) )
1351+ } )
1352+
1353+ it ( "should maintain separate configs for different workspaces" , async ( ) => {
1354+ // First workspace
1355+ vi . mocked ( getWorkspacePath ) . mockReturnValue ( "/workspace/project1" )
1356+ const manager1 = new ProviderSettingsManager ( mockContext )
1357+
1358+ const config1 = {
1359+ currentApiConfigName : "project1-config" ,
1360+ apiConfigs : {
1361+ "project1-config" : {
1362+ apiProvider : "anthropic" ,
1363+ apiKey : "project1-key" ,
1364+ id : "project1-id" ,
1365+ } ,
1366+ } ,
1367+ }
1368+
1369+ const crypto = await import ( "crypto" )
1370+ const workspace1Hash = crypto
1371+ . createHash ( "sha256" )
1372+ . update ( "/workspace/project1" )
1373+ . digest ( "hex" )
1374+ . substring ( 0 , 8 )
1375+ mockSecrets . get . mockImplementation ( ( key ) => {
1376+ if ( key === `roo_cline_config_ws_${ workspace1Hash } ` ) {
1377+ return JSON . stringify ( config1 )
1378+ }
1379+ return null
1380+ } )
1381+
1382+ const configs1 = await manager1 . listConfig ( )
1383+ expect ( configs1 ) . toEqual ( [ { name : "project1-config" , id : "project1-id" , apiProvider : "anthropic" } ] )
1384+
1385+ // Second workspace
1386+ vi . mocked ( getWorkspacePath ) . mockReturnValue ( "/workspace/project2" )
1387+ const manager2 = new ProviderSettingsManager ( mockContext )
1388+
1389+ const config2 = {
1390+ currentApiConfigName : "project2-config" ,
1391+ apiConfigs : {
1392+ "project2-config" : {
1393+ apiProvider : "openai" ,
1394+ apiKey : "project2-key" ,
1395+ id : "project2-id" ,
1396+ } ,
1397+ } ,
1398+ }
1399+
1400+ const workspace2Hash = crypto
1401+ . createHash ( "sha256" )
1402+ . update ( "/workspace/project2" )
1403+ . digest ( "hex" )
1404+ . substring ( 0 , 8 )
1405+ mockSecrets . get . mockImplementation ( ( key ) => {
1406+ if ( key === `roo_cline_config_ws_${ workspace2Hash } ` ) {
1407+ return JSON . stringify ( config2 )
1408+ }
1409+ return null
1410+ } )
1411+
1412+ const configs2 = await manager2 . listConfig ( )
1413+ expect ( configs2 ) . toEqual ( [ { name : "project2-config" , id : "project2-id" , apiProvider : "openai" } ] )
1414+
1415+ // Verify different workspace hashes
1416+ expect ( workspace1Hash ) . not . toEqual ( workspace2Hash )
1417+ } )
1418+ } )
12391419} )
0 commit comments