Files
flynn/src/gateway/router.test.ts
T
William Valentin f30a8bc318 feat(gateway): add WebSocket gateway with JSON-RPC protocol and auth
Phase 2 of the Flynn roadmap. Adds a WebSocket gateway server that
starts alongside the Telegram bot, providing real-time API access to
the agent, sessions, and tools.

Protocol: JSON-RPC-like (request/response/event) over WebSocket.
8 methods: agent.send, agent.cancel, sessions.list, sessions.history,
sessions.create, tools.list, tools.invoke, system.health.

Auth: Bearer token + Tailscale identity header support.
Session bridge: per-connection agent instances with shared model router.

New files: src/gateway/ (protocol, router, server, auth, session-bridge,
handlers for agent/sessions/tools/system).
57 new tests (181 total), typecheck clean.
2026-02-05 19:11:25 -08:00

59 lines
2.2 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { Router } from './router.js';
import type { GatewayRequest, OutboundMessage } from './protocol.js';
import { makeResponse, ErrorCode } from './protocol.js';
describe('Router', () => {
it('dispatches to registered handler', async () => {
const router = new Router();
const handler = vi.fn(async (req: GatewayRequest) => makeResponse(req.id, { ok: true }));
router.register('test.method', handler);
const request: GatewayRequest = { id: 1, method: 'test.method', params: {} };
const send = vi.fn();
const result = await router.dispatch(request, send);
expect(handler).toHaveBeenCalledWith(request, send);
expect(result).toEqual({ id: 1, result: { ok: true } });
});
it('returns MethodNotFound for unregistered method', async () => {
const router = new Router();
const request: GatewayRequest = { id: 1, method: 'unknown.method' };
const send = vi.fn();
const result = await router.dispatch(request, send);
expect(result).toEqual({
id: 1,
error: { code: ErrorCode.MethodNotFound, message: 'Unknown method: unknown.method' },
});
});
it('lists registered methods', () => {
const router = new Router();
router.register('a.method', async () => {});
router.register('b.method', async () => {});
expect(router.listMethods()).toEqual(['a.method', 'b.method']);
});
it('handler can send streaming events via send function', async () => {
const router = new Router();
router.register('stream.test', async (req, send) => {
send({ id: req.id, event: 'content', data: { text: 'chunk1' } });
send({ id: req.id, event: 'content', data: { text: 'chunk2' } });
send({ id: req.id, event: 'done', data: { content: 'chunk1chunk2' } });
});
const request: GatewayRequest = { id: 1, method: 'stream.test' };
const sent: OutboundMessage[] = [];
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
await router.dispatch(request, send);
expect(sent).toHaveLength(3);
expect(sent[0]).toEqual({ id: 1, event: 'content', data: { text: 'chunk1' } });
expect(sent[2]).toEqual({ id: 1, event: 'done', data: { content: 'chunk1chunk2' } });
});
});