mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge main into massive-terminal-upgrade
Resolves merge conflicts: - apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger - apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions - apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling) - apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes - apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
611
apps/server/tests/unit/services/settings-service.test.ts
Normal file
611
apps/server/tests/unit/services/settings-service.test.ts
Normal file
@@ -0,0 +1,611 @@
|
||||
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',
|
||||
},
|
||||
};
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCredentials', () => {
|
||||
it('should create credentials file with updates', async () => {
|
||||
const updates: Partial<Credentials> = {
|
||||
apiKeys: {
|
||||
anthropic: 'sk-test-key',
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
||||
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');
|
||||
});
|
||||
|
||||
it('should deep merge api keys', async () => {
|
||||
const initial: Credentials = {
|
||||
...DEFAULT_CREDENTIALS,
|
||||
apiKeys: {
|
||||
anthropic: 'sk-anthropic',
|
||||
},
|
||||
};
|
||||
const credentialsPath = path.join(testDataDir, 'credentials.json');
|
||||
await fs.writeFile(credentialsPath, JSON.stringify(initial, null, 2));
|
||||
|
||||
const updates: Partial<Credentials> = {
|
||||
apiKeys: {
|
||||
anthropic: 'sk-updated-anthropic',
|
||||
},
|
||||
};
|
||||
|
||||
const updated = await settingsService.updateCredentials(updates);
|
||||
|
||||
expect(updated.apiKeys.anthropic).toBe('sk-updated-anthropic');
|
||||
});
|
||||
});
|
||||
|
||||
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('');
|
||||
});
|
||||
|
||||
it('should mask keys correctly', async () => {
|
||||
await settingsService.updateCredentials({
|
||||
apiKeys: {
|
||||
anthropic: 'sk-ant-api03-1234567890abcdef',
|
||||
},
|
||||
});
|
||||
|
||||
const masked = await settingsService.getMaskedCredentials();
|
||||
expect(masked.anthropic.configured).toBe(true);
|
||||
expect(masked.anthropic.masked).toBe('sk-a...cdef');
|
||||
});
|
||||
|
||||
it('should handle short keys', async () => {
|
||||
await settingsService.updateCredentials({
|
||||
apiKeys: {
|
||||
anthropic: 'short',
|
||||
},
|
||||
});
|
||||
|
||||
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' },
|
||||
});
|
||||
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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user