import { EventEmitter } from 'events' import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises' import { tmpdir } from 'os' import { join } from 'path' import { afterEach, describe, expect, it, vi } from 'vitest' const originalEnv = { ...process.env } const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') class FakeChild extends EventEmitter { pid: number killSignals: string[] = [] constructor(pid: number) { super() this.pid = pid } unref() { /* no-op */ } kill(signal?: string) { this.killSignals.push(signal || 'SIGTERM') return true } } let fakeChildren: FakeChild[] = [] vi.mock('../../packages/server/src/services/hermes/hermes-process', () => ({ execHermesWithBin: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }), spawnHermesWithBin: vi.fn(() => { const child = new FakeChild(20000 + fakeChildren.length) fakeChildren.push(child) return child }), })) afterEach(() => { vi.useRealTimers() vi.restoreAllMocks() vi.resetModules() process.env = { ...originalEnv } if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform) fakeChildren = [] }) describe('profile delete managed gateway lifecycle', () => { it('reproduces #1633 and proves delete prep suppresses managed respawn', async () => { vi.useFakeTimers() vi.resetModules() // 固定 platform:停止网关的手段随平台不同(POSIX 走 child.kill('SIGTERM'), // Windows 走 taskkill 杀进程树)。不固定的话这个用例在 Windows 上会因为 // taskkill 是异步的而读不到 killSignals,而且会真的拿假 pid 去杀进程。 // Windows 分支由 gateway-respawn.test.ts 单独覆盖。 Object.defineProperty(process, 'platform', { value: 'linux' }) const home = await mkdtemp(join(tmpdir(), 'wui-1633-')) process.env.HERMES_HOME = home process.env.HERMES_BIN = '/usr/bin/hermes' const profileDir = join(home, 'profiles', 'work') await mkdir(profileDir, { recursive: true }) await writeFile(join(profileDir, 'config.yaml'), 'model:\n default: test\n', 'utf-8') try { const { startGatewayRunManaged } = await import('../../packages/server/src/services/hermes/gateway-runner') const { prepareGatewayForProfileDelete } = await import('../../packages/server/src/services/hermes/gateway-autostart') startGatewayRunManaged('/usr/bin/hermes', { profileDir }) expect(fakeChildren).toHaveLength(1) const prep = prepareGatewayForProfileDelete('work') expect(fakeChildren[0].killSignals).toEqual(['SIGTERM']) fakeChildren[0].emit('exit', 0, 'SIGTERM') await prep await vi.advanceTimersByTimeAsync(6000) expect(fakeChildren).toHaveLength(1) } finally { await rm(home, { recursive: true, force: true }) } }) })