Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled
- Added a new integration script to manage custom features related to smart context. - Implemented handlers for smart context operations (get, update, clear, stats) in ipc. - Created a SmartContextStore class to manage context snippets and summaries. - Developed hooks for React to interact with smart context (useSmartContext, useUpdateSmartContext, useClearSmartContext, useSmartContextStats). - Included backup and restore functionality in the integration script. - Validated integration by checking for custom modifications and file existence.
410 lines
12 KiB
TypeScript
410 lines
12 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { safeStorage } from "electron";
|
|
import { readSettings, getSettingsFilePath } from "@/main/settings";
|
|
import { getUserDataPath } from "@/paths/paths";
|
|
import { UserSettings } from "@/lib/schemas";
|
|
|
|
// Mock dependencies
|
|
vi.mock("node:fs");
|
|
vi.mock("node:path");
|
|
vi.mock("electron", () => ({
|
|
safeStorage: {
|
|
isEncryptionAvailable: vi.fn(),
|
|
decryptString: vi.fn(),
|
|
},
|
|
}));
|
|
vi.mock("@/paths/paths", () => ({
|
|
getUserDataPath: vi.fn(),
|
|
}));
|
|
|
|
const mockFs = vi.mocked(fs);
|
|
const mockPath = vi.mocked(path);
|
|
const mockSafeStorage = vi.mocked(safeStorage);
|
|
const mockGetUserDataPath = vi.mocked(getUserDataPath);
|
|
|
|
describe("readSettings", () => {
|
|
const mockUserDataPath = "/mock/user/data";
|
|
const mockSettingsPath = "/mock/user/data/user-settings.json";
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockGetUserDataPath.mockReturnValue(mockUserDataPath);
|
|
mockPath.join.mockReturnValue(mockSettingsPath);
|
|
mockSafeStorage.isEncryptionAvailable.mockReturnValue(true);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("when settings file does not exist", () => {
|
|
it("should create default settings file and return default settings", () => {
|
|
mockFs.existsSync.mockReturnValue(false);
|
|
mockFs.writeFileSync.mockImplementation(() => {});
|
|
|
|
const result = readSettings();
|
|
|
|
expect(mockFs.existsSync).toHaveBeenCalledWith(mockSettingsPath);
|
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
mockSettingsPath,
|
|
expect.stringContaining('"selectedModel"'),
|
|
);
|
|
expect(scrubSettings(result)).toMatchInlineSnapshot(`
|
|
{
|
|
"enableAutoFixProblems": false,
|
|
"enableAutoUpdate": true,
|
|
"enableProLazyEditsMode": true,
|
|
"enableProSmartFilesContextMode": true,
|
|
"experiments": {},
|
|
"hasRunBefore": false,
|
|
"isRunning": false,
|
|
"lastKnownPerformance": undefined,
|
|
"providerSettings": {},
|
|
"releaseChannel": "stable",
|
|
"selectedChatMode": "build",
|
|
"selectedModel": {
|
|
"name": "auto",
|
|
"provider": "auto",
|
|
},
|
|
"selectedTemplateId": "react",
|
|
"telemetryConsent": "unset",
|
|
"telemetryUserId": "[scrubbed]",
|
|
}
|
|
`);
|
|
});
|
|
});
|
|
|
|
describe("when settings file exists", () => {
|
|
it("should read and merge settings with defaults", () => {
|
|
const mockFileContent = {
|
|
selectedModel: {
|
|
name: "gpt-4",
|
|
provider: "openai",
|
|
},
|
|
telemetryConsent: "opted_in",
|
|
hasRunBefore: true,
|
|
};
|
|
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
|
|
|
const result = readSettings();
|
|
|
|
expect(mockFs.readFileSync).toHaveBeenCalledWith(
|
|
mockSettingsPath,
|
|
"utf-8",
|
|
);
|
|
expect(result.selectedModel).toEqual({
|
|
name: "gpt-4",
|
|
provider: "openai",
|
|
});
|
|
expect(result.telemetryConsent).toBe("opted_in");
|
|
expect(result.hasRunBefore).toBe(true);
|
|
// Should still have defaults for missing properties
|
|
expect(result.enableAutoUpdate).toBe(true);
|
|
expect(result.releaseChannel).toBe("stable");
|
|
});
|
|
|
|
it("should decrypt encrypted provider API keys", () => {
|
|
const mockFileContent = {
|
|
providerSettings: {
|
|
openai: {
|
|
apiKey: {
|
|
value: "encrypted-api-key",
|
|
encryptionType: "electron-safe-storage",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
|
mockSafeStorage.decryptString.mockReturnValue("decrypted-api-key");
|
|
|
|
const result = readSettings();
|
|
|
|
expect(mockSafeStorage.decryptString).toHaveBeenCalledWith(
|
|
Buffer.from("encrypted-api-key", "base64"),
|
|
);
|
|
expect(result.providerSettings.openai.apiKey).toEqual({
|
|
value: "decrypted-api-key",
|
|
encryptionType: "electron-safe-storage",
|
|
});
|
|
});
|
|
|
|
it("should decrypt encrypted GitHub access token", () => {
|
|
const mockFileContent = {
|
|
githubAccessToken: {
|
|
value: "encrypted-github-token",
|
|
encryptionType: "electron-safe-storage",
|
|
},
|
|
};
|
|
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
|
mockSafeStorage.decryptString.mockReturnValue("decrypted-github-token");
|
|
|
|
const result = readSettings();
|
|
|
|
expect(mockSafeStorage.decryptString).toHaveBeenCalledWith(
|
|
Buffer.from("encrypted-github-token", "base64"),
|
|
);
|
|
expect(result.githubAccessToken).toEqual({
|
|
value: "decrypted-github-token",
|
|
encryptionType: "electron-safe-storage",
|
|
});
|
|
});
|
|
|
|
it("should decrypt encrypted Supabase tokens", () => {
|
|
const mockFileContent = {
|
|
supabase: {
|
|
accessToken: {
|
|
value: "encrypted-access-token",
|
|
encryptionType: "electron-safe-storage",
|
|
},
|
|
refreshToken: {
|
|
value: "encrypted-refresh-token",
|
|
encryptionType: "electron-safe-storage",
|
|
},
|
|
},
|
|
};
|
|
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
|
mockSafeStorage.decryptString
|
|
.mockReturnValueOnce("decrypted-refresh-token")
|
|
.mockReturnValueOnce("decrypted-access-token");
|
|
|
|
const result = readSettings();
|
|
|
|
expect(mockSafeStorage.decryptString).toHaveBeenCalledTimes(2);
|
|
expect(result.supabase?.refreshToken).toEqual({
|
|
value: "decrypted-refresh-token",
|
|
encryptionType: "electron-safe-storage",
|
|
});
|
|
expect(result.supabase?.accessToken).toEqual({
|
|
value: "decrypted-access-token",
|
|
encryptionType: "electron-safe-storage",
|
|
});
|
|
});
|
|
|
|
it("should handle plaintext secrets without decryption", () => {
|
|
const mockFileContent = {
|
|
githubAccessToken: {
|
|
value: "plaintext-token",
|
|
encryptionType: "plaintext",
|
|
},
|
|
providerSettings: {
|
|
openai: {
|
|
apiKey: {
|
|
value: "plaintext-api-key",
|
|
encryptionType: "plaintext",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
|
|
|
const result = readSettings();
|
|
|
|
expect(mockSafeStorage.decryptString).not.toHaveBeenCalled();
|
|
expect(result.githubAccessToken?.value).toBe("plaintext-token");
|
|
expect(result.providerSettings.openai.apiKey?.value).toBe(
|
|
"plaintext-api-key",
|
|
);
|
|
});
|
|
|
|
it("should handle secrets without encryptionType", () => {
|
|
const mockFileContent = {
|
|
githubAccessToken: {
|
|
value: "token-without-encryption-type",
|
|
},
|
|
providerSettings: {
|
|
openai: {
|
|
apiKey: {
|
|
value: "api-key-without-encryption-type",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
|
|
|
const result = readSettings();
|
|
|
|
expect(mockSafeStorage.decryptString).not.toHaveBeenCalled();
|
|
expect(result.githubAccessToken?.value).toBe(
|
|
"token-without-encryption-type",
|
|
);
|
|
expect(result.providerSettings.openai.apiKey?.value).toBe(
|
|
"api-key-without-encryption-type",
|
|
);
|
|
});
|
|
|
|
it("should strip extra fields not recognized by the schema", () => {
|
|
const mockFileContent = {
|
|
selectedModel: {
|
|
name: "gpt-4",
|
|
provider: "openai",
|
|
},
|
|
telemetryConsent: "opted_in",
|
|
hasRunBefore: true,
|
|
// Extra fields that are not in the schema
|
|
unknownField: "should be removed",
|
|
deprecatedSetting: true,
|
|
extraConfig: {
|
|
someValue: 123,
|
|
anotherValue: "test",
|
|
},
|
|
};
|
|
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
|
|
|
const result = readSettings();
|
|
|
|
expect(mockFs.readFileSync).toHaveBeenCalledWith(
|
|
mockSettingsPath,
|
|
"utf-8",
|
|
);
|
|
expect(result.selectedModel).toEqual({
|
|
name: "gpt-4",
|
|
provider: "openai",
|
|
});
|
|
expect(result.telemetryConsent).toBe("opted_in");
|
|
expect(result.hasRunBefore).toBe(true);
|
|
|
|
// Extra fields should be stripped by schema validation
|
|
expect(result).not.toHaveProperty("unknownField");
|
|
expect(result).not.toHaveProperty("deprecatedSetting");
|
|
expect(result).not.toHaveProperty("extraConfig");
|
|
|
|
// Should still have defaults for missing properties
|
|
expect(result.enableAutoUpdate).toBe(true);
|
|
expect(result.releaseChannel).toBe("stable");
|
|
});
|
|
});
|
|
|
|
describe("error handling", () => {
|
|
it("should return default settings when file read fails", () => {
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockImplementation(() => {
|
|
throw new Error("File read error");
|
|
});
|
|
|
|
const result = readSettings();
|
|
|
|
expect(scrubSettings(result)).toMatchInlineSnapshot(`
|
|
{
|
|
"enableAutoFixProblems": false,
|
|
"enableAutoUpdate": true,
|
|
"enableProLazyEditsMode": true,
|
|
"enableProSmartFilesContextMode": true,
|
|
"experiments": {},
|
|
"hasRunBefore": false,
|
|
"isRunning": false,
|
|
"lastKnownPerformance": undefined,
|
|
"providerSettings": {},
|
|
"releaseChannel": "stable",
|
|
"selectedChatMode": "build",
|
|
"selectedModel": {
|
|
"name": "auto",
|
|
"provider": "auto",
|
|
},
|
|
"selectedTemplateId": "react",
|
|
"telemetryConsent": "unset",
|
|
"telemetryUserId": "[scrubbed]",
|
|
}
|
|
`);
|
|
});
|
|
|
|
it("should return default settings when JSON parsing fails", () => {
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue("invalid json");
|
|
|
|
const result = readSettings();
|
|
|
|
expect(result).toMatchObject({
|
|
selectedModel: {
|
|
name: "auto",
|
|
provider: "auto",
|
|
},
|
|
releaseChannel: "stable",
|
|
});
|
|
});
|
|
|
|
it("should return default settings when schema validation fails", () => {
|
|
const mockFileContent = {
|
|
selectedModel: {
|
|
name: "gpt-4",
|
|
// Missing required 'provider' field
|
|
},
|
|
releaseChannel: "invalid-channel", // Invalid enum value
|
|
};
|
|
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
|
|
|
const result = readSettings();
|
|
|
|
expect(result).toMatchObject({
|
|
selectedModel: {
|
|
name: "auto",
|
|
provider: "auto",
|
|
},
|
|
releaseChannel: "stable",
|
|
});
|
|
});
|
|
|
|
it("should handle decryption errors gracefully", () => {
|
|
const mockFileContent = {
|
|
githubAccessToken: {
|
|
value: "corrupted-encrypted-data",
|
|
encryptionType: "electron-safe-storage",
|
|
},
|
|
};
|
|
|
|
mockFs.existsSync.mockReturnValue(true);
|
|
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
|
mockSafeStorage.decryptString.mockImplementation(() => {
|
|
throw new Error("Decryption failed");
|
|
});
|
|
|
|
const result = readSettings();
|
|
|
|
expect(result).toMatchObject({
|
|
selectedModel: {
|
|
name: "auto",
|
|
provider: "auto",
|
|
},
|
|
releaseChannel: "stable",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getSettingsFilePath", () => {
|
|
it("should return correct settings file path", () => {
|
|
const result = getSettingsFilePath();
|
|
|
|
expect(mockGetUserDataPath).toHaveBeenCalled();
|
|
expect(mockPath.join).toHaveBeenCalledWith(
|
|
mockUserDataPath,
|
|
"user-settings.json",
|
|
);
|
|
expect(result).toBe(mockSettingsPath);
|
|
});
|
|
});
|
|
});
|
|
|
|
function scrubSettings(result: UserSettings) {
|
|
return {
|
|
...result,
|
|
telemetryUserId: "[scrubbed]",
|
|
};
|
|
}
|