@roj-ai/transport 0.1.36 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,235 +0,0 @@
1
- /**
2
- * Protocol Definition Tests
3
- */
4
-
5
- import { describe, expect, it } from 'bun:test'
6
- import { z } from 'zod'
7
- import { defineProtocol, method, notification } from './protocol.js'
8
-
9
- describe('Protocol Definition', () => {
10
- describe('method()', () => {
11
- it('should create a method definition', () => {
12
- const def = method({
13
- input: z.object({ id: z.string() }),
14
- output: z.object({ success: z.boolean() }),
15
- error: z.object({ code: z.string() }),
16
- })
17
-
18
- expect(def._type).toBe('method')
19
- expect(def.input).toBeDefined()
20
- expect(def.output).toBeDefined()
21
- expect(def.error).toBeDefined()
22
- })
23
- })
24
-
25
- describe('notification()', () => {
26
- it('should create a notification definition', () => {
27
- const def = notification({
28
- input: z.object({ message: z.string() }),
29
- })
30
-
31
- expect(def._type).toBe('notification')
32
- expect(def.input).toBeDefined()
33
- })
34
- })
35
-
36
- describe('defineProtocol()', () => {
37
- const testProtocol = defineProtocol({
38
- subscribe: method({
39
- input: z.object({ sessionId: z.string() }),
40
- output: z.void(),
41
- error: z.object({ code: z.enum(['SESSION_NOT_FOUND']), message: z.string() }),
42
- }),
43
- unsubscribe: method({
44
- input: z.object({ sessionId: z.string() }),
45
- output: z.void(),
46
- error: z.never(),
47
- }),
48
- agentMessage: notification({
49
- input: z.object({
50
- sessionId: z.string(),
51
- content: z.string(),
52
- format: z.enum(['text', 'markdown']),
53
- }),
54
- }),
55
- })
56
-
57
- it('should expose the definition', () => {
58
- expect(testProtocol._def).toBeDefined()
59
- expect(testProtocol._def.subscribe).toBeDefined()
60
- expect(testProtocol._def.unsubscribe).toBeDefined()
61
- expect(testProtocol._def.agentMessage).toBeDefined()
62
- })
63
-
64
- describe('getEndpoint()', () => {
65
- it('should return endpoint definition by name', () => {
66
- const endpoint = testProtocol.getEndpoint('subscribe')
67
- expect(endpoint._type).toBe('method')
68
- })
69
- })
70
-
71
- describe('isMethod()', () => {
72
- it('should return true for methods', () => {
73
- expect(testProtocol.isMethod('subscribe')).toBe(true)
74
- expect(testProtocol.isMethod('unsubscribe')).toBe(true)
75
- })
76
-
77
- it('should return false for notifications', () => {
78
- expect(testProtocol.isMethod('agentMessage')).toBe(false)
79
- })
80
- })
81
-
82
- describe('isNotification()', () => {
83
- it('should return true for notifications', () => {
84
- expect(testProtocol.isNotification('agentMessage')).toBe(true)
85
- })
86
-
87
- it('should return false for methods', () => {
88
- expect(testProtocol.isNotification('subscribe')).toBe(false)
89
- })
90
- })
91
-
92
- describe('getMethodNames()', () => {
93
- it('should return all method names', () => {
94
- const names = testProtocol.getMethodNames()
95
- expect(names).toContain('subscribe')
96
- expect(names).toContain('unsubscribe')
97
- expect(names).not.toContain('agentMessage')
98
- })
99
- })
100
-
101
- describe('getNotificationNames()', () => {
102
- it('should return all notification names', () => {
103
- const names = testProtocol.getNotificationNames()
104
- expect(names).toContain('agentMessage')
105
- expect(names).not.toContain('subscribe')
106
- })
107
- })
108
-
109
- describe('validateInput()', () => {
110
- it('should validate correct input', () => {
111
- const result = testProtocol.validateInput('subscribe', { sessionId: 'test-123' })
112
- expect(result.success).toBe(true)
113
- if (result.success) {
114
- expect(result.data.sessionId).toBe('test-123')
115
- }
116
- })
117
-
118
- it('should reject invalid input', () => {
119
- const result = testProtocol.validateInput('subscribe', { sessionId: 123 })
120
- expect(result.success).toBe(false)
121
- if (!result.success) {
122
- expect(result.error.message).toBeDefined()
123
- }
124
- })
125
-
126
- it('should reject missing required fields', () => {
127
- const result = testProtocol.validateInput('subscribe', {})
128
- expect(result.success).toBe(false)
129
- })
130
-
131
- it('should validate notification input', () => {
132
- const result = testProtocol.validateInput('agentMessage', {
133
- sessionId: 'test',
134
- content: 'Hello',
135
- format: 'text',
136
- })
137
- expect(result.success).toBe(true)
138
- })
139
-
140
- it('should reject invalid notification input', () => {
141
- const result = testProtocol.validateInput('agentMessage', {
142
- sessionId: 'test',
143
- content: 'Hello',
144
- format: 'invalid',
145
- })
146
- expect(result.success).toBe(false)
147
- })
148
- })
149
-
150
- describe('validateOutput()', () => {
151
- it('should validate method output', () => {
152
- const result = testProtocol.validateOutput('subscribe', undefined)
153
- expect(result.success).toBe(true)
154
- })
155
-
156
- it('should throw for notification output validation', () => {
157
- expect(() => testProtocol.validateOutput('agentMessage', {})).toThrow(
158
- 'Endpoint agentMessage is not a method',
159
- )
160
- })
161
- })
162
- })
163
-
164
- describe('complex schemas', () => {
165
- it('should handle nested objects', () => {
166
- const protocol = defineProtocol({
167
- createUser: method({
168
- input: z.object({
169
- name: z.string(),
170
- address: z.object({
171
- street: z.string(),
172
- city: z.string(),
173
- }),
174
- }),
175
- output: z.object({ id: z.string() }),
176
- error: z.object({ code: z.string() }),
177
- }),
178
- })
179
-
180
- const valid = protocol.validateInput('createUser', {
181
- name: 'John',
182
- address: { street: '123 Main St', city: 'NYC' },
183
- })
184
- expect(valid.success).toBe(true)
185
-
186
- const invalid = protocol.validateInput('createUser', {
187
- name: 'John',
188
- address: { street: '123 Main St' }, // Missing city
189
- })
190
- expect(invalid.success).toBe(false)
191
- })
192
-
193
- it('should handle arrays', () => {
194
- const protocol = defineProtocol({
195
- sendBatch: method({
196
- input: z.object({
197
- messages: z.array(z.object({
198
- id: z.string(),
199
- content: z.string(),
200
- })),
201
- }),
202
- output: z.object({ processed: z.number() }),
203
- error: z.object({ code: z.string() }),
204
- }),
205
- })
206
-
207
- const valid = protocol.validateInput('sendBatch', {
208
- messages: [
209
- { id: '1', content: 'Hello' },
210
- { id: '2', content: 'World' },
211
- ],
212
- })
213
- expect(valid.success).toBe(true)
214
- })
215
-
216
- it('should handle optional fields', () => {
217
- const protocol = defineProtocol({
218
- search: method({
219
- input: z.object({
220
- query: z.string(),
221
- limit: z.number().optional(),
222
- }),
223
- output: z.array(z.string()),
224
- error: z.object({ code: z.string() }),
225
- }),
226
- })
227
-
228
- const withOptional = protocol.validateInput('search', { query: 'test', limit: 10 })
229
- expect(withOptional.success).toBe(true)
230
-
231
- const withoutOptional = protocol.validateInput('search', { query: 'test' })
232
- expect(withoutOptional.success).toBe(true)
233
- })
234
- })
235
- })
@@ -1,248 +0,0 @@
1
- /**
2
- * Message Router Tests
3
- *
4
- * Tests for notification-only message router.
5
- */
6
-
7
- import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
8
- import { z } from 'zod'
9
- import { defineProtocol, notification } from './protocol.js'
10
- import { createRouter, MessageRouter } from './router.js'
11
-
12
- // Test protocols (notification-only)
13
- const receiveProtocol = defineProtocol({
14
- agentMessage: notification({
15
- input: z.object({ sessionId: z.string(), content: z.string() }),
16
- }),
17
- error: notification({
18
- input: z.object({ code: z.string(), message: z.string() }),
19
- }),
20
- })
21
-
22
- const sendProtocol = defineProtocol({
23
- userMessage: notification({
24
- input: z.object({ sessionId: z.string(), content: z.string() }),
25
- }),
26
- status: notification({
27
- input: z.object({ status: z.string() }),
28
- }),
29
- })
30
-
31
- describe('MessageRouter', () => {
32
- let router: MessageRouter<typeof receiveProtocol._def, typeof sendProtocol._def>
33
- let sender: ReturnType<typeof mock>
34
- let sentMessages: string[]
35
-
36
- beforeEach(() => {
37
- router = new MessageRouter(receiveProtocol, sendProtocol)
38
- sentMessages = []
39
- sender = mock((msg: string) => {
40
- sentMessages.push(msg)
41
- return true
42
- })
43
- router.setSender(sender)
44
- })
45
-
46
- afterEach(() => {
47
- // No cleanup needed - no pending requests in notification-only router
48
- })
49
-
50
- describe('handleMessage - incoming notifications', () => {
51
- it('should dispatch notification to handler', async () => {
52
- const handler = mock().mockResolvedValue(undefined)
53
- router.setHandlers({ agentMessage: handler })
54
-
55
- await router.handleMessage(
56
- JSON.stringify({
57
- type: 'agentMessage',
58
- payload: { sessionId: 'sess-1', content: 'Hello' },
59
- ts: Date.now(),
60
- }),
61
- { connectionId: 'conn-1' },
62
- )
63
-
64
- expect(handler).toHaveBeenCalledWith(
65
- { sessionId: 'sess-1', content: 'Hello' },
66
- { connectionId: 'conn-1' },
67
- )
68
- })
69
-
70
- it('should not send any response for notifications', async () => {
71
- const handler = mock().mockResolvedValue(undefined)
72
- router.setHandlers({ agentMessage: handler })
73
-
74
- await router.handleMessage(
75
- JSON.stringify({
76
- type: 'agentMessage',
77
- payload: { sessionId: 'sess-1', content: 'Hello' },
78
- ts: Date.now(),
79
- }),
80
- { connectionId: 'conn-1' },
81
- )
82
-
83
- // Notifications don't send responses
84
- expect(sentMessages.length).toBe(0)
85
- })
86
-
87
- it('should ignore invalid input without error', async () => {
88
- const handler = mock().mockResolvedValue(undefined)
89
- router.setHandlers({ agentMessage: handler })
90
-
91
- await router.handleMessage(
92
- JSON.stringify({
93
- type: 'agentMessage',
94
- payload: { sessionId: 123 }, // Should be string, missing content
95
- ts: Date.now(),
96
- }),
97
- { connectionId: 'conn-1' },
98
- )
99
-
100
- // Handler should not be called for invalid input
101
- expect(handler).not.toHaveBeenCalled()
102
- // No error response sent (notification-only)
103
- expect(sentMessages.length).toBe(0)
104
- })
105
-
106
- it('should ignore messages when no handler registered', async () => {
107
- // No handlers set
108
-
109
- await router.handleMessage(
110
- JSON.stringify({
111
- type: 'agentMessage',
112
- payload: { sessionId: 'test', content: 'Hello' },
113
- ts: Date.now(),
114
- }),
115
- { connectionId: 'conn-1' },
116
- )
117
-
118
- // Should not throw and not send any response
119
- expect(sentMessages.length).toBe(0)
120
- })
121
-
122
- it('should silently ignore handler errors', async () => {
123
- const handler = mock().mockRejectedValue(new Error('Handler failed'))
124
- router.setHandlers({ agentMessage: handler })
125
-
126
- // Should not throw
127
- await router.handleMessage(
128
- JSON.stringify({
129
- type: 'agentMessage',
130
- payload: { sessionId: 'test', content: 'Hello' },
131
- ts: Date.now(),
132
- }),
133
- { connectionId: 'conn-1' },
134
- )
135
-
136
- expect(handler).toHaveBeenCalled()
137
- // No error response sent (notification-only)
138
- expect(sentMessages.length).toBe(0)
139
- })
140
-
141
- it('should handle multiple notification types', async () => {
142
- const agentHandler = mock().mockResolvedValue(undefined)
143
- const errorHandler = mock().mockResolvedValue(undefined)
144
- router.setHandlers({
145
- agentMessage: agentHandler,
146
- error: errorHandler,
147
- })
148
-
149
- await router.handleMessage(
150
- JSON.stringify({
151
- type: 'agentMessage',
152
- payload: { sessionId: 'sess-1', content: 'Hello' },
153
- ts: Date.now(),
154
- }),
155
- { connectionId: 'conn-1' },
156
- )
157
-
158
- await router.handleMessage(
159
- JSON.stringify({
160
- type: 'error',
161
- payload: { code: 'ERR_001', message: 'Something failed' },
162
- ts: Date.now(),
163
- }),
164
- { connectionId: 'conn-1' },
165
- )
166
-
167
- expect(agentHandler).toHaveBeenCalledTimes(1)
168
- expect(errorHandler).toHaveBeenCalledTimes(1)
169
- })
170
- })
171
-
172
- describe('notify - outbound notifications', () => {
173
- it('should send notification', () => {
174
- router.notify('userMessage', { sessionId: 'test', content: 'Hello' })
175
-
176
- expect(sentMessages.length).toBe(1)
177
- const message = JSON.parse(sentMessages[0])
178
- expect(message.type).toBe('userMessage')
179
- expect(message.payload).toEqual({ sessionId: 'test', content: 'Hello' })
180
- })
181
-
182
- it('should include timestamp', () => {
183
- const before = Date.now()
184
- router.notify('status', { status: 'online' })
185
- const after = Date.now()
186
-
187
- const message = JSON.parse(sentMessages[0])
188
- expect(message.ts).toBeGreaterThanOrEqual(before)
189
- expect(message.ts).toBeLessThanOrEqual(after)
190
- })
191
-
192
- it('should return true on success', () => {
193
- const result = router.notify('userMessage', { sessionId: 'test', content: 'Hello' })
194
- expect(result).toBe(true)
195
- })
196
-
197
- it('should return false if no sender configured', () => {
198
- const noSenderRouter = new MessageRouter(receiveProtocol, sendProtocol)
199
- const result = noSenderRouter.notify('userMessage', { sessionId: 'test', content: 'Hello' })
200
- expect(result).toBe(false)
201
- })
202
-
203
- it('should return false if sender returns false', () => {
204
- const failingRouter = new MessageRouter(receiveProtocol, sendProtocol)
205
- failingRouter.setSender(() => false)
206
-
207
- const result = failingRouter.notify('userMessage', { sessionId: 'test', content: 'Hello' })
208
- expect(result).toBe(false)
209
- })
210
- })
211
-
212
- describe('createRouter factory', () => {
213
- it('should create a router instance', () => {
214
- const router = createRouter(receiveProtocol, sendProtocol)
215
- expect(router).toBeInstanceOf(MessageRouter)
216
- })
217
- })
218
-
219
- describe('invalid JSON handling', () => {
220
- it('should ignore invalid JSON', async () => {
221
- const handler = mock()
222
- router.setHandlers({ agentMessage: handler })
223
-
224
- await router.handleMessage('not valid json', { connectionId: 'test' })
225
-
226
- expect(handler).not.toHaveBeenCalled()
227
- })
228
- })
229
-
230
- describe('unknown message types', () => {
231
- it('should ignore unknown message types', async () => {
232
- const handler = mock()
233
- router.setHandlers({ agentMessage: handler })
234
-
235
- await router.handleMessage(
236
- JSON.stringify({
237
- type: 'unknownType',
238
- payload: {},
239
- ts: Date.now(),
240
- }),
241
- { connectionId: 'test' },
242
- )
243
-
244
- expect(handler).not.toHaveBeenCalled()
245
- expect(sentMessages.length).toBe(0)
246
- })
247
- })
248
- })