Files
flynn/src/cli/companion.test.ts
T
2026-02-18 11:02:21 -08:00

153 lines
4.9 KiB
TypeScript

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<typeof vi.fn>;
registerNode: ReturnType<typeof vi.fn>;
setNodeStatus: ReturnType<typeof vi.fn>;
subscribeAgentStream: ReturnType<typeof vi.fn>;
subscribeAgentTyping: ReturnType<typeof vi.fn>;
disconnect: ReturnType<typeof vi.fn>;
}> = [];
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();
});
});