Files
flynn/src/gateway/protocol.test.ts
T
2026-02-16 12:47:34 -08:00

298 lines
8.4 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
isValidRequest,
parseMessage,
parseNodeRegisterParams,
parseNodeLocationSetParams,
parseNodeLocationGetParams,
parseNodeStatusSetParams,
parseNodePushTokenSetParams,
makeResponse,
makeError,
makeEvent,
ErrorCode,
} from './protocol.js';
describe('protocol', () => {
describe('isValidRequest', () => {
it('accepts valid request with params', () => {
expect(isValidRequest({ id: 1, method: 'agent.send', params: { message: 'hello' } })).toBe(true);
});
it('accepts valid request without params', () => {
expect(isValidRequest({ id: 1, method: 'system.health' })).toBe(true);
});
it('rejects missing id', () => {
expect(isValidRequest({ method: 'test' })).toBe(false);
});
it('rejects missing method', () => {
expect(isValidRequest({ id: 1 })).toBe(false);
});
it('rejects non-object', () => {
expect(isValidRequest('not an object')).toBe(false);
expect(isValidRequest(null)).toBe(false);
expect(isValidRequest(42)).toBe(false);
});
it('rejects non-numeric id', () => {
expect(isValidRequest({ id: 'abc', method: 'test' })).toBe(false);
});
it('rejects non-string method', () => {
expect(isValidRequest({ id: 1, method: 42 })).toBe(false);
});
it('rejects non-object params', () => {
expect(isValidRequest({ id: 1, method: 'test', params: 'bad' })).toBe(false);
});
});
describe('parseMessage', () => {
it('parses valid JSON into GatewayRequest', () => {
const msg = parseMessage('{"id":1,"method":"agent.send","params":{"message":"hi"}}');
expect(msg).toEqual({ id: 1, method: 'agent.send', params: { message: 'hi' } });
});
it('returns null for invalid JSON', () => {
expect(parseMessage('not json')).toBeNull();
});
it('returns null for valid JSON that is not a valid request', () => {
expect(parseMessage('{"method":"test"}')).toBeNull();
});
});
describe('parseNodeRegisterParams', () => {
it('parses valid node registration params', () => {
const parsed = parseNodeRegisterParams({
connectionId: 'conn-1',
nodeId: 'node-a',
role: 'companion',
protocolVersion: 1,
capabilities: ['ui.canvas', 'notifications'],
});
expect(parsed).toEqual({
connectionId: 'conn-1',
nodeId: 'node-a',
role: 'companion',
protocolVersion: 1,
capabilities: ['ui.canvas', 'notifications'],
});
});
it('rejects invalid node registration params', () => {
expect(parseNodeRegisterParams({
connectionId: 'conn-1',
nodeId: '',
role: 'companion',
protocolVersion: 1,
capabilities: [],
})).toBeNull();
expect(parseNodeRegisterParams({
connectionId: 'conn-1',
nodeId: 'node',
role: 'companion',
protocolVersion: 0,
capabilities: [],
})).toBeNull();
expect(parseNodeRegisterParams({
connectionId: 'conn-1',
nodeId: 'node',
role: 'companion',
protocolVersion: 1,
capabilities: [1],
})).toBeNull();
});
});
describe('parseNodeLocationSetParams', () => {
it('parses valid node location set params', () => {
const parsed = parseNodeLocationSetParams({
connectionId: 'conn-1',
latitude: 40.7128,
longitude: -74.0060,
accuracyMeters: 12.5,
source: 'gps',
});
expect(parsed).toEqual({
connectionId: 'conn-1',
latitude: 40.7128,
longitude: -74.006,
accuracyMeters: 12.5,
altitudeMeters: undefined,
headingDegrees: undefined,
speedMps: undefined,
source: 'gps',
capturedAt: undefined,
});
});
it('rejects invalid node location set params', () => {
expect(parseNodeLocationSetParams({
connectionId: 'conn-1',
latitude: 200,
longitude: -74,
})).toBeNull();
expect(parseNodeLocationSetParams({
connectionId: 'conn-1',
latitude: 10,
longitude: -190,
})).toBeNull();
expect(parseNodeLocationSetParams({
connectionId: 'conn-1',
latitude: 10,
longitude: 20,
source: 'beacon',
})).toBeNull();
expect(parseNodeLocationSetParams({
connectionId: 'conn-1',
latitude: 10,
longitude: 20,
headingDegrees: 361,
})).toBeNull();
});
});
describe('parseNodeLocationGetParams', () => {
it('parses valid node location get params', () => {
const parsed = parseNodeLocationGetParams({
connectionId: 'conn-1',
});
expect(parsed).toEqual({
connectionId: 'conn-1',
});
});
it('rejects invalid node location get params', () => {
expect(parseNodeLocationGetParams({})).toBeNull();
expect(parseNodeLocationGetParams({ connectionId: '' })).toBeNull();
expect(parseNodeLocationGetParams(null)).toBeNull();
});
});
describe('parseNodeStatusSetParams', () => {
it('parses valid node status set params', () => {
const parsed = parseNodeStatusSetParams({
connectionId: 'conn-1',
platform: 'macos',
appVersion: '0.1.0',
deviceName: 'Willbook',
statusText: 'Idle',
batteryPct: 73,
powerSource: 'battery',
});
expect(parsed).toEqual({
connectionId: 'conn-1',
platform: 'macos',
appVersion: '0.1.0',
deviceName: 'Willbook',
statusText: 'Idle',
batteryPct: 73,
powerSource: 'battery',
});
});
it('rejects invalid node status set params', () => {
expect(parseNodeStatusSetParams({
connectionId: 'conn-1',
platform: 'beos',
})).toBeNull();
expect(parseNodeStatusSetParams({
connectionId: 'conn-1',
platform: 'macos',
batteryPct: 120,
})).toBeNull();
expect(parseNodeStatusSetParams({
connectionId: '',
platform: 'macos',
})).toBeNull();
});
});
describe('parseNodePushTokenSetParams', () => {
it('parses valid node push token params', () => {
const parsed = parseNodePushTokenSetParams({
connectionId: 'conn-1',
provider: 'apns',
token: 'abcd1234abcd1234abcd1234abcd1234',
topic: 'com.example.flynn',
environment: 'production',
});
expect(parsed).toEqual({
connectionId: 'conn-1',
provider: 'apns',
token: 'abcd1234abcd1234abcd1234abcd1234',
topic: 'com.example.flynn',
environment: 'production',
});
});
it('rejects invalid node push token params', () => {
expect(parseNodePushTokenSetParams({
connectionId: 'conn-1',
provider: 'fcm',
token: 'abcd1234abcd1234abcd1234abcd1234',
})).toBeNull();
expect(parseNodePushTokenSetParams({
connectionId: 'conn-1',
provider: 'apns',
token: 'short',
})).toBeNull();
expect(parseNodePushTokenSetParams({
connectionId: '',
provider: 'apns',
token: 'abcd1234abcd1234abcd1234abcd1234',
})).toBeNull();
});
});
describe('makeResponse', () => {
it('creates a response message', () => {
expect(makeResponse(1, { status: 'ok' })).toEqual({
id: 1,
result: { status: 'ok' },
});
});
});
describe('makeError', () => {
it('creates an error message', () => {
expect(makeError(1, ErrorCode.MethodNotFound, 'Not found')).toEqual({
id: 1,
error: { code: -3, message: 'Not found' },
});
});
});
describe('makeEvent', () => {
it('creates an event message', () => {
expect(makeEvent(1, 'content', { text: 'hello' })).toEqual({
id: 1,
event: 'content',
data: { text: 'hello' },
});
});
it('creates an attachment event message', () => {
const data = { mimeType: 'image/png', data: 'iVBOR...', filename: 'screenshot.png' };
expect(makeEvent(1, 'attachment', data)).toEqual({
id: 1,
event: 'attachment',
data,
});
});
it('creates an attachment event with url', () => {
const data = { mimeType: 'application/pdf', url: 'https://example.com/doc.pdf', filename: 'doc.pdf' };
expect(makeEvent(2, 'attachment', data)).toEqual({
id: 2,
event: 'attachment',
data,
});
});
});
});