// @vitest-environment node import { expect, it, vi } from 'vitest' import { ServerDashboardController } from '../../src/server/dashboardController' const origin = new URL('http://127.0.0.1:9119') const target = 'gui/501/com.samien.hermes.dashboard.local' function fixture(options: { upstream?: URL; platform?: NodeJS.Platform; label?: string; command?: string; owned?: { canRestart: boolean; restart(): Promise; suspendChecks?(): () => void } } = {}) { let pid = 1043, listener = 1043, replacement = 2043, available = true, health = true const service = options.label ? `gui/501/${options.label}` : target const run = vi.fn(async (command: string, args: string[]) => { if (command === '/bin/launchctl' && args[0] === 'print') { if (!available || args[1] !== service) throw new Error('job not found') return `${service} = {\n\targuments = {\n\t\t${options.command ?? '/server/.hermes/venv/bin/python\n\t\t/server/.hermes/hermes-agent/hermes\n\t\tdashboard'}\n\t\t--host\n\t\t127.0.0.1\n\t}\n\tpid = ${pid}\n}` } if (command === '/usr/sbin/lsof') return `p${listener}\n` if (command === '/bin/launchctl' && args[0] === 'list') return available ? `${pid}\t0\t${service.split('/').at(-1)}\n` : '' if (command === '/bin/launchctl' && args[0] === 'kickstart') { pid = replacement; listener = replacement; return '' } throw new Error('unexpected command') }) const healthy = vi.fn(async () => health) const controller = new ServerDashboardController(options.upstream ?? origin, options.owned, { platform: options.platform ?? 'darwin', uid: 501, run, healthy, readyTimeoutMs: 15, pollIntervalMs: 1, }) const mutations = () => run.mock.calls.filter(([, args]) => args[0] === 'kickstart') return { controller, run, healthy, mutations, set: (state: { available?: boolean; listener?: number; replacement?: number; healthy?: boolean }) => { if (state.available !== undefined) available = state.available if (state.listener !== undefined) listener = state.listener if (state.replacement !== undefined) replacement = state.replacement if (state.healthy !== undefined) health = state.healthy } } } it('discovers and restarts the server LaunchAgent, then verifies a new listener and HTTP health', async () => { const f = fixture() expect(f.controller.canRestart).toBe(false) await f.controller.refresh() expect(f.controller.canRestart).toBe(true) expect(f.mutations()).toEqual([]) await f.controller.restart() expect(f.mutations()).toEqual([['/bin/launchctl', ['kickstart', '-k', target]]]) expect(f.healthy).toHaveBeenCalledWith('http://127.0.0.1:9119') expect(f.controller.canRestart).toBe(true) }) it('supports the standard Hermes LaunchAgent only when it owns the configured listener', async () => { const f = fixture({ label: 'ai.hermes.dashboard', command: '/server/.local/bin/hermes\n\t\tdashboard' }) await f.controller.refresh(); await f.controller.restart() expect(f.mutations()[0]?.[1]).toEqual(['kickstart', '-k', 'gui/501/ai.hermes.dashboard']) }) it.each([ '/server/.hermes/venv/bin/python3\n\t\t-m\n\t\thermes_cli.main\n\t\tdashboard', '/server/.hermes/venv/bin/python\n\t\t/server/.hermes/hermes-agent/hermes_cli/main.py\n\t\tserve', '/server/.local/bin/hermes\n\t\t--profile\n\t\tyaoyao\n\t\tdashboard', ])('recognizes supported Hermes entry points under a custom LaunchAgent label: %s', async command => { const f = fixture({ label: 'local.custom.hermes', command }) await f.controller.refresh(); expect(f.controller.canRestart).toBe(true) await f.controller.restart() expect(f.mutations()[0]?.[1]).toEqual(['kickstart', '-k', 'gui/501/local.custom.hermes']) }) it.each([ new URL('https://hermes.example'), new URL('http://192.168.1.5:9119'), new URL('http://127.0.0.1:9120'), ])('never runs local commands for an upstream on another server or port: %s', async upstream => { const owned = { canRestart: true, restart: vi.fn(async () => {}) } const f = fixture({ upstream, owned }) await f.controller.refresh() expect(f.controller.canRestart).toBe(false) await expect(f.controller.restart()).rejects.toThrow('所在节点') expect(f.run).not.toHaveBeenCalled(); expect(owned.restart).not.toHaveBeenCalled() }) it('does not run launchctl on an unsupported server OS', async () => { const f = fixture({ platform: 'linux' }) await f.controller.refresh() await expect(f.controller.restart()).rejects.toThrow('尚未支持') expect(f.run).not.toHaveBeenCalled() }) it.each(['missing', 'other-listener', 'other-command', 'multiple-listeners'])('refuses an unverified service: %s', async mode => { const f = fixture(mode === 'other-command' ? { command: '/usr/bin/node\n\t\tdashboard' } : {}) if (mode === 'missing') f.set({ available: false }) if (mode === 'other-listener') f.set({ listener: 999 }) if (mode === 'multiple-listeners') { const original = f.run.getMockImplementation()! f.run.mockImplementation(async (command, args) => command === '/usr/sbin/lsof' ? 'p1043\np999\n' : original(command, args)) } await f.controller.refresh() expect(f.controller.canRestart).toBe(false) await expect(f.controller.restart()).rejects.toThrow('未识别到') expect(f.mutations()).toEqual([]) }) it('revalidates ownership immediately before restarting, regardless of an earlier available status', async () => { const f = fixture() await f.controller.refresh(); expect(f.controller.canRestart).toBe(true) f.set({ listener: 999 }) await expect(f.controller.restart()).rejects.toThrow('未识别到') expect(f.mutations()).toEqual([]) expect(f.controller.canRestart).toBe(false) }) it.each(['same-pid', 'unhealthy'])('does not report success until replacement is verified: %s', async mode => { const f = fixture() f.set(mode === 'same-pid' ? { replacement: 1043 } : { healthy: false }) await expect(f.controller.restart()).rejects.toThrow('未恢复健康') expect(f.controller.canRestart).toBe(false) f.set({ replacement: 3043, healthy: true }) await f.controller.restart() expect(f.controller.canRestart).toBe(true) }) it('preserves owned-child restart support and never touches a system service in that case', async () => { const owned = { canRestart: true, restart: vi.fn(async () => {}) } const f = fixture({ owned }) await f.controller.refresh(); await f.controller.restart() expect(owned.restart).toHaveBeenCalledTimes(1) expect(f.run).not.toHaveBeenCalled() expect(f.healthy).toHaveBeenCalledWith(origin.origin) }) it.each([true, false])('suspends automatic starts during a system restart and releases them on success=%s', async success => { const resume = vi.fn() const owned = { canRestart: false, restart: vi.fn(async () => {}), suspendChecks: vi.fn(() => resume) } const f = fixture({ owned }) f.healthy.mockImplementation(async () => { expect(owned.suspendChecks).toHaveBeenCalledTimes(1) expect(resume).not.toHaveBeenCalled() return success }) if (success) await f.controller.restart() else await expect(f.controller.restart()).rejects.toThrow('未恢复健康') expect(owned.restart).not.toHaveBeenCalled() expect(resume).toHaveBeenCalledTimes(1) }) it('serializes restarts and stops verification when the server shuts down', async () => { let release!: () => void const owned = { canRestart: true, restart: vi.fn(() => new Promise(done => { release = done })) } const f = fixture({ owned }) const pending = f.controller.restart() await expect(f.controller.restart()).rejects.toThrow('正在重启') f.controller.stop(); release() await expect(pending).rejects.toThrow('未恢复健康') expect(f.controller.canRestart).toBe(false) expect(f.run).not.toHaveBeenCalled() }) function standaloneFixture() { let pid = 1043, running = true, uid = 501, parent = 99 let command = '/server/venv/bin/python3 -m hermes_cli.main dashboard --host 0.0.0.0 --port 9119 --no-open' let jobs = '', stopped = true, cwd = '/server/hermes-agent' const run = vi.fn(async (program: string, args: string[]) => { if (program === '/usr/sbin/lsof') return args.includes('cwd') ? `p${pid}\nfcwd\nn${cwd}\n` : running ? `p${pid}\n` : '' if (program === '/bin/launchctl' && args[0] === 'list') return jobs if (program === '/bin/launchctl') throw new Error('no managed job') if (program === '/bin/ps') { if (args[1] === '99') return '501 98 /bin/zsh -l\n' if (running && args[1] === String(pid)) return `${uid} ${parent} ${command}\n` throw new Error('process exited') } if (program === '/bin/kill' && args[1] === String(pid)) { if (stopped) running = false; return '' } throw new Error('unexpected command') }) const launch = vi.fn(async (program: string, args: string[], directory: string) => { pid = 2043; parent = 500; jobs = '500\t0\tcom.samien.hermes-yaoyao\n'; running = true; command = [program, ...args].join(' '); cwd = directory; return pid }) const healthy = vi.fn(async () => true) const controller = new ServerDashboardController(origin, undefined, { platform: 'darwin', uid: 501, run, launch, healthy, readyTimeoutMs: 15, pollIntervalMs: 1 }) return { controller, run, launch, healthy, set: (value: { uid?: number; command?: string; jobs?: string; stopped?: boolean; parent?: number; cwd?: string }) => { if (value.uid !== undefined) uid = value.uid if (value.command !== undefined) command = value.command if (value.jobs !== undefined) jobs = value.jobs if (value.stopped !== undefined) stopped = value.stopped if (value.parent !== undefined) parent = value.parent if (value.cwd !== undefined) cwd = value.cwd } } } it('restarts a verified terminal-launched Hermes listener while preserving its entry point and binding', async () => { const f = standaloneFixture() await f.controller.refresh(); expect(f.controller.canRestart).toBe(true) expect(f.launch).not.toHaveBeenCalled() await f.controller.restart() expect(f.run).toHaveBeenCalledWith('/bin/kill', ['-TERM', '1043']) expect(f.launch).toHaveBeenCalledWith('/server/venv/bin/python3', ['-m', 'hermes_cli.main', 'dashboard', '--host', '0.0.0.0', '--port', '9119', '--no-open'], '/server/hermes-agent') expect(f.healthy).toHaveBeenCalledWith(origin.origin) await f.controller.refresh(); expect(f.controller.canRestart).toBe(true) }) it.each([ { uid: 502 }, { command: '/usr/bin/node /server/dashboard.js' }, { command: '/bin/sh -c /server/hermes dashboard' }, { command: '/server/hermes dashboard --ssh-session-token-file /private/token' }, { command: '/server/hermes dashboard --port 9120' }, { jobs: '1043\t0\tlocal.unknown.service\n' }, { jobs: '99\t0\tlocal.wrapper.service\n' }, { parent: 444 }, { cwd: '' }, ])('refuses to stop an unverified standalone listener: %j', async state => { const f = standaloneFixture(); f.set(state) await f.controller.refresh(); expect(f.controller.canRestart).toBe(false) await expect(f.controller.restart()).rejects.toThrow('未识别到') expect(f.launch).not.toHaveBeenCalled(); expect(f.run.mock.calls.some(([program]) => program === '/bin/kill')).toBe(false) }) it('rechecks a standalone process owner immediately before the restart', async () => { const f = standaloneFixture(); await f.controller.refresh(); expect(f.controller.canRestart).toBe(true) f.set({ uid: 502 }); await expect(f.controller.restart()).rejects.toThrow('未识别到') expect(f.launch).not.toHaveBeenCalled(); expect(f.run.mock.calls.some(([program]) => program === '/bin/kill')).toBe(false) }) it('does not launch a competitor if the original standalone process ignores termination', async () => { const f = standaloneFixture(); f.set({ stopped: false }) await expect(f.controller.restart()).rejects.toThrow('未能停止') expect(f.launch).not.toHaveBeenCalled(); expect(f.controller.canRestart).toBe(false) })