feat(gateway): add node capability negotiation foundation

This commit is contained in:
William Valentin
2026-02-16 12:14:25 -08:00
parent de0c1f41b3
commit d9f7807ab2
17 changed files with 675 additions and 7 deletions
+83
View File
@@ -577,3 +577,86 @@ describe('GatewayServer WebSocket ingress rate limiting', () => {
}
});
});
describe('GatewayServer node registration and capability negotiation', () => {
const NODE_PORT = 18894;
let nodeServer: GatewayServer;
beforeAll(async () => {
if (!LISTEN_ALLOWED) {
return;
}
nodeServer = new GatewayServer({
port: NODE_PORT,
sessionManager: mockSessionManager as unknown as GatewayServerConfig['sessionManager'],
modelClient: mockModelClient,
systemPrompt: 'Test prompt',
toolRegistry: mockToolRegistry as unknown as GatewayServerConfig['toolRegistry'],
toolExecutor: mockToolExecutor as unknown as GatewayServerConfig['toolExecutor'],
uiDir: resolve(import.meta.dirname, 'ui'),
nodes: {
enabled: true,
allowedRoles: ['companion'],
featureGates: { 'ui.canvas': true },
},
});
await nodeServer.start();
});
afterAll(async () => {
if (!LISTEN_ALLOWED) {
return;
}
await nodeServer.stop();
});
it('enforces role allow/deny and node registration lifecycle', async () => {
if (!LISTEN_ALLOWED) {
return;
}
const ws = await new Promise<WebSocket>((resolve, reject) => {
const c = new WebSocket(`ws://127.0.0.1:${NODE_PORT}`);
c.on('open', () => resolve(c));
c.on('error', reject);
});
try {
const beforeRegister = await sendAndReceive(ws, { id: 1, method: 'node.capabilities.get', params: {} });
expect((beforeRegister as GatewayError).error.code).toBe(ErrorCode.AuthFailed);
const badRole = await sendAndReceive(ws, {
id: 2,
method: 'node.register',
params: {
nodeId: 'node-bad',
role: 'observer',
protocolVersion: 1,
capabilities: ['ui.canvas'],
},
});
expect((badRole as GatewayError).error.code).toBe(ErrorCode.AuthFailed);
const registered = await sendAndReceive(ws, {
id: 3,
method: 'node.register',
params: {
nodeId: 'node-good',
role: 'companion',
protocolVersion: 1,
capabilities: ['ui.canvas'],
},
});
expect((registered as GatewayResponse).id).toBe(3);
expect(((registered as GatewayResponse).result as { registered: boolean }).registered).toBe(true);
const capabilities = await sendAndReceive(ws, { id: 4, method: 'node.capabilities.get', params: {} });
expect((capabilities as GatewayResponse).id).toBe(4);
expect(((capabilities as GatewayResponse).result as { node: { role: string } }).node.role).toBe('companion');
} finally {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
}
});
});