feat(companion): add reconnect resilience

This commit is contained in:
William Valentin
2026-02-25 11:12:21 -08:00
parent 7b170cff4d
commit ac60fa5be3
9 changed files with 297 additions and 27 deletions
+16 -6
View File
@@ -7,13 +7,14 @@ const {
mockRuntimeCtorArgs,
mockRuntimeInstances,
} = vi.hoisted(() => {
const runtimeCtorArgs: Array<{ url: string; token?: string }> = [];
const runtimeCtorArgs: Array<{ url: string; token?: string; autoReconnect?: boolean }> = [];
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>;
subscribeConnectionEvents: ReturnType<typeof vi.fn>;
disconnect: ReturnType<typeof vi.fn>;
}> = [];
@@ -43,7 +44,13 @@ vi.mock('./shared.js', () => ({
vi.mock('../companion/index.js', () => ({
CompanionRuntimeClient: class {
connect = vi.fn(async () => undefined);
private connectionHandlers: Array<(event: { status: string }) => void> = [];
connect = vi.fn(async () => {
for (const handler of this.connectionHandlers) {
handler({ status: 'connected' });
}
return undefined;
});
registerNode = vi.fn(async ({ nodeId, role, capabilities }: { nodeId: string; role: string; capabilities: string[] }) => ({
registered: true,
node: { id: nodeId, role },
@@ -53,9 +60,13 @@ vi.mock('../companion/index.js', () => ({
setNodeStatus = vi.fn(async () => ({ updated: true, node: { id: 'n', role: 'companion' } }));
subscribeAgentStream = vi.fn(() => () => undefined);
subscribeAgentTyping = vi.fn(() => () => undefined);
subscribeConnectionEvents = vi.fn((handler: (event: { status: string }) => void) => {
this.connectionHandlers.push(handler);
return () => undefined;
});
disconnect = vi.fn(() => undefined);
constructor(opts: { url: string; token?: string }) {
constructor(opts: { url: string; token?: string; autoReconnect?: boolean }) {
mockRuntimeCtorArgs.push(opts);
mockRuntimeInstances.push(this);
}
@@ -89,7 +100,7 @@ describe('companion command', () => {
await program.parseAsync(['node', 'test', 'companion', '--once']);
expect(mockGetConfigPath).toHaveBeenCalledOnce();
expect(mockRuntimeCtorArgs).toEqual([{ url: 'ws://127.0.0.1:18888', token: 'config-token' }]);
expect(mockRuntimeCtorArgs).toEqual([{ url: 'ws://127.0.0.1:18888', token: 'config-token', autoReconnect: false }]);
expect(mockRuntimeInstances[0]?.connect).toHaveBeenCalledOnce();
expect(mockRuntimeInstances[0]?.registerNode).toHaveBeenCalledOnce();
expect(mockRuntimeInstances[0]?.setNodeStatus).toHaveBeenCalledOnce();
@@ -124,7 +135,7 @@ describe('companion command', () => {
'node.push.register',
]);
expect(mockRuntimeCtorArgs).toEqual([{ url: 'ws://10.0.0.5:19000', token: 'override-token' }]);
expect(mockRuntimeCtorArgs).toEqual([{ url: 'ws://10.0.0.5:19000', token: 'override-token', autoReconnect: false }]);
expect(mockRuntimeInstances[0]?.registerNode).toHaveBeenCalledWith(expect.objectContaining({
nodeId: 'test-node',
capabilities: ['ui.canvas', 'node.push.register'],
@@ -149,4 +160,3 @@ describe('companion command', () => {
errSpy.mockRestore();
});
});