The desktop shell pinned USE_TMUX=0 on every platform, not just Windows. Without tmux the shell is a direct child of the embedded server, so SessionManager.shutdown takes the kill branch and quitting the app — the tray Quit, a logout, an update — ends every session with it. That defeats the app's premise. The point is to hand Claude Code a task, walk away, and reconnect from another device; a session that cannot survive the window closing does not do that. With tmux the node-pty process is only a tmux CLIENT, so shutdown detaches and the shell keeps running, which is exactly what the standalone server has always done (USE_TMUX unset → 'auto' → is the binary present?). Non-Windows now defaults to 'auto'. Windows stays forced off — tmux is a *nix keepalive. An explicit ambient USE_TMUX still wins either way, so USE_TMUX=0 remains the opt-out. Tests assert the end-to-end result, not just the emitted string: buildServerEnv fed through loadConfig yields useTmux === true with tmux installed, and false for an explicit opt-out. The original bug was these two layers disagreeing about what a value meant, so checking the string alone would not have caught it. Sessions started in the app before this change are NOT retroactively protected — they have no tmux session behind them and will still die with the app. Only sessions created after the new build is installed survive.
136 lines
4.9 KiB
TypeScript
136 lines
4.9 KiB
TypeScript
/**
|
|
* test/desktop/server-config.test.ts — unit tests for buildServerEnv (B1).
|
|
*
|
|
* Covers the PORT / BIND_HOST / SHELL_PATH / USE_TMUX mapping, the immutability
|
|
* guarantee (input.env untouched), and pass-through of unrelated env vars.
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process'
|
|
|
|
import { describe, test, expect } from 'vitest'
|
|
import { buildServerEnv } from '../../desktop/src/server-config.js'
|
|
import { loadConfig } from '../../src/config.js'
|
|
import type { DesktopPrefs } from '../../desktop/src/types.js'
|
|
|
|
const tmuxInstalled = (() => {
|
|
try {
|
|
execFileSync('tmux', ['-V'], { stdio: 'ignore' })
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
})()
|
|
|
|
const BASE_PREFS: DesktopPrefs = {
|
|
port: null,
|
|
lanSharing: false,
|
|
shellPath: null,
|
|
openAtLogin: false,
|
|
notifyOnApproval: true,
|
|
notifyOnStatusChange: true,
|
|
}
|
|
|
|
describe('buildServerEnv', () => {
|
|
test('sets PORT to the stringified port', () => {
|
|
const result = buildServerEnv({ prefs: BASE_PREFS, port: 4321, platform: 'darwin', env: {} })
|
|
expect(result.PORT).toBe('4321')
|
|
})
|
|
|
|
test('binds 127.0.0.1 when lanSharing is off', () => {
|
|
const result = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'darwin', env: {} })
|
|
expect(result.BIND_HOST).toBe('127.0.0.1')
|
|
})
|
|
|
|
test('binds 0.0.0.0 when lanSharing is on', () => {
|
|
// Arrange
|
|
const prefs = { ...BASE_PREFS, lanSharing: true }
|
|
|
|
// Act
|
|
const result = buildServerEnv({ prefs, port: 3000, platform: 'darwin', env: {} })
|
|
|
|
// Assert
|
|
expect(result.BIND_HOST).toBe('0.0.0.0')
|
|
})
|
|
|
|
test('uses prefs.shellPath as SHELL_PATH when provided (overriding the platform default)', () => {
|
|
// Arrange
|
|
const prefs = { ...BASE_PREFS, shellPath: '/usr/bin/fish' }
|
|
|
|
// Act
|
|
const result = buildServerEnv({ prefs, port: 3000, platform: 'darwin', env: { SHELL: '/bin/zsh' } })
|
|
|
|
// Assert
|
|
expect(result.SHELL_PATH).toBe('/usr/bin/fish')
|
|
})
|
|
|
|
test('falls back to the platform default SHELL_PATH when prefs.shellPath is null', () => {
|
|
const win = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'win32', env: {} })
|
|
expect(win.SHELL_PATH).toBe('powershell.exe')
|
|
|
|
const mac = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'darwin', env: { SHELL: '/bin/zsh' } })
|
|
expect(mac.SHELL_PATH).toBe('/bin/zsh')
|
|
})
|
|
|
|
test('forces USE_TMUX off on win32 even when the ambient env enables it', () => {
|
|
const result = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'win32', env: { USE_TMUX: '1' } })
|
|
expect(result.USE_TMUX).toBe('0')
|
|
})
|
|
|
|
test('passes ambient USE_TMUX through on non-Windows platforms', () => {
|
|
const result = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'linux', env: { USE_TMUX: '1' } })
|
|
expect(result.USE_TMUX).toBe('1')
|
|
})
|
|
|
|
test('defaults USE_TMUX to auto-detect on non-Windows when the env is unset', () => {
|
|
// Not '0'. Without tmux the shell is a direct child of the embedded server, so
|
|
// quitting the app kills every session — which defeats the point of walking away
|
|
// from a task and reconnecting later. 'auto' means "use tmux if it is installed".
|
|
const result = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'linux', env: {} })
|
|
expect(result.USE_TMUX).toBe('auto')
|
|
})
|
|
|
|
test('an explicit ambient USE_TMUX=0 still wins — opting out stays possible', () => {
|
|
const result = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'darwin', env: { USE_TMUX: '0' } })
|
|
expect(result.USE_TMUX).toBe('0')
|
|
})
|
|
|
|
// The bug this guards against was the two layers disagreeing: buildServerEnv emitted
|
|
// a value that loadConfig then resolved to something else than intended. Assert the
|
|
// end-to-end result, not just the string.
|
|
test('the default resolves to tmux ON once loadConfig sees it', () => {
|
|
if (!tmuxInstalled) return // 'auto' correctly resolves to false without the binary
|
|
const env = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'darwin', env: {} })
|
|
expect(loadConfig({ ...env, BIND_HOST: '127.0.0.1' }).useTmux).toBe(true)
|
|
})
|
|
|
|
test('an explicit opt-out resolves to tmux OFF through loadConfig', () => {
|
|
const env = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'darwin', env: { USE_TMUX: '0' } })
|
|
expect(loadConfig({ ...env, BIND_HOST: '127.0.0.1' }).useTmux).toBe(false)
|
|
})
|
|
|
|
test('does not mutate the input env', () => {
|
|
// Arrange
|
|
const env = { PATH: '/usr/bin', USE_TMUX: '1' }
|
|
const snapshot = { ...env }
|
|
|
|
// Act
|
|
buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'win32', env })
|
|
|
|
// Assert
|
|
expect(env).toEqual(snapshot)
|
|
})
|
|
|
|
test('preserves unrelated ambient env vars', () => {
|
|
// Arrange
|
|
const env = { PATH: '/usr/bin', HOME: '/Users/me', CUSTOM: 'x' }
|
|
|
|
// Act
|
|
const result = buildServerEnv({ prefs: BASE_PREFS, port: 3000, platform: 'darwin', env })
|
|
|
|
// Assert
|
|
expect(result.PATH).toBe('/usr/bin')
|
|
expect(result.HOME).toBe('/Users/me')
|
|
expect(result.CUSTOM).toBe('x')
|
|
})
|
|
})
|