/** * test/desktop/settings-store.test.ts — B3: unit tests for the on-disk prefs store. * * Uses a REAL temp dir under os.tmpdir() (unique per test via process.pid + an * incrementing counter — Math.random/Date.now are intentionally avoided). Covers: * missing file → defaults (no warn); set→get round-trip; on-disk file contents; * corrupt JSON → defaults + warn; and a write failure → error logged, merged * result still returned. A spy Logger asserts the logging contract. */ import { describe, test, expect, afterEach, vi } from 'vitest' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import type { Logger } from '../../desktop/src/logger.js' import type { DesktopPrefs } from '../../desktop/src/types.js' import { createSettingsStore } from '../../desktop/src/settings-store.js' import { defaultPrefs } from '../../desktop/src/prefs.js' const PLATFORM: NodeJS.Platform = 'darwin' // ── unique temp-dir helper (no Math.random / Date.now) ────────────────────────── let dirCounter = 0 const createdDirs: string[] = [] function makeTempDir(): string { dirCounter += 1 const dir = path.join(os.tmpdir(), `web-terminal-store-test-${process.pid}-${dirCounter}`) fs.mkdirSync(dir, { recursive: true }) createdDirs.push(dir) return dir } interface SpyLogger extends Logger { info: ReturnType warn: ReturnType error: ReturnType } function makeSpyLogger(): SpyLogger { return { info: vi.fn(), warn: vi.fn(), error: vi.fn() } } afterEach(() => { for (const dir of createdDirs) { fs.rmSync(dir, { recursive: true, force: true }) } createdDirs.length = 0 }) describe('createSettingsStore.get', () => { test('returns defaults when the prefs file does not exist (and logs no warn)', () => { // Arrange const dir = makeTempDir() const logger = makeSpyLogger() const store = createSettingsStore(dir, PLATFORM, logger) // Act const prefs = store.get() // Assert — missing file is the normal first-run case, not a warning expect(prefs).toEqual(defaultPrefs(PLATFORM)) expect(logger.warn).not.toHaveBeenCalled() }) test('returns defaults and logs a warn when the file is corrupt JSON', () => { // Arrange const dir = makeTempDir() const logger = makeSpyLogger() fs.writeFileSync(path.join(dir, 'prefs.json'), '{ not valid json', 'utf8') const store = createSettingsStore(dir, PLATFORM, logger) // Act const prefs = store.get() // Assert expect(prefs).toEqual(defaultPrefs(PLATFORM)) expect(logger.warn).toHaveBeenCalledTimes(1) }) test('validates untrusted on-disk fields, falling back per bad field', () => { // Arrange — a hand-edited file with an out-of-range port and wrong-typed flag const dir = makeTempDir() const logger = makeSpyLogger() fs.writeFileSync( path.join(dir, 'prefs.json'), JSON.stringify({ port: 999999, lanSharing: 'yes', shellPath: '/bin/bash' }), 'utf8', ) const store = createSettingsStore(dir, PLATFORM, logger) // Act const prefs = store.get() // Assert expect(prefs.port).toBeNull() expect(prefs.lanSharing).toBe(false) expect(prefs.shellPath).toBe('/bin/bash') }) }) describe('createSettingsStore.set', () => { test('persists the patch and round-trips via a fresh store', () => { // Arrange const dir = makeTempDir() const store = createSettingsStore(dir, PLATFORM, makeSpyLogger()) // Act const merged = store.set({ port: 4321, lanSharing: true }) const reread = createSettingsStore(dir, PLATFORM, makeSpyLogger()).get() // Assert expect(merged.port).toBe(4321) expect(merged.lanSharing).toBe(true) expect(reread).toEqual(merged) }) test('merges over previously-saved prefs rather than replacing them', () => { // Arrange const dir = makeTempDir() const store = createSettingsStore(dir, PLATFORM, makeSpyLogger()) store.set({ port: 4321, shellPath: '/bin/zsh' }) // Act — a second partial set keeps the earlier shellPath const merged = store.set({ lanSharing: true }) // Assert expect(merged.port).toBe(4321) expect(merged.shellPath).toBe('/bin/zsh') expect(merged.lanSharing).toBe(true) }) test('writes pretty 2-space JSON to /prefs.json', () => { // Arrange const dir = makeTempDir() const store = createSettingsStore(dir, PLATFORM, makeSpyLogger()) // Act store.set({ port: 5000 }) const onDisk = fs.readFileSync(path.join(dir, 'prefs.json'), 'utf8') const parsed: unknown = JSON.parse(onDisk) // Assert — content matches and the file is indented (pretty-printed) expect(parsed).toMatchObject({ port: 5000 }) expect(onDisk).toContain('\n "port": 5000') }) test('creates the store directory if it does not yet exist (mkdir -p)', () => { // Arrange — point at a not-yet-created nested subdir const dir = path.join(makeTempDir(), 'nested', 'deeper') const store = createSettingsStore(dir, PLATFORM, makeSpyLogger()) // Act store.set({ port: 6000 }) // Assert expect(fs.existsSync(path.join(dir, 'prefs.json'))).toBe(true) }) test('logs an error but still returns the merged prefs when the write fails', () => { // Arrange — make the store "dir" live under a regular FILE, so mkdirSync fails const base = makeTempDir() const blocker = path.join(base, 'blocker') fs.writeFileSync(blocker, 'i am a file, not a directory', 'utf8') const unwritableDir = path.join(blocker, 'sub') const logger = makeSpyLogger() const store = createSettingsStore(unwritableDir, PLATFORM, logger) // Act const merged = store.set({ port: 7000 }) // Assert — best-effort: error surfaced, caller still gets the change expect(logger.error).toHaveBeenCalledTimes(1) expect(merged.port).toBe(7000) }) test('returned merged prefs are a fully-formed DesktopPrefs', () => { // Arrange const dir = makeTempDir() const store = createSettingsStore(dir, PLATFORM, makeSpyLogger()) // Act const merged: DesktopPrefs = store.set({ port: 8000 }) // Assert — every field present (defaults + patch) expect(Object.keys(merged).sort()).toEqual(Object.keys(defaultPrefs(PLATFORM)).sort()) }) })