feat(desktop): Electron all-in-one desktop shell (Mac/Windows) embedding the server

Add a `desktop/` Electron app that embeds the existing Node server + node-pty
(all-in-one): the window loads http://127.0.0.1:<port>/ and reuses the frontend
unchanged; other LAN devices can still connect. The server needs zero changes —
startServer(cfg)/loadConfig already support programmatic embedding.

- Pure, unit-tested modules: port, shell, server-config, deep-link, notify-policy,
  notifications, live-poll, prefs, settings-store (94 tests; desktop/src ~97% cov)
- Electron glue: main/window/tray/menu/preload/embedded-server/logger
  (hardened: contextIsolation, sandbox, deny foreign-origin navigation)
- Native value: OS notifications driven by /live-sessions status, tray, deep links
- Packaging (electron-builder -> arm64 .dmg): ships dist/ + public/ + node_modules
  on-disk under Resources so the server resolves its deps from /Applications;
  node-pty rebuilt for the Electron ABI
- Docs: docs/DESKTOP_PLAN.md; PROGRESS_LOG updated

Verified: desktop tsc clean; 1401 tests pass; coverage >=80% (desktop/src 97/95/100/97);
.dmg built and launch-tested on arm64 (server boots, UI serves 200, node-pty loads).
This commit is contained in:
Yaojia Wang
2026-07-02 06:13:17 +02:00
parent 2af57e6686
commit cf8cfccab4
39 changed files with 7781 additions and 0 deletions

218
test/desktop/prefs.test.ts Normal file
View File

@@ -0,0 +1,218 @@
/**
* 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 })
})
})