feat: add minimal companion client CLI command
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user