Files
Sisyphus/apps/sisyphus/dev/daemon-process.test.mjs
T
excho0 833bca1641 feat(desktop): start and stop an owned local daemon (#167)
* feat(desktop): own local daemon startup and shutdown

* fix(desktop): prefer the CLI pool port and allow graceful daemon shutdown
2026-10-10 01:53:08 -04:00

159 lines
6.8 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { EventEmitter } from 'node:events'
import { createServer } from 'node:net'
import { spawn } from 'node:child_process'
import { DesktopDaemon, daemonShutdownGraceMs, localApiAddress, portIsUnused } from '../src/main/daemon-process.ts'
test('only explicit loopback API addresses may start a daemon', () => {
for (const endpoint of ['example.com:50051', '0.0.0.0:50051', '127.0.0.1:0', 'localhost:65536', 'unix:/tmp/node']) assert.equal(localApiAddress(endpoint), null)
assert.deepEqual(localApiAddress('127.0.0.1:50051'), { host: '127.0.0.1', port: 50051 })
assert.deepEqual(localApiAddress('[::1]:50051'), { host: '::1', port: 50051 })
})
test('real port probe never takes over a running service', async () => {
const server = createServer((socket) => socket.end())
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
const endpoint = `127.0.0.1:${server.address().port}`
try { assert.equal(await portIsUnused(endpoint), false) }
finally { await new Promise((resolve) => server.close(resolve)) }
assert.equal(await portIsUnused(endpoint), true)
assert.equal(await portIsUnused('example.com:50051'), false)
})
function fixture(overrides = {}) {
const child = new EventEmitter()
const signals = []
child.kill = (signal) => { signals.push(signal); child.emit('close', 0, signal); return true }
const launches = []
const errors = []
const daemon = new DesktopDaemon({
executable: '/test path/sisyphusd', endpoint: '127.0.0.1:50051', dataDir: '/test path/data', enabled: true,
unused: async () => true,
launch: (executable, args) => { launches.push({ executable, args }); return child },
onError: (error) => errors.push(error), ...overrides,
})
return { daemon, child, signals, launches, errors }
}
test('launches once with separate arguments and loopback-only networking', async () => {
const { daemon, launches, signals } = fixture()
await Promise.all([daemon.ensureStarted(), daemon.ensureStarted()])
await daemon.ensureStarted()
assert.deepEqual(launches, [{ executable: '/test path/sisyphusd', args: ['run', '--api-listen', '127.0.0.1:50051', '--data-dir', '/test path/data', '--listen', '127.0.0.1:7700', '--discovery', 'off'] }])
await daemon.stop()
assert.deepEqual(signals, ['SIGTERM'])
await daemon.stop()
await daemon.ensureStarted()
assert.equal(launches.length, 1)
assert.deepEqual(signals, ['SIGTERM'])
})
test('occupied default pool port falls back without taking over its service', async () => {
const server = createServer(socket => socket.end())
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve) })
const occupied = `127.0.0.1:${server.address().port}`
try {
const { daemon, launches } = fixture({ unused: endpoint => endpoint.endsWith(':50051') ? Promise.resolve(true) : portIsUnused(occupied) })
await daemon.ensureStarted()
assert.equal(launches[0].args[6], '127.0.0.1:0')
assert.equal(await portIsUnused(occupied), false)
await daemon.stop()
} finally { await new Promise(resolve => server.close(resolve)) }
})
test('API on the preferred pool port cannot conflict with the pool listener', async () => {
const { daemon, launches } = fixture({ endpoint: '127.0.0.1:7700' })
await daemon.ensureStarted()
assert.equal(launches[0].args[6], '127.0.0.1:0')
await daemon.stop()
})
test('quit during the second port probe still cannot spawn', async () => {
let release
const { daemon, launches } = fixture({ unused: endpoint => endpoint.endsWith(':50051') ? Promise.resolve(true) : new Promise(resolve => { release = resolve }) })
const start = daemon.ensureStarted()
await new Promise(resolve => setImmediate(resolve))
const stop = daemon.stop()
release(true)
await Promise.all([start, stop])
assert.equal(launches.length, 0)
})
test('external, disabled and remote daemons are never started or stopped', async () => {
for (const options of [{ unused: async () => false }, { enabled: false }, { endpoint: 'remote:50051' }]) {
const { daemon, launches, signals } = fixture(options)
await daemon.ensureStarted()
await daemon.stop()
assert.equal(launches.length, 0)
assert.deepEqual(signals, [])
}
})
test('quit during probing cannot spawn a late orphan', async () => {
let release
const { daemon, launches } = fixture({ unused: () => new Promise((resolve) => { release = resolve }) })
const start = daemon.ensureStarted()
const stop = daemon.stop()
release(true)
await Promise.all([start, stop])
assert.equal(launches.length, 0)
})
test('launch failure and unexpected exit are reported without a restart storm', async () => {
const { daemon, child, errors, launches } = fixture()
await daemon.ensureStarted()
child.emit('error', new Error('not installed'))
child.emit('close')
await daemon.ensureStarted()
assert.match(errors[0], /not installed/)
assert.equal(launches.length, 1)
await daemon.stop()
const other = fixture()
await other.daemon.ensureStarted()
other.child.emit('exit', 1, null)
await other.daemon.ensureStarted()
assert.match(other.errors[0], /stopped \(1\)/)
assert.equal(other.launches.length, 1)
await other.daemon.stop()
})
test('an actual owned child is stopped and reaped, without touching other processes', async () => {
let child
const { daemon } = fixture({ launch: () => {
child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore' })
return child
} })
await daemon.ensureStarted()
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject) })
const closed = new Promise((resolve) => child.once('close', resolve))
await daemon.stop()
await closed
assert.ok(child.exitCode !== null || child.signalCode !== null)
})
test('probe errors become a connection diagnostic, not an unhandled rejection', async () => {
const { daemon, errors, launches } = fixture({ unused: async () => { throw new Error('probe failed') } })
await daemon.ensureStarted()
assert.match(errors[0], /probe failed/)
assert.equal(launches.length, 0)
await daemon.stop()
})
test('shutdown waits for a delayed graceful close without force killing', async () => {
const { daemon, child, signals } = fixture({ shutdownGraceMs: 200 })
child.kill = signal => { signals.push(signal); setTimeout(() => child.emit('close', 0, signal), 30); return true }
await daemon.ensureStarted()
await daemon.stop()
assert.deepEqual(signals, ['SIGTERM'])
})
test('shutdown has a bounded fallback if an owned child ignores termination', async () => {
assert.equal(daemonShutdownGraceMs, 30000)
const { daemon, child, signals } = fixture({ shutdownGraceMs: 20 })
child.kill = (signal) => { signals.push(signal); return true }
await daemon.ensureStarted()
await daemon.stop()
assert.deepEqual(signals, ['SIGTERM', 'SIGKILL'])
})