|
|
@@ -1,6 +1,7 @@
|
|
|
import * as vscode from "vscode"
|
|
|
import { ContextProxy } from "../contextProxy"
|
|
|
import { logger } from "../../utils/logging"
|
|
|
+import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../../shared/globalState"
|
|
|
|
|
|
// Mock the logger
|
|
|
jest.mock("../../utils/logging", () => ({
|
|
|
@@ -12,6 +13,12 @@ jest.mock("../../utils/logging", () => ({
|
|
|
},
|
|
|
}))
|
|
|
|
|
|
+// Mock shared/globalState
|
|
|
+jest.mock("../../shared/globalState", () => ({
|
|
|
+ GLOBAL_STATE_KEYS: ["apiProvider", "apiModelId", "mode"],
|
|
|
+ SECRET_KEYS: ["apiKey", "openAiApiKey"],
|
|
|
+}))
|
|
|
+
|
|
|
// Mock VSCode API
|
|
|
jest.mock("vscode", () => ({
|
|
|
Uri: {
|
|
|
@@ -42,7 +49,7 @@ describe("ContextProxy", () => {
|
|
|
|
|
|
// Mock secrets
|
|
|
mockSecrets = {
|
|
|
- get: jest.fn(),
|
|
|
+ get: jest.fn().mockResolvedValue("test-secret"),
|
|
|
store: jest.fn().mockResolvedValue(undefined),
|
|
|
delete: jest.fn().mockResolvedValue(undefined),
|
|
|
}
|
|
|
@@ -74,98 +81,80 @@ describe("ContextProxy", () => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- describe("getGlobalState", () => {
|
|
|
- it("should return pending change when it exists", async () => {
|
|
|
- // Set up a pending change
|
|
|
- await proxy.updateGlobalState("test-key", "new-value")
|
|
|
-
|
|
|
- // Should return the pending value
|
|
|
- const result = await proxy.getGlobalState("test-key")
|
|
|
- expect(result).toBe("new-value")
|
|
|
+ describe("constructor", () => {
|
|
|
+ it("should initialize state cache with all global state keys", () => {
|
|
|
+ expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length)
|
|
|
+ for (const key of GLOBAL_STATE_KEYS) {
|
|
|
+ expect(mockGlobalState.get).toHaveBeenCalledWith(key)
|
|
|
+ }
|
|
|
+ })
|
|
|
|
|
|
- // Original context should not be called
|
|
|
- expect(mockGlobalState.get).not.toHaveBeenCalled()
|
|
|
+ it("should initialize secret cache with all secret keys", () => {
|
|
|
+ expect(mockSecrets.get).toHaveBeenCalledTimes(SECRET_KEYS.length)
|
|
|
+ for (const key of SECRET_KEYS) {
|
|
|
+ expect(mockSecrets.get).toHaveBeenCalledWith(key)
|
|
|
+ }
|
|
|
})
|
|
|
+ })
|
|
|
+
|
|
|
+ describe("getGlobalState", () => {
|
|
|
+ it("should return value from cache when it exists", async () => {
|
|
|
+ // Manually set a value in the cache
|
|
|
+ await proxy.updateGlobalState("test-key", "cached-value")
|
|
|
|
|
|
- it("should fall back to original context when no pending change exists", async () => {
|
|
|
- // Set up original context value
|
|
|
- mockGlobalState.get.mockReturnValue("original-value")
|
|
|
+ // Should return the cached value
|
|
|
+ const result = proxy.getGlobalState("test-key")
|
|
|
+ expect(result).toBe("cached-value")
|
|
|
|
|
|
- // Should get from original context
|
|
|
- const result = await proxy.getGlobalState("test-key")
|
|
|
- expect(result).toBe("original-value")
|
|
|
- expect(mockGlobalState.get).toHaveBeenCalledWith("test-key", undefined)
|
|
|
+ // Original context should be called once during updateGlobalState
|
|
|
+ expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length) // Only from initialization
|
|
|
})
|
|
|
|
|
|
it("should handle default values correctly", async () => {
|
|
|
- // No value in either pending or original
|
|
|
- mockGlobalState.get.mockImplementation((key: string, defaultValue: any) => defaultValue)
|
|
|
-
|
|
|
- // Should return the default value
|
|
|
- const result = await proxy.getGlobalState("test-key", "default-value")
|
|
|
+ // No value in cache
|
|
|
+ const result = proxy.getGlobalState("unknown-key", "default-value")
|
|
|
expect(result).toBe("default-value")
|
|
|
})
|
|
|
})
|
|
|
|
|
|
describe("updateGlobalState", () => {
|
|
|
- it("should buffer changes without calling original context", async () => {
|
|
|
+ it("should update state directly in original context", async () => {
|
|
|
await proxy.updateGlobalState("test-key", "new-value")
|
|
|
|
|
|
// Should have called logger.debug
|
|
|
- expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("buffering state update"))
|
|
|
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("updating state for key"))
|
|
|
|
|
|
- // Should not have called original context
|
|
|
- expect(mockGlobalState.update).not.toHaveBeenCalled()
|
|
|
+ // Should have called original context
|
|
|
+ expect(mockGlobalState.update).toHaveBeenCalledWith("test-key", "new-value")
|
|
|
|
|
|
- // Should have stored the value in pendingStateChanges
|
|
|
+ // Should have stored the value in cache
|
|
|
const storedValue = await proxy.getGlobalState("test-key")
|
|
|
expect(storedValue).toBe("new-value")
|
|
|
})
|
|
|
-
|
|
|
- it("should throw an error when context is disposed", async () => {
|
|
|
- await proxy.dispose()
|
|
|
-
|
|
|
- await expect(proxy.updateGlobalState("test-key", "new-value")).rejects.toThrow(
|
|
|
- "Cannot update state on disposed context",
|
|
|
- )
|
|
|
- })
|
|
|
})
|
|
|
|
|
|
describe("getSecret", () => {
|
|
|
- it("should return pending secret when it exists", async () => {
|
|
|
- // Set up a pending secret
|
|
|
- await proxy.storeSecret("api-key", "secret123")
|
|
|
+ it("should return value from cache when it exists", async () => {
|
|
|
+ // Manually set a value in the cache
|
|
|
+ await proxy.storeSecret("api-key", "cached-secret")
|
|
|
|
|
|
- // Should return the pending value
|
|
|
- const result = await proxy.getSecret("api-key")
|
|
|
- expect(result).toBe("secret123")
|
|
|
-
|
|
|
- // Original context should not be called
|
|
|
- expect(mockSecrets.get).not.toHaveBeenCalled()
|
|
|
- })
|
|
|
-
|
|
|
- it("should fall back to original context when no pending secret exists", async () => {
|
|
|
- // Set up original context value
|
|
|
- mockSecrets.get.mockResolvedValue("original-secret")
|
|
|
-
|
|
|
- // Should get from original context
|
|
|
- const result = await proxy.getSecret("api-key")
|
|
|
- expect(result).toBe("original-secret")
|
|
|
- expect(mockSecrets.get).toHaveBeenCalledWith("api-key")
|
|
|
+ // Should return the cached value
|
|
|
+ const result = proxy.getSecret("api-key")
|
|
|
+ expect(result).toBe("cached-secret")
|
|
|
})
|
|
|
})
|
|
|
|
|
|
describe("storeSecret", () => {
|
|
|
- it("should buffer secret changes without calling original context", async () => {
|
|
|
+ it("should store secret directly in original context", async () => {
|
|
|
await proxy.storeSecret("api-key", "new-secret")
|
|
|
|
|
|
// Should have called logger.debug
|
|
|
- expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("buffering secret update"))
|
|
|
+ expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("storing secret for key"))
|
|
|
|
|
|
- // Should not have called original context
|
|
|
- expect(mockSecrets.store).not.toHaveBeenCalled()
|
|
|
+ // Should have called original context
|
|
|
+ expect(mockSecrets.store).toHaveBeenCalledWith("api-key", "new-secret")
|
|
|
|
|
|
- // Should have stored the value in pendingSecretChanges
|
|
|
+ // Should have stored the value in cache
|
|
|
const storedValue = await proxy.getSecret("api-key")
|
|
|
expect(storedValue).toBe("new-secret")
|
|
|
})
|
|
|
@@ -173,110 +162,12 @@ describe("ContextProxy", () => {
|
|
|
it("should handle undefined value for secret deletion", async () => {
|
|
|
await proxy.storeSecret("api-key", undefined)
|
|
|
|
|
|
- // Should have stored undefined in pendingSecretChanges
|
|
|
+ // Should have called delete on original context
|
|
|
+ expect(mockSecrets.delete).toHaveBeenCalledWith("api-key")
|
|
|
+
|
|
|
+ // Should have stored undefined in cache
|
|
|
const storedValue = await proxy.getSecret("api-key")
|
|
|
expect(storedValue).toBeUndefined()
|
|
|
})
|
|
|
-
|
|
|
- it("should throw an error when context is disposed", async () => {
|
|
|
- await proxy.dispose()
|
|
|
-
|
|
|
- await expect(proxy.storeSecret("api-key", "new-secret")).rejects.toThrow(
|
|
|
- "Cannot store secret on disposed context",
|
|
|
- )
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- describe("saveChanges", () => {
|
|
|
- it("should apply state changes to original context", async () => {
|
|
|
- // Set up pending changes
|
|
|
- await proxy.updateGlobalState("key1", "value1")
|
|
|
- await proxy.updateGlobalState("key2", "value2")
|
|
|
-
|
|
|
- // Save changes
|
|
|
- await proxy.saveChanges()
|
|
|
-
|
|
|
- // Should have called update on original context
|
|
|
- expect(mockGlobalState.update).toHaveBeenCalledTimes(2)
|
|
|
- expect(mockGlobalState.update).toHaveBeenCalledWith("key1", "value1")
|
|
|
- expect(mockGlobalState.update).toHaveBeenCalledWith("key2", "value2")
|
|
|
-
|
|
|
- // Should have cleared pending changes
|
|
|
- expect(proxy.hasPendingChanges()).toBe(false)
|
|
|
- })
|
|
|
-
|
|
|
- it("should apply secret changes to original context", async () => {
|
|
|
- // Set up pending changes
|
|
|
- await proxy.storeSecret("secret1", "value1")
|
|
|
- await proxy.storeSecret("secret2", undefined)
|
|
|
-
|
|
|
- // Save changes
|
|
|
- await proxy.saveChanges()
|
|
|
-
|
|
|
- // Should have called store and delete on original context
|
|
|
- expect(mockSecrets.store).toHaveBeenCalledTimes(1)
|
|
|
- expect(mockSecrets.store).toHaveBeenCalledWith("secret1", "value1")
|
|
|
- expect(mockSecrets.delete).toHaveBeenCalledTimes(1)
|
|
|
- expect(mockSecrets.delete).toHaveBeenCalledWith("secret2")
|
|
|
-
|
|
|
- // Should have cleared pending changes
|
|
|
- expect(proxy.hasPendingChanges()).toBe(false)
|
|
|
- })
|
|
|
-
|
|
|
- it("should do nothing when there are no pending changes", async () => {
|
|
|
- await proxy.saveChanges()
|
|
|
-
|
|
|
- expect(mockGlobalState.update).not.toHaveBeenCalled()
|
|
|
- expect(mockSecrets.store).not.toHaveBeenCalled()
|
|
|
- expect(mockSecrets.delete).not.toHaveBeenCalled()
|
|
|
- })
|
|
|
-
|
|
|
- it("should throw an error when context is disposed", async () => {
|
|
|
- await proxy.dispose()
|
|
|
-
|
|
|
- await expect(proxy.saveChanges()).rejects.toThrow("Cannot save changes on disposed context")
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- describe("dispose", () => {
|
|
|
- it("should save pending changes to original context", async () => {
|
|
|
- // Set up pending changes
|
|
|
- await proxy.updateGlobalState("key1", "value1")
|
|
|
- await proxy.storeSecret("secret1", "value1")
|
|
|
-
|
|
|
- // Dispose
|
|
|
- await proxy.dispose()
|
|
|
-
|
|
|
- // Should have saved changes
|
|
|
- expect(mockGlobalState.update).toHaveBeenCalledWith("key1", "value1")
|
|
|
- expect(mockSecrets.store).toHaveBeenCalledWith("secret1", "value1")
|
|
|
-
|
|
|
- // Should be marked as disposed
|
|
|
- expect(proxy.hasPendingChanges()).toBe(false)
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- describe("hasPendingChanges", () => {
|
|
|
- it("should return false when no changes are pending", () => {
|
|
|
- expect(proxy.hasPendingChanges()).toBe(false)
|
|
|
- })
|
|
|
-
|
|
|
- it("should return true when state changes are pending", async () => {
|
|
|
- await proxy.updateGlobalState("key", "value")
|
|
|
- expect(proxy.hasPendingChanges()).toBe(true)
|
|
|
- })
|
|
|
-
|
|
|
- it("should return true when secret changes are pending", async () => {
|
|
|
- await proxy.storeSecret("key", "value")
|
|
|
- expect(proxy.hasPendingChanges()).toBe(true)
|
|
|
- })
|
|
|
-
|
|
|
- it("should return false after changes are saved", async () => {
|
|
|
- await proxy.updateGlobalState("key", "value")
|
|
|
- expect(proxy.hasPendingChanges()).toBe(true)
|
|
|
-
|
|
|
- await proxy.saveChanges()
|
|
|
- expect(proxy.hasPendingChanges()).toBe(false)
|
|
|
- })
|
|
|
})
|
|
|
})
|