import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'; import { spawn } from 'child_process'; import { EventEmitter } from 'events'; import type { ChildProcess } from 'child_process'; vi.mock('child_process', () => ({ spawn: vi.fn(), })); class MockChildProcess extends EventEmitter { exitCode: number | null = null; killed = false; unref = vi.fn(); kill = vi.fn((signal?: NodeJS.Signals) => { this.killed = true; this.emit('exit', signal === 'SIGKILL' ? 137 : 0, signal ?? null); return true; }); } const mockSpawn = vi.mocked(spawn); describe('gateway discovery', () => { let startGatewayDiscovery: typeof import('./discovery.js').startGatewayDiscovery; beforeAll(async () => { const mod = await import('./discovery.js'); startGatewayDiscovery = mod.startGatewayDiscovery; }); beforeEach(() => { vi.clearAllMocks(); }); it('starts avahi publisher with txt records', async () => { const child = new MockChildProcess(); mockSpawn.mockReturnValueOnce(child as unknown as ChildProcess); setTimeout(() => child.emit('spawn'), 0); const handle = await startGatewayDiscovery({ serviceName: 'flynn-gateway', serviceType: '_flynn._tcp', port: 18800, txtRecord: { instance: 'pid-123', version: '0.1.0' }, }); expect(mockSpawn).toHaveBeenCalledWith('avahi-publish-service', [ 'flynn-gateway', '_flynn._tcp', '18800', 'instance=pid-123', 'version=0.1.0', ], { stdio: 'ignore' }); expect(child.unref).toHaveBeenCalledOnce(); await handle.stop(); expect(child.kill).toHaveBeenCalledWith('SIGTERM'); }); it('falls back to dns-sd when avahi is unavailable', async () => { const avahiChild = new MockChildProcess(); const dnsChild = new MockChildProcess(); mockSpawn.mockReturnValueOnce(avahiChild as unknown as ChildProcess); mockSpawn.mockReturnValueOnce(dnsChild as unknown as ChildProcess); setTimeout(() => avahiChild.emit('error', new Error('ENOENT')), 0); setTimeout(() => dnsChild.emit('spawn'), 0); const handle = await startGatewayDiscovery({ serviceName: 'flynn-gateway', serviceType: '_flynn._tcp', port: 18800, }); expect(mockSpawn).toHaveBeenNthCalledWith(2, 'dns-sd', [ '-R', 'flynn-gateway', '_flynn._tcp', 'local', '18800', ], { stdio: 'ignore' }); await handle.stop(); expect(dnsChild.kill).toHaveBeenCalledWith('SIGTERM'); }); it('throws when no supported advertiser command is available', async () => { const avahiChild = new MockChildProcess(); const dnsChild = new MockChildProcess(); mockSpawn.mockReturnValueOnce(avahiChild as unknown as ChildProcess); mockSpawn.mockReturnValueOnce(dnsChild as unknown as ChildProcess); setTimeout(() => avahiChild.emit('error', new Error('ENOENT')), 0); setTimeout(() => dnsChild.emit('error', new Error('ENOENT')), 0); await expect(startGatewayDiscovery({ serviceName: 'flynn-gateway', serviceType: '_flynn._tcp', port: 18800, })).rejects.toThrow(/Failed to start mDNS advertiser/); }); });