/** * test/desktop/prefs.test.ts — B3: unit tests for prefs defaults/validate/merge. * * Covers: defaultPrefs shape (and platform-independence); validatePrefs adopting * valid fields and falling back per-field on wrong types / out-of-range port / * empty shellPath / non-object raw / null / array; that it never throws; and * mergePrefs immutability + ignoring undefined patch fields. */ import { describe, test, expect } from 'vitest' import type { DesktopPrefs } from '../../desktop/src/types.js' import { defaultPrefs, validatePrefs, mergePrefs } from '../../desktop/src/prefs.js' const EXPECTED_DEFAULTS: DesktopPrefs = { port: null, lanSharing: false, shellPath: null, openAtLogin: false, notifyOnApproval: true, notifyOnStatusChange: true, } describe('defaultPrefs', () => { test('returns the documented baseline shape (LAN sharing off by default)', () => { // Arrange / Act const prefs = defaultPrefs('darwin') // Assert expect(prefs).toEqual(EXPECTED_DEFAULTS) }) test('is platform-independent (same defaults across platforms)', () => { // Arrange / Act const darwin = defaultPrefs('darwin') const win32 = defaultPrefs('win32') const linux = defaultPrefs('linux') // Assert expect(win32).toEqual(darwin) expect(linux).toEqual(darwin) }) test('returns a new object each call (no shared mutable state)', () => { // Arrange / Act const a = defaultPrefs('darwin') const b = defaultPrefs('darwin') // Assert expect(a).not.toBe(b) }) }) describe('validatePrefs', () => { test('adopts every field from a fully-valid object', () => { // Arrange const raw = { port: 8080, lanSharing: true, shellPath: '/bin/bash', openAtLogin: true, notifyOnApproval: false, notifyOnStatusChange: false, } // Act const prefs = validatePrefs(raw, 'darwin') // Assert expect(prefs).toEqual(raw) }) test('falls back to defaults when raw is not an object', () => { // Assert — strings, numbers, booleans are not prefs objects expect(validatePrefs('nope', 'darwin')).toEqual(EXPECTED_DEFAULTS) expect(validatePrefs(42, 'darwin')).toEqual(EXPECTED_DEFAULTS) expect(validatePrefs(true, 'darwin')).toEqual(EXPECTED_DEFAULTS) }) test('falls back to defaults when raw is null', () => { expect(validatePrefs(null, 'darwin')).toEqual(EXPECTED_DEFAULTS) }) test('falls back to defaults when raw is undefined', () => { expect(validatePrefs(undefined, 'darwin')).toEqual(EXPECTED_DEFAULTS) }) test('falls back to defaults when raw is an array (arrays are not records)', () => { expect(validatePrefs([1, 2, 3], 'darwin')).toEqual(EXPECTED_DEFAULTS) }) test('falls back per-field on wrong types, keeping valid siblings', () => { // Arrange — lanSharing is a number (invalid); port/shellPath are valid const raw = { port: 3001, lanSharing: 1, shellPath: '/bin/zsh', openAtLogin: 'yes', notifyOnApproval: null, notifyOnStatusChange: 0, } // Act const prefs = validatePrefs(raw, 'darwin') // Assert — valid fields adopted, invalid fields fall back to defaults expect(prefs.port).toBe(3001) expect(prefs.shellPath).toBe('/bin/zsh') expect(prefs.lanSharing).toBe(false) expect(prefs.openAtLogin).toBe(false) expect(prefs.notifyOnApproval).toBe(true) expect(prefs.notifyOnStatusChange).toBe(true) }) test('rejects out-of-range and non-integer ports, keeping default null', () => { expect(validatePrefs({ port: 0 }, 'darwin').port).toBeNull() expect(validatePrefs({ port: 65536 }, 'darwin').port).toBeNull() expect(validatePrefs({ port: -1 }, 'darwin').port).toBeNull() expect(validatePrefs({ port: 3.5 }, 'darwin').port).toBeNull() expect(validatePrefs({ port: NaN }, 'darwin').port).toBeNull() expect(validatePrefs({ port: '3000' }, 'darwin').port).toBeNull() }) test('accepts boundary ports (1 and 65535)', () => { expect(validatePrefs({ port: 1 }, 'darwin').port).toBe(1) expect(validatePrefs({ port: 65535 }, 'darwin').port).toBe(65535) }) test('accepts an explicit null port', () => { expect(validatePrefs({ port: null }, 'darwin').port).toBeNull() }) test('rejects empty and whitespace-only shellPath, keeping default null', () => { expect(validatePrefs({ shellPath: '' }, 'darwin').shellPath).toBeNull() expect(validatePrefs({ shellPath: ' ' }, 'darwin').shellPath).toBeNull() }) test('accepts an explicit null shellPath', () => { expect(validatePrefs({ shellPath: null }, 'darwin').shellPath).toBeNull() }) test('rejects a non-string shellPath, keeping default null', () => { expect(validatePrefs({ shellPath: 123 }, 'darwin').shellPath).toBeNull() }) test('never throws on hostile input shapes', () => { // Assert — a grab-bag of malformed inputs must all return defaults, no throw const hostile: unknown[] = [Symbol('x'), () => 0, new Map(), 0n] for (const raw of hostile) { expect(() => validatePrefs(raw, 'darwin')).not.toThrow() expect(validatePrefs(raw, 'darwin')).toEqual(EXPECTED_DEFAULTS) } }) }) describe('mergePrefs', () => { test('applies defined patch fields over the base', () => { // Arrange const base = defaultPrefs('darwin') // Act const merged = mergePrefs(base, { port: 4000, lanSharing: true }) // Assert expect(merged.port).toBe(4000) expect(merged.lanSharing).toBe(true) expect(merged.shellPath).toBeNull() }) test('ignores undefined patch fields (does not clobber base)', () => { // Arrange const base = mergePrefs(defaultPrefs('darwin'), { port: 5000, shellPath: '/bin/bash' }) // Act — an explicit-undefined patch field must be ignored const merged = mergePrefs(base, { port: undefined, lanSharing: true }) // Assert expect(merged.port).toBe(5000) expect(merged.shellPath).toBe('/bin/bash') expect(merged.lanSharing).toBe(true) }) test('applies falsy-but-defined patch fields (false / null)', () => { // Arrange const base = mergePrefs(defaultPrefs('darwin'), { port: 6000, notifyOnApproval: true }) // Act const merged = mergePrefs(base, { port: null, notifyOnApproval: false }) // Assert — null/false are defined values and must overwrite expect(merged.port).toBeNull() expect(merged.notifyOnApproval).toBe(false) }) test('does not mutate the base object (immutability)', () => { // Arrange const base = defaultPrefs('darwin') const snapshot = { ...base } // Act const merged = mergePrefs(base, { port: 7000 }) // Assert — base untouched, a new object returned expect(base).toEqual(snapshot) expect(merged).not.toBe(base) }) test('does not mutate the patch object', () => { // Arrange const base = defaultPrefs('darwin') const patch = { port: 8000 } // Act mergePrefs(base, patch) // Assert expect(patch).toEqual({ port: 8000 }) }) })