mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
- Add comprehensive unit tests for SettingsService, covering global and project settings management, including creation, updates, and merging with defaults. - Implement tests for handling credentials, ensuring proper masking and merging of API keys. - Introduce tests for migration from localStorage, validating successful data transfer and error handling. - Enhance error handling in subprocess management tests, ensuring robust timeout and output reading scenarios.
644 lines
22 KiB
TypeScript
644 lines
22 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import os from "os";
|
|
import { SettingsService } from "@/services/settings-service.js";
|
|
import {
|
|
DEFAULT_GLOBAL_SETTINGS,
|
|
DEFAULT_CREDENTIALS,
|
|
DEFAULT_PROJECT_SETTINGS,
|
|
SETTINGS_VERSION,
|
|
CREDENTIALS_VERSION,
|
|
PROJECT_SETTINGS_VERSION,
|
|
type GlobalSettings,
|
|
type Credentials,
|
|
type ProjectSettings,
|
|
} from "@/types/settings.js";
|
|
|
|
describe("settings-service.ts", () => {
|
|
let testDataDir: string;
|
|
let testProjectDir: string;
|
|
let settingsService: SettingsService;
|
|
|
|
beforeEach(async () => {
|
|
testDataDir = path.join(os.tmpdir(), `settings-test-${Date.now()}`);
|
|
testProjectDir = path.join(os.tmpdir(), `project-test-${Date.now()}`);
|
|
await fs.mkdir(testDataDir, { recursive: true });
|
|
await fs.mkdir(testProjectDir, { recursive: true });
|
|
settingsService = new SettingsService(testDataDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
try {
|
|
await fs.rm(testDataDir, { recursive: true, force: true });
|
|
await fs.rm(testProjectDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
describe("getGlobalSettings", () => {
|
|
it("should return default settings when file does not exist", async () => {
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings).toEqual(DEFAULT_GLOBAL_SETTINGS);
|
|
});
|
|
|
|
it("should read and return existing settings", async () => {
|
|
const customSettings: GlobalSettings = {
|
|
...DEFAULT_GLOBAL_SETTINGS,
|
|
theme: "light",
|
|
sidebarOpen: false,
|
|
maxConcurrency: 5,
|
|
};
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2));
|
|
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.theme).toBe("light");
|
|
expect(settings.sidebarOpen).toBe(false);
|
|
expect(settings.maxConcurrency).toBe(5);
|
|
});
|
|
|
|
it("should merge with defaults for missing properties", async () => {
|
|
const partialSettings = {
|
|
version: SETTINGS_VERSION,
|
|
theme: "dark",
|
|
};
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(partialSettings, null, 2));
|
|
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.theme).toBe("dark");
|
|
expect(settings.sidebarOpen).toBe(DEFAULT_GLOBAL_SETTINGS.sidebarOpen);
|
|
expect(settings.maxConcurrency).toBe(DEFAULT_GLOBAL_SETTINGS.maxConcurrency);
|
|
});
|
|
|
|
it("should merge keyboard shortcuts deeply", async () => {
|
|
const customSettings: GlobalSettings = {
|
|
...DEFAULT_GLOBAL_SETTINGS,
|
|
keyboardShortcuts: {
|
|
...DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts,
|
|
board: "B",
|
|
},
|
|
};
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2));
|
|
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.keyboardShortcuts.board).toBe("B");
|
|
expect(settings.keyboardShortcuts.agent).toBe(
|
|
DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts.agent
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("updateGlobalSettings", () => {
|
|
it("should create settings file with updates", async () => {
|
|
const updates: Partial<GlobalSettings> = {
|
|
theme: "light",
|
|
sidebarOpen: false,
|
|
};
|
|
|
|
const updated = await settingsService.updateGlobalSettings(updates);
|
|
|
|
expect(updated.theme).toBe("light");
|
|
expect(updated.sidebarOpen).toBe(false);
|
|
expect(updated.version).toBe(SETTINGS_VERSION);
|
|
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
const fileContent = await fs.readFile(settingsPath, "utf-8");
|
|
const saved = JSON.parse(fileContent);
|
|
expect(saved.theme).toBe("light");
|
|
expect(saved.sidebarOpen).toBe(false);
|
|
});
|
|
|
|
it("should merge updates with existing settings", async () => {
|
|
const initial: GlobalSettings = {
|
|
...DEFAULT_GLOBAL_SETTINGS,
|
|
theme: "dark",
|
|
maxConcurrency: 3,
|
|
};
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<GlobalSettings> = {
|
|
theme: "light",
|
|
};
|
|
|
|
const updated = await settingsService.updateGlobalSettings(updates);
|
|
|
|
expect(updated.theme).toBe("light");
|
|
expect(updated.maxConcurrency).toBe(3); // Preserved from initial
|
|
});
|
|
|
|
it("should deep merge keyboard shortcuts", async () => {
|
|
const updates: Partial<GlobalSettings> = {
|
|
keyboardShortcuts: {
|
|
board: "B",
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateGlobalSettings(updates);
|
|
|
|
expect(updated.keyboardShortcuts.board).toBe("B");
|
|
expect(updated.keyboardShortcuts.agent).toBe(
|
|
DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts.agent
|
|
);
|
|
});
|
|
|
|
it("should create data directory if it does not exist", async () => {
|
|
const newDataDir = path.join(os.tmpdir(), `new-data-dir-${Date.now()}`);
|
|
const newService = new SettingsService(newDataDir);
|
|
|
|
await newService.updateGlobalSettings({ theme: "light" });
|
|
|
|
const stats = await fs.stat(newDataDir);
|
|
expect(stats.isDirectory()).toBe(true);
|
|
|
|
await fs.rm(newDataDir, { recursive: true, force: true });
|
|
});
|
|
});
|
|
|
|
describe("hasGlobalSettings", () => {
|
|
it("should return false when settings file does not exist", async () => {
|
|
const exists = await settingsService.hasGlobalSettings();
|
|
expect(exists).toBe(false);
|
|
});
|
|
|
|
it("should return true when settings file exists", async () => {
|
|
await settingsService.updateGlobalSettings({ theme: "light" });
|
|
const exists = await settingsService.hasGlobalSettings();
|
|
expect(exists).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("getCredentials", () => {
|
|
it("should return default credentials when file does not exist", async () => {
|
|
const credentials = await settingsService.getCredentials();
|
|
expect(credentials).toEqual(DEFAULT_CREDENTIALS);
|
|
});
|
|
|
|
it("should read and return existing credentials", async () => {
|
|
const customCredentials: Credentials = {
|
|
...DEFAULT_CREDENTIALS,
|
|
apiKeys: {
|
|
anthropic: "sk-test-key",
|
|
google: "",
|
|
openai: "",
|
|
},
|
|
};
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
await fs.writeFile(credentialsPath, JSON.stringify(customCredentials, null, 2));
|
|
|
|
const credentials = await settingsService.getCredentials();
|
|
expect(credentials.apiKeys.anthropic).toBe("sk-test-key");
|
|
});
|
|
|
|
it("should merge with defaults for missing api keys", async () => {
|
|
const partialCredentials = {
|
|
version: CREDENTIALS_VERSION,
|
|
apiKeys: {
|
|
anthropic: "sk-test",
|
|
},
|
|
};
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
await fs.writeFile(credentialsPath, JSON.stringify(partialCredentials, null, 2));
|
|
|
|
const credentials = await settingsService.getCredentials();
|
|
expect(credentials.apiKeys.anthropic).toBe("sk-test");
|
|
expect(credentials.apiKeys.google).toBe("");
|
|
expect(credentials.apiKeys.openai).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("updateCredentials", () => {
|
|
it("should create credentials file with updates", async () => {
|
|
const updates: Partial<Credentials> = {
|
|
apiKeys: {
|
|
anthropic: "sk-test-key",
|
|
google: "",
|
|
openai: "",
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateCredentials(updates);
|
|
|
|
expect(updated.apiKeys.anthropic).toBe("sk-test-key");
|
|
expect(updated.version).toBe(CREDENTIALS_VERSION);
|
|
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
const fileContent = await fs.readFile(credentialsPath, "utf-8");
|
|
const saved = JSON.parse(fileContent);
|
|
expect(saved.apiKeys.anthropic).toBe("sk-test-key");
|
|
});
|
|
|
|
it("should merge updates with existing credentials", async () => {
|
|
const initial: Credentials = {
|
|
...DEFAULT_CREDENTIALS,
|
|
apiKeys: {
|
|
anthropic: "sk-initial",
|
|
google: "google-key",
|
|
openai: "",
|
|
},
|
|
};
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
await fs.writeFile(credentialsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<Credentials> = {
|
|
apiKeys: {
|
|
anthropic: "sk-updated",
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateCredentials(updates);
|
|
|
|
expect(updated.apiKeys.anthropic).toBe("sk-updated");
|
|
expect(updated.apiKeys.google).toBe("google-key"); // Preserved
|
|
});
|
|
|
|
it("should deep merge api keys", async () => {
|
|
const initial: Credentials = {
|
|
...DEFAULT_CREDENTIALS,
|
|
apiKeys: {
|
|
anthropic: "sk-anthropic",
|
|
google: "google-key",
|
|
openai: "openai-key",
|
|
},
|
|
};
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
await fs.writeFile(credentialsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<Credentials> = {
|
|
apiKeys: {
|
|
openai: "new-openai-key",
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateCredentials(updates);
|
|
|
|
expect(updated.apiKeys.anthropic).toBe("sk-anthropic");
|
|
expect(updated.apiKeys.google).toBe("google-key");
|
|
expect(updated.apiKeys.openai).toBe("new-openai-key");
|
|
});
|
|
});
|
|
|
|
describe("getMaskedCredentials", () => {
|
|
it("should return masked credentials for empty keys", async () => {
|
|
const masked = await settingsService.getMaskedCredentials();
|
|
expect(masked.anthropic.configured).toBe(false);
|
|
expect(masked.anthropic.masked).toBe("");
|
|
expect(masked.google.configured).toBe(false);
|
|
expect(masked.openai.configured).toBe(false);
|
|
});
|
|
|
|
it("should mask keys correctly", async () => {
|
|
await settingsService.updateCredentials({
|
|
apiKeys: {
|
|
anthropic: "sk-ant-api03-1234567890abcdef",
|
|
google: "AIzaSy1234567890abcdef",
|
|
openai: "sk-1234567890abcdef",
|
|
},
|
|
});
|
|
|
|
const masked = await settingsService.getMaskedCredentials();
|
|
expect(masked.anthropic.configured).toBe(true);
|
|
expect(masked.anthropic.masked).toBe("sk-a...cdef");
|
|
expect(masked.google.configured).toBe(true);
|
|
expect(masked.google.masked).toBe("AIza...cdef");
|
|
expect(masked.openai.configured).toBe(true);
|
|
expect(masked.openai.masked).toBe("sk-1...cdef");
|
|
});
|
|
|
|
it("should handle short keys", async () => {
|
|
await settingsService.updateCredentials({
|
|
apiKeys: {
|
|
anthropic: "short",
|
|
google: "",
|
|
openai: "",
|
|
},
|
|
});
|
|
|
|
const masked = await settingsService.getMaskedCredentials();
|
|
expect(masked.anthropic.configured).toBe(true);
|
|
expect(masked.anthropic.masked).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("hasCredentials", () => {
|
|
it("should return false when credentials file does not exist", async () => {
|
|
const exists = await settingsService.hasCredentials();
|
|
expect(exists).toBe(false);
|
|
});
|
|
|
|
it("should return true when credentials file exists", async () => {
|
|
await settingsService.updateCredentials({
|
|
apiKeys: { anthropic: "test", google: "", openai: "" },
|
|
});
|
|
const exists = await settingsService.hasCredentials();
|
|
expect(exists).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("getProjectSettings", () => {
|
|
it("should return default settings when file does not exist", async () => {
|
|
const settings = await settingsService.getProjectSettings(testProjectDir);
|
|
expect(settings).toEqual(DEFAULT_PROJECT_SETTINGS);
|
|
});
|
|
|
|
it("should read and return existing project settings", async () => {
|
|
const customSettings: ProjectSettings = {
|
|
...DEFAULT_PROJECT_SETTINGS,
|
|
theme: "light",
|
|
useWorktrees: true,
|
|
};
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
await fs.mkdir(automakerDir, { recursive: true });
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2));
|
|
|
|
const settings = await settingsService.getProjectSettings(testProjectDir);
|
|
expect(settings.theme).toBe("light");
|
|
expect(settings.useWorktrees).toBe(true);
|
|
});
|
|
|
|
it("should merge with defaults for missing properties", async () => {
|
|
const partialSettings = {
|
|
version: PROJECT_SETTINGS_VERSION,
|
|
theme: "dark",
|
|
};
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
await fs.mkdir(automakerDir, { recursive: true });
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(partialSettings, null, 2));
|
|
|
|
const settings = await settingsService.getProjectSettings(testProjectDir);
|
|
expect(settings.theme).toBe("dark");
|
|
expect(settings.version).toBe(PROJECT_SETTINGS_VERSION);
|
|
});
|
|
});
|
|
|
|
describe("updateProjectSettings", () => {
|
|
it("should create project settings file with updates", async () => {
|
|
const updates: Partial<ProjectSettings> = {
|
|
theme: "light",
|
|
useWorktrees: true,
|
|
};
|
|
|
|
const updated = await settingsService.updateProjectSettings(testProjectDir, updates);
|
|
|
|
expect(updated.theme).toBe("light");
|
|
expect(updated.useWorktrees).toBe(true);
|
|
expect(updated.version).toBe(PROJECT_SETTINGS_VERSION);
|
|
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
const fileContent = await fs.readFile(settingsPath, "utf-8");
|
|
const saved = JSON.parse(fileContent);
|
|
expect(saved.theme).toBe("light");
|
|
expect(saved.useWorktrees).toBe(true);
|
|
});
|
|
|
|
it("should merge updates with existing project settings", async () => {
|
|
const initial: ProjectSettings = {
|
|
...DEFAULT_PROJECT_SETTINGS,
|
|
theme: "dark",
|
|
useWorktrees: false,
|
|
};
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
await fs.mkdir(automakerDir, { recursive: true });
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<ProjectSettings> = {
|
|
theme: "light",
|
|
};
|
|
|
|
const updated = await settingsService.updateProjectSettings(testProjectDir, updates);
|
|
|
|
expect(updated.theme).toBe("light");
|
|
expect(updated.useWorktrees).toBe(false); // Preserved
|
|
});
|
|
|
|
it("should deep merge board background", async () => {
|
|
const initial: ProjectSettings = {
|
|
...DEFAULT_PROJECT_SETTINGS,
|
|
boardBackground: {
|
|
imagePath: "/path/to/image.jpg",
|
|
cardOpacity: 0.8,
|
|
columnOpacity: 0.9,
|
|
columnBorderEnabled: true,
|
|
cardGlassmorphism: false,
|
|
cardBorderEnabled: true,
|
|
cardBorderOpacity: 0.5,
|
|
hideScrollbar: false,
|
|
},
|
|
};
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
await fs.mkdir(automakerDir, { recursive: true });
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<ProjectSettings> = {
|
|
boardBackground: {
|
|
cardOpacity: 0.9,
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateProjectSettings(testProjectDir, updates);
|
|
|
|
expect(updated.boardBackground?.imagePath).toBe("/path/to/image.jpg");
|
|
expect(updated.boardBackground?.cardOpacity).toBe(0.9);
|
|
expect(updated.boardBackground?.columnOpacity).toBe(0.9);
|
|
});
|
|
|
|
it("should create .automaker directory if it does not exist", async () => {
|
|
const newProjectDir = path.join(os.tmpdir(), `new-project-${Date.now()}`);
|
|
|
|
await settingsService.updateProjectSettings(newProjectDir, { theme: "light" });
|
|
|
|
const automakerDir = path.join(newProjectDir, ".automaker");
|
|
const stats = await fs.stat(automakerDir);
|
|
expect(stats.isDirectory()).toBe(true);
|
|
|
|
await fs.rm(newProjectDir, { recursive: true, force: true });
|
|
});
|
|
});
|
|
|
|
describe("hasProjectSettings", () => {
|
|
it("should return false when project settings file does not exist", async () => {
|
|
const exists = await settingsService.hasProjectSettings(testProjectDir);
|
|
expect(exists).toBe(false);
|
|
});
|
|
|
|
it("should return true when project settings file exists", async () => {
|
|
await settingsService.updateProjectSettings(testProjectDir, { theme: "light" });
|
|
const exists = await settingsService.hasProjectSettings(testProjectDir);
|
|
expect(exists).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("migrateFromLocalStorage", () => {
|
|
it("should migrate global settings from localStorage data", async () => {
|
|
const localStorageData = {
|
|
"automaker-storage": JSON.stringify({
|
|
state: {
|
|
theme: "light",
|
|
sidebarOpen: false,
|
|
maxConcurrency: 5,
|
|
},
|
|
}),
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.migratedGlobalSettings).toBe(true);
|
|
expect(result.migratedCredentials).toBe(false);
|
|
expect(result.migratedProjectCount).toBe(0);
|
|
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.theme).toBe("light");
|
|
expect(settings.sidebarOpen).toBe(false);
|
|
expect(settings.maxConcurrency).toBe(5);
|
|
});
|
|
|
|
it("should migrate credentials from localStorage data", async () => {
|
|
const localStorageData = {
|
|
"automaker-storage": JSON.stringify({
|
|
state: {
|
|
apiKeys: {
|
|
anthropic: "sk-test-key",
|
|
google: "google-key",
|
|
openai: "openai-key",
|
|
},
|
|
},
|
|
}),
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.migratedCredentials).toBe(true);
|
|
|
|
const credentials = await settingsService.getCredentials();
|
|
expect(credentials.apiKeys.anthropic).toBe("sk-test-key");
|
|
expect(credentials.apiKeys.google).toBe("google-key");
|
|
expect(credentials.apiKeys.openai).toBe("openai-key");
|
|
});
|
|
|
|
it("should migrate project settings from localStorage data", async () => {
|
|
const localStorageData = {
|
|
"automaker-storage": JSON.stringify({
|
|
state: {
|
|
projects: [
|
|
{
|
|
id: "proj1",
|
|
name: "Project 1",
|
|
path: testProjectDir,
|
|
theme: "light",
|
|
},
|
|
],
|
|
boardBackgroundByProject: {
|
|
[testProjectDir]: {
|
|
imagePath: "/path/to/image.jpg",
|
|
cardOpacity: 0.8,
|
|
columnOpacity: 0.9,
|
|
columnBorderEnabled: true,
|
|
cardGlassmorphism: false,
|
|
cardBorderEnabled: true,
|
|
cardBorderOpacity: 0.5,
|
|
hideScrollbar: false,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.migratedProjectCount).toBe(1);
|
|
|
|
const projectSettings = await settingsService.getProjectSettings(testProjectDir);
|
|
expect(projectSettings.theme).toBe("light");
|
|
expect(projectSettings.boardBackground?.imagePath).toBe("/path/to/image.jpg");
|
|
});
|
|
|
|
it("should handle direct localStorage values", async () => {
|
|
const localStorageData = {
|
|
"automaker:lastProjectDir": "/path/to/project",
|
|
"file-browser-recent-folders": JSON.stringify(["/path1", "/path2"]),
|
|
"worktree-panel-collapsed": "true",
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(true);
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.lastProjectDir).toBe("/path/to/project");
|
|
expect(settings.recentFolders).toEqual(["/path1", "/path2"]);
|
|
expect(settings.worktreePanelCollapsed).toBe(true);
|
|
});
|
|
|
|
it("should handle invalid JSON gracefully", async () => {
|
|
const localStorageData = {
|
|
"automaker-storage": "invalid json",
|
|
"file-browser-recent-folders": "invalid json",
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should handle migration errors gracefully", async () => {
|
|
// Create a read-only directory to cause write errors
|
|
const readOnlyDir = path.join(os.tmpdir(), `readonly-${Date.now()}`);
|
|
await fs.mkdir(readOnlyDir, { recursive: true });
|
|
await fs.chmod(readOnlyDir, 0o444);
|
|
|
|
const readOnlyService = new SettingsService(readOnlyDir);
|
|
const localStorageData = {
|
|
"automaker-storage": JSON.stringify({
|
|
state: { theme: "light" },
|
|
}),
|
|
};
|
|
|
|
const result = await readOnlyService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
|
|
await fs.chmod(readOnlyDir, 0o755);
|
|
await fs.rm(readOnlyDir, { recursive: true, force: true });
|
|
});
|
|
});
|
|
|
|
describe("getDataDir", () => {
|
|
it("should return the data directory path", () => {
|
|
const dataDir = settingsService.getDataDir();
|
|
expect(dataDir).toBe(testDataDir);
|
|
});
|
|
});
|
|
|
|
describe("atomicWriteJson", () => {
|
|
it("should handle write errors and clean up temp file", async () => {
|
|
// Create a read-only directory to cause write errors
|
|
const readOnlyDir = path.join(os.tmpdir(), `readonly-${Date.now()}`);
|
|
await fs.mkdir(readOnlyDir, { recursive: true });
|
|
await fs.chmod(readOnlyDir, 0o444);
|
|
|
|
const readOnlyService = new SettingsService(readOnlyDir);
|
|
|
|
await expect(
|
|
readOnlyService.updateGlobalSettings({ theme: "light" })
|
|
).rejects.toThrow();
|
|
|
|
await fs.chmod(readOnlyDir, 0o755);
|
|
await fs.rm(readOnlyDir, { recursive: true, force: true });
|
|
});
|
|
});
|
|
});
|
|
|