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, }); }); }); });