From a9b70e4e7696664d494faa41c4d723c3fdb282e4 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 22 Jul 2025 16:36:43 +0000 Subject: [PATCH 1/2] fix: use stable cache key for SSH remote workspaces - Generate cache key using workspace name and relative path from home - This ensures cache persistence across SSH sessions where absolute paths may change - Add comprehensive tests for the new cache key generation logic Fixes #6066 --- .../__tests__/cache-manager.spec.ts | 51 ++++++++++++++++++- src/services/code-index/cache-manager.ts | 41 +++++++++++++-- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/services/code-index/__tests__/cache-manager.spec.ts b/src/services/code-index/__tests__/cache-manager.spec.ts index 54775c9069..bbdab5ff07 100644 --- a/src/services/code-index/__tests__/cache-manager.spec.ts +++ b/src/services/code-index/__tests__/cache-manager.spec.ts @@ -3,6 +3,8 @@ import * as vscode from "vscode" import { createHash } from "crypto" import debounce from "lodash.debounce" import { CacheManager } from "../cache-manager" +import * as path from "path" +import * as os from "os" // Mock safeWriteJson utility vitest.mock("../../../utils/safeWriteJson", () => ({ @@ -38,6 +40,11 @@ vitest.mock("@roo-code/telemetry", () => ({ }, })) +// Mock os module +vitest.mock("os", () => ({ + homedir: vitest.fn(() => "/home/user"), +})) + describe("CacheManager", () => { let mockContext: vscode.ExtensionContext let mockWorkspacePath: string @@ -63,8 +70,18 @@ describe("CacheManager", () => { }) describe("constructor", () => { - it("should correctly set up cachePath using Uri.joinPath and crypto.createHash", () => { - const expectedHash = createHash("sha256").update(mockWorkspacePath).digest("hex") + it("should correctly set up cachePath using Uri.joinPath with stable cache key", () => { + // The cache key should be based on workspace name and relative path + const workspaceName = path.basename(mockWorkspacePath) + const homedir = os.homedir() + let relativePath = mockWorkspacePath + + if (mockWorkspacePath.startsWith(homedir)) { + relativePath = path.relative(homedir, mockWorkspacePath) + } + + const compositeKey = `${workspaceName}::${relativePath}` + const expectedHash = createHash("sha256").update(compositeKey).digest("hex") expect(vscode.Uri.joinPath).toHaveBeenCalledWith( mockContext.globalStorageUri, @@ -75,6 +92,36 @@ describe("CacheManager", () => { it("should set up debounced save function", () => { expect(debounce).toHaveBeenCalledWith(expect.any(Function), 1500) }) + + it("should generate stable cache key for workspace under home directory", () => { + // os.homedir is already mocked to return '/home/user' + const workspaceUnderHome = "/home/user/projects/myproject" + const cacheManagerHome = new CacheManager(mockContext, workspaceUnderHome) + + // Expected key should use relative path from home + const expectedKey = `myproject::projects/myproject` + const expectedHash = createHash("sha256").update(expectedKey).digest("hex") + + expect(vscode.Uri.joinPath).toHaveBeenCalledWith( + mockContext.globalStorageUri, + `roo-index-cache-${expectedHash}.json`, + ) + }) + + it("should generate stable cache key for workspace outside home directory", () => { + // os.homedir is already mocked to return '/home/user' + const workspaceOutsideHome = "/opt/projects/myproject" + const cacheManagerOutside = new CacheManager(mockContext, workspaceOutsideHome) + + // Expected key should use full path since it's outside home + const expectedKey = `myproject::/opt/projects/myproject` + const expectedHash = createHash("sha256").update(expectedKey).digest("hex") + + expect(vscode.Uri.joinPath).toHaveBeenCalledWith( + mockContext.globalStorageUri, + `roo-index-cache-${expectedHash}.json`, + ) + }) }) describe("initialize", () => { diff --git a/src/services/code-index/cache-manager.ts b/src/services/code-index/cache-manager.ts index a9a4f0ac47..dc43ea8384 100644 --- a/src/services/code-index/cache-manager.ts +++ b/src/services/code-index/cache-manager.ts @@ -5,6 +5,8 @@ import debounce from "lodash.debounce" import { safeWriteJson } from "../../utils/safeWriteJson" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" +import * as path from "path" +import * as os from "os" /** * Manages the cache for code indexing @@ -23,15 +25,46 @@ export class CacheManager implements ICacheManager { private context: vscode.ExtensionContext, private workspacePath: string, ) { - this.cachePath = vscode.Uri.joinPath( - context.globalStorageUri, - `roo-index-cache-${createHash("sha256").update(workspacePath).digest("hex")}.json`, - ) + // Generate a stable cache key that persists across SSH sessions + const cacheKey = this.generateStableCacheKey(workspacePath) + this.cachePath = vscode.Uri.joinPath(context.globalStorageUri, `roo-index-cache-${cacheKey}.json`) this._debouncedSaveCache = debounce(async () => { await this._performSave() }, 1500) } + /** + * Generates a stable cache key for the workspace that persists across SSH sessions + * @param workspacePath The workspace path + * @returns A stable hash key + */ + private generateStableCacheKey(workspacePath: string): string { + // Get the workspace folder name + const workspaceName = path.basename(workspacePath) + + // Try to get a relative path from home directory for additional stability + const homedir = os.homedir() + let relativePath = workspacePath + + try { + // If the workspace is under the home directory, use the relative path + if (workspacePath.startsWith(homedir)) { + relativePath = path.relative(homedir, workspacePath) + } + } catch (error) { + // If we can't get relative path, just use the full path + console.warn("Failed to get relative path from home directory:", error) + } + + // Create a composite key using workspace name and relative path + // This should be more stable across SSH sessions where the absolute path might change + // but the relative structure remains the same + const compositeKey = `${workspaceName}::${relativePath}` + + // Generate hash from the composite key + return createHash("sha256").update(compositeKey).digest("hex") + } + /** * Initializes the cache manager by loading the cache file */ From 743dc97ef4d1276464a261496c4be398fcb4bd54 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 22 Jul 2025 16:46:30 +0000 Subject: [PATCH 2/2] fix: normalize path separators for cross-platform compatibility - Ensure consistent cache keys across Windows and Unix systems - Replace backslashes with forward slashes in relative paths --- src/services/code-index/__tests__/cache-manager.spec.ts | 4 +++- src/services/code-index/cache-manager.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/services/code-index/__tests__/cache-manager.spec.ts b/src/services/code-index/__tests__/cache-manager.spec.ts index bbdab5ff07..a5b34a4e8c 100644 --- a/src/services/code-index/__tests__/cache-manager.spec.ts +++ b/src/services/code-index/__tests__/cache-manager.spec.ts @@ -80,7 +80,9 @@ describe("CacheManager", () => { relativePath = path.relative(homedir, mockWorkspacePath) } - const compositeKey = `${workspaceName}::${relativePath}` + // Normalize path separators for consistency + const normalizedRelativePath = relativePath.replace(/\\/g, "/") + const compositeKey = `${workspaceName}::${normalizedRelativePath}` const expectedHash = createHash("sha256").update(compositeKey).digest("hex") expect(vscode.Uri.joinPath).toHaveBeenCalledWith( diff --git a/src/services/code-index/cache-manager.ts b/src/services/code-index/cache-manager.ts index dc43ea8384..19cb6e835a 100644 --- a/src/services/code-index/cache-manager.ts +++ b/src/services/code-index/cache-manager.ts @@ -56,10 +56,14 @@ export class CacheManager implements ICacheManager { console.warn("Failed to get relative path from home directory:", error) } - // Create a composite key using workspace name and relative path + // Normalize path separators to forward slashes for consistency across platforms + // This ensures the same cache key is generated regardless of the OS + const normalizedRelativePath = relativePath.replace(/\\/g, "/") + + // Create a composite key using workspace name and normalized relative path // This should be more stable across SSH sessions where the absolute path might change // but the relative structure remains the same - const compositeKey = `${workspaceName}::${relativePath}` + const compositeKey = `${workspaceName}::${normalizedRelativePath}` // Generate hash from the composite key return createHash("sha256").update(compositeKey).digest("hex")