import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { SettingsService } from '@main/services/SettingsService.js'; describe('SettingsService', () => { let dir: string; let svc: SettingsService; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'inkling-settings-')); svc = new SettingsService(dir); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); it('load() returns empty object when file does not exist', async () => { const s = await svc.load(); expect(s).toEqual({}); }); it('load() returns empty object on corrupted JSON (no throw)', async () => { writeFileSync(join(dir, 'settings.json'), '{ this is not json'); const s = await svc.load(); expect(s).toEqual({}); }); it('load() caches result — second call does not re-read file', async () => { await svc.setOllama({ endpoint: 'http://localhost:11434', model: 'gemma4:e4b' }); const before = await svc.load(); // 외부에서 파일 변경 writeFileSync(join(dir, 'settings.json'), JSON.stringify({ ollama: { endpoint: 'http://lan:11434', model: 'gemma4:26b' } })); const after = await svc.load(); // 캐시 적용 — 파일 변경 무시 expect(after).toEqual(before); }); it('setOllama() throws on non-URL endpoint', async () => { await expect( svc.setOllama({ endpoint: 'not-a-url', model: 'gemma4:e4b' }) ).rejects.toThrow(); }); it('setOllama() persists to disk with valid JSON', async () => { await svc.setOllama({ endpoint: 'http://localhost:11435', model: 'gemma4:e4b' }); const raw = readFileSync(join(dir, 'settings.json'), 'utf8'); const parsed = JSON.parse(raw); expect(parsed.ollama.endpoint).toBe('http://localhost:11435'); expect(parsed.ollama.model).toBe('gemma4:e4b'); }); it('setOllama() atomic write — tmp file does not remain', async () => { await svc.setOllama({ endpoint: 'http://localhost:11434', model: 'gemma4:e4b' }); expect(existsSync(join(dir, 'settings.json.tmp'))).toBe(false); expect(existsSync(join(dir, 'settings.json'))).toBe(true); }); describe('v0.3.0 Cut E — sync settings', () => { it('getSyncRepoUrl() defaults to null', async () => { expect(await svc.getSyncRepoUrl()).toBeNull(); }); it('setSyncRepoUrl() / getSyncRepoUrl() round-trip', async () => { await svc.setSyncRepoUrl('git@gitea.example:user/notes.git'); expect(await svc.getSyncRepoUrl()).toBe('git@gitea.example:user/notes.git'); // setting null clears await svc.setSyncRepoUrl(null); expect(await svc.getSyncRepoUrl()).toBeNull(); }); it('isAutoSyncEnabled() defaults to true', async () => { expect(await svc.isAutoSyncEnabled()).toBe(true); }); it('setAutoSyncEnabled() persists', async () => { await svc.setAutoSyncEnabled(false); expect(await svc.isAutoSyncEnabled()).toBe(false); await svc.setAutoSyncEnabled(true); expect(await svc.isAutoSyncEnabled()).toBe(true); }); it('getSyncIntervalMin() defaults to 30', async () => { expect(await svc.getSyncIntervalMin()).toBe(30); }); it('setSyncIntervalMin() persists + rejects values < 5 / non-integer', async () => { await svc.setSyncIntervalMin(15); expect(await svc.getSyncIntervalMin()).toBe(15); await expect(svc.setSyncIntervalMin(3)).rejects.toThrow(); await expect(svc.setSyncIntervalMin(10.5)).rejects.toThrow(); }); }); });