import { Command } from 'commander'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const { mockLoadConfigSafe, mockGetConfigPath, mockRuntimeCtorArgs, mockRuntimeInstances, } = vi.hoisted(() => { const runtimeCtorArgs: Array<{ url: string; token?: string }> = []; const runtimeInstances: Array<{ connect: ReturnType; registerNode: ReturnType; setNodeStatus: ReturnType; subscribeAgentStream: ReturnType; subscribeAgentTyping: ReturnType; disconnect: ReturnType; }> = []; const loadConfigSafe = vi.fn(() => ({ config: { server: { port: 18888, token: 'config-token', }, }, })); const getConfigPath = vi.fn(() => '/tmp/flynn-config.yaml'); return { mockLoadConfigSafe: loadConfigSafe, mockGetConfigPath: getConfigPath, mockRuntimeCtorArgs: runtimeCtorArgs, mockRuntimeInstances: runtimeInstances, }; }); vi.mock('./shared.js', () => ({ loadConfigSafe: mockLoadConfigSafe, getConfigPath: mockGetConfigPath, })); vi.mock('../companion/index.js', () => ({ CompanionRuntimeClient: class { connect = vi.fn(async () => undefined); registerNode = vi.fn(async ({ nodeId, role, capabilities }: { nodeId: string; role: string; capabilities: string[] }) => ({ registered: true, node: { id: nodeId, role }, protocol: { serverVersion: 1, clientVersion: 1, negotiatedVersion: 1 }, capabilities: { declared: capabilities, enabled: capabilities }, })); setNodeStatus = vi.fn(async () => ({ updated: true, node: { id: 'n', role: 'companion' } })); subscribeAgentStream = vi.fn(() => () => undefined); subscribeAgentTyping = vi.fn(() => () => undefined); disconnect = vi.fn(() => undefined); constructor(opts: { url: string; token?: string }) { mockRuntimeCtorArgs.push(opts); mockRuntimeInstances.push(this); } }, })); describe('companion command', () => { beforeEach(() => { vi.clearAllMocks(); mockRuntimeCtorArgs.length = 0; mockRuntimeInstances.length = 0; mockLoadConfigSafe.mockReturnValue({ config: { server: { port: 18888, token: 'config-token', }, }, }); mockGetConfigPath.mockReturnValue('/tmp/flynn-config.yaml'); process.exitCode = undefined; }); it('uses config-derived gateway url/token by default', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); const errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); const program = new Command(); const { registerCompanionCommand } = await import('./companion.js'); registerCompanionCommand(program); await program.parseAsync(['node', 'test', 'companion', '--once']); expect(mockGetConfigPath).toHaveBeenCalledOnce(); expect(mockRuntimeCtorArgs).toEqual([{ url: 'ws://127.0.0.1:18888', token: 'config-token' }]); expect(mockRuntimeInstances[0]?.connect).toHaveBeenCalledOnce(); expect(mockRuntimeInstances[0]?.registerNode).toHaveBeenCalledOnce(); expect(mockRuntimeInstances[0]?.setNodeStatus).toHaveBeenCalledOnce(); expect(mockRuntimeInstances[0]?.disconnect).toHaveBeenCalled(); expect(errSpy).not.toHaveBeenCalled(); expect(process.exitCode).toBeUndefined(); logSpy.mockRestore(); errSpy.mockRestore(); }); it('prefers explicit url/token and capability overrides', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); const errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); const program = new Command(); const { registerCompanionCommand } = await import('./companion.js'); registerCompanionCommand(program); await program.parseAsync([ 'node', 'test', 'companion', '--once', '--url', 'ws://10.0.0.5:19000', '--token', 'override-token', '--node-id', 'test-node', '--capability', 'ui.canvas', 'node.push.register', ]); expect(mockRuntimeCtorArgs).toEqual([{ url: 'ws://10.0.0.5:19000', token: 'override-token' }]); expect(mockRuntimeInstances[0]?.registerNode).toHaveBeenCalledWith(expect.objectContaining({ nodeId: 'test-node', capabilities: ['ui.canvas', 'node.push.register'], })); expect(errSpy).not.toHaveBeenCalled(); logSpy.mockRestore(); errSpy.mockRestore(); }); it('sets process exit code when options are invalid', async () => { const errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); const program = new Command(); const { registerCompanionCommand } = await import('./companion.js'); registerCompanionCommand(program); await program.parseAsync(['node', 'test', 'companion', '--once', '--heartbeat', '0']); expect(errSpy).toHaveBeenCalled(); expect(process.exitCode).toBe(1); errSpy.mockRestore(); }); });