@roj-ai/transport 0.0.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.
Files changed (64) hide show
  1. package/dist/client/client-connection.d.ts +48 -0
  2. package/dist/client/client-connection.d.ts.map +1 -0
  3. package/dist/client/client-connection.js +160 -0
  4. package/dist/client/index.d.ts +10 -0
  5. package/dist/client/index.d.ts.map +1 -0
  6. package/dist/client/index.js +7 -0
  7. package/dist/core/connection.d.ts +59 -0
  8. package/dist/core/connection.d.ts.map +1 -0
  9. package/dist/core/connection.js +165 -0
  10. package/dist/core/index.d.ts +16 -0
  11. package/dist/core/index.d.ts.map +1 -0
  12. package/dist/core/index.js +13 -0
  13. package/dist/core/protocol.d.ts +206 -0
  14. package/dist/core/protocol.d.ts.map +1 -0
  15. package/dist/core/protocol.js +93 -0
  16. package/dist/core/result.d.ts +26 -0
  17. package/dist/core/result.d.ts.map +1 -0
  18. package/dist/core/result.js +20 -0
  19. package/dist/core/router.d.ts +79 -0
  20. package/dist/core/router.d.ts.map +1 -0
  21. package/dist/core/router.js +119 -0
  22. package/dist/core/types.d.ts +44 -0
  23. package/dist/core/types.d.ts.map +1 -0
  24. package/dist/core/types.js +14 -0
  25. package/dist/index.d.ts +24 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +26 -0
  28. package/dist/platform/browser.d.ts +14 -0
  29. package/dist/platform/browser.d.ts.map +1 -0
  30. package/dist/platform/browser.js +65 -0
  31. package/dist/platform/bun.d.ts +69 -0
  32. package/dist/platform/bun.d.ts.map +1 -0
  33. package/dist/platform/bun.js +152 -0
  34. package/dist/platform/types.d.ts +83 -0
  35. package/dist/platform/types.d.ts.map +1 -0
  36. package/dist/platform/types.js +14 -0
  37. package/dist/server/connection-manager.d.ts +106 -0
  38. package/dist/server/connection-manager.d.ts.map +1 -0
  39. package/dist/server/connection-manager.js +234 -0
  40. package/dist/server/index.d.ts +12 -0
  41. package/dist/server/index.d.ts.map +1 -0
  42. package/dist/server/index.js +8 -0
  43. package/dist/server/server-connection.d.ts +50 -0
  44. package/dist/server/server-connection.d.ts.map +1 -0
  45. package/dist/server/server-connection.js +84 -0
  46. package/package.json +52 -0
  47. package/src/__tests__/integration.test.ts +443 -0
  48. package/src/client/client-connection.ts +207 -0
  49. package/src/client/index.ts +24 -0
  50. package/src/core/connection.ts +203 -0
  51. package/src/core/index.ts +52 -0
  52. package/src/core/protocol.test.ts +235 -0
  53. package/src/core/protocol.ts +348 -0
  54. package/src/core/result.ts +35 -0
  55. package/src/core/router.test.ts +248 -0
  56. package/src/core/router.ts +186 -0
  57. package/src/core/types.ts +85 -0
  58. package/src/index.ts +80 -0
  59. package/src/platform/browser.ts +79 -0
  60. package/src/platform/bun.ts +220 -0
  61. package/src/platform/types.ts +102 -0
  62. package/src/server/connection-manager.ts +298 -0
  63. package/src/server/index.ts +26 -0
  64. package/src/server/server-connection.ts +122 -0
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Protocol Definition API
3
+ *
4
+ * Provides a type-safe way to define RPC-like protocols for WebSocket communication.
5
+ * Inspired by tRPC but designed for bidirectional WebSocket protocols.
6
+ */
7
+
8
+ import { z } from 'zod'
9
+ import type { Result } from './result.js'
10
+
11
+ // ============================================================================
12
+ // Method/Notification Definition Types
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Definition for a request-response method.
17
+ */
18
+ export interface MethodDef<
19
+ TInput extends z.ZodTypeAny = z.ZodTypeAny,
20
+ TOutput extends z.ZodTypeAny = z.ZodTypeAny,
21
+ TError extends z.ZodTypeAny = z.ZodTypeAny,
22
+ > {
23
+ _type: 'method'
24
+ input: TInput
25
+ output: TOutput
26
+ error: TError
27
+ }
28
+
29
+ /**
30
+ * Definition for a fire-and-forget notification.
31
+ */
32
+ export interface NotificationDef<TInput extends z.ZodTypeAny = z.ZodTypeAny> {
33
+ _type: 'notification'
34
+ input: TInput
35
+ }
36
+
37
+ /**
38
+ * A single endpoint definition (method or notification).
39
+ */
40
+ export type EndpointDef = MethodDef | NotificationDef
41
+
42
+ /**
43
+ * Protocol definition - a collection of named endpoints.
44
+ */
45
+ export interface ProtocolDef {
46
+ [key: string]: EndpointDef
47
+ }
48
+
49
+ // ============================================================================
50
+ // Type Inference Utilities
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Extract input type from an endpoint definition.
55
+ */
56
+ export type InferInput<T extends EndpointDef> = T extends MethodDef<infer I, infer _O, infer _E> ? z.infer<I>
57
+ : T extends NotificationDef<infer I> ? z.infer<I>
58
+ : never
59
+
60
+ /**
61
+ * Extract output type from a method definition.
62
+ */
63
+ export type InferOutput<T extends MethodDef> = T extends MethodDef<infer _I, infer O, infer _E> ? z.infer<O>
64
+ : never
65
+
66
+ /**
67
+ * Extract error type from a method definition.
68
+ */
69
+ export type InferError<T extends MethodDef> = T extends MethodDef<infer _I, infer _O, infer E> ? z.infer<E>
70
+ : never
71
+
72
+ /**
73
+ * Check if endpoint is a method.
74
+ */
75
+ export type IsMethod<T extends EndpointDef> = T extends MethodDef ? true : false
76
+
77
+ /**
78
+ * Check if endpoint is a notification.
79
+ */
80
+ export type IsNotification<T extends EndpointDef> = T extends NotificationDef ? true : false
81
+
82
+ // ============================================================================
83
+ // Handler Types
84
+ // ============================================================================
85
+
86
+ /**
87
+ * Handler context passed to method/notification handlers.
88
+ */
89
+ export interface HandlerContext {
90
+ /** Connection identifier */
91
+ connectionId: string
92
+ /** Additional context data (platform-specific) */
93
+ [key: string]: unknown
94
+ }
95
+
96
+ /**
97
+ * Handler for a method endpoint.
98
+ */
99
+ export type MethodHandler<T extends MethodDef> = (
100
+ input: InferInput<T>,
101
+ ctx: HandlerContext,
102
+ ) => Promise<Result<InferOutput<T>, InferError<T>>>
103
+
104
+ /**
105
+ * Handler for a notification endpoint.
106
+ */
107
+ export type NotificationHandler<T extends NotificationDef> = (
108
+ input: InferInput<T>,
109
+ ctx: HandlerContext,
110
+ ) => Promise<void>
111
+
112
+ /**
113
+ * Handler for an endpoint (method or notification).
114
+ */
115
+ export type EndpointHandler<T extends EndpointDef> = T extends MethodDef ? MethodHandler<T>
116
+ : T extends NotificationDef ? NotificationHandler<T>
117
+ : never
118
+
119
+ /**
120
+ * Handlers object for a protocol.
121
+ */
122
+ export type ProtocolHandlers<T extends ProtocolDef> = {
123
+ [K in keyof T]: EndpointHandler<T[K]>
124
+ }
125
+
126
+ // ============================================================================
127
+ // Caller Types (for making outbound calls)
128
+ // ============================================================================
129
+
130
+ /**
131
+ * Caller function for a method endpoint.
132
+ */
133
+ export type MethodCaller<T extends MethodDef> = (
134
+ input: InferInput<T>,
135
+ ) => Promise<Result<InferOutput<T>, InferError<T>>>
136
+
137
+ /**
138
+ * Notifier function for a notification endpoint.
139
+ */
140
+ export type NotificationNotifier<T extends NotificationDef> = (
141
+ input: InferInput<T>,
142
+ ) => void
143
+
144
+ /**
145
+ * Caller object for methods in a protocol.
146
+ * Only includes method endpoints.
147
+ */
148
+ export type ProtocolCaller<T extends ProtocolDef> = {
149
+ [K in keyof T as T[K] extends MethodDef ? K : never]: T[K] extends MethodDef ? MethodCaller<T[K]>
150
+ : never
151
+ }
152
+
153
+ /**
154
+ * Notifier object for notifications in a protocol.
155
+ * Only includes notification endpoints.
156
+ */
157
+ export type ProtocolNotifier<T extends ProtocolDef> = {
158
+ [K in keyof T as T[K] extends NotificationDef ? K : never]: T[K] extends NotificationDef ? NotificationNotifier<T[K]>
159
+ : never
160
+ }
161
+
162
+ // ============================================================================
163
+ // Definition Functions
164
+ // ============================================================================
165
+
166
+ /**
167
+ * Options for method definition.
168
+ */
169
+ export interface MethodOptions<
170
+ TInput extends z.ZodTypeAny,
171
+ TOutput extends z.ZodTypeAny,
172
+ TError extends z.ZodTypeAny,
173
+ > {
174
+ /** Input schema (what the caller sends) */
175
+ input: TInput
176
+ /** Output schema (what the handler returns on success) */
177
+ output: TOutput
178
+ /** Error schema (what the handler returns on failure) */
179
+ error: TError
180
+ }
181
+
182
+ /**
183
+ * Define a request-response method endpoint.
184
+ */
185
+ export function method<
186
+ TInput extends z.ZodTypeAny,
187
+ TOutput extends z.ZodTypeAny,
188
+ TError extends z.ZodTypeAny,
189
+ >(options: MethodOptions<TInput, TOutput, TError>): MethodDef<TInput, TOutput, TError> {
190
+ return {
191
+ _type: 'method',
192
+ input: options.input,
193
+ output: options.output,
194
+ error: options.error,
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Options for notification definition.
200
+ */
201
+ export interface NotificationOptions<TInput extends z.ZodTypeAny> {
202
+ /** Input schema (what is sent) */
203
+ input: TInput
204
+ }
205
+
206
+ /**
207
+ * Define a fire-and-forget notification endpoint.
208
+ */
209
+ export function notification<TInput extends z.ZodTypeAny>(
210
+ options: NotificationOptions<TInput>,
211
+ ): NotificationDef<TInput> {
212
+ return {
213
+ _type: 'notification',
214
+ input: options.input,
215
+ }
216
+ }
217
+
218
+ // ============================================================================
219
+ // Protocol Definition
220
+ // ============================================================================
221
+
222
+ /**
223
+ * Safe parse result type (matches Zod's structure).
224
+ */
225
+ export type SafeParseResult<T> =
226
+ | { success: true; data: T }
227
+ | { success: false; error: { message: string } }
228
+
229
+ /**
230
+ * Protocol object with definition and metadata.
231
+ */
232
+ export interface Protocol<T extends ProtocolDef> {
233
+ /** The protocol definition */
234
+ _def: T
235
+ /** Get endpoint definition by name */
236
+ getEndpoint<K extends keyof T>(name: K): T[K]
237
+ /** Check if endpoint is a method */
238
+ isMethod(name: keyof T): boolean
239
+ /** Check if endpoint is a notification */
240
+ isNotification(name: keyof T): boolean
241
+ /** Get all method names */
242
+ getMethodNames(): (keyof T)[]
243
+ /** Get all notification names */
244
+ getNotificationNames(): (keyof T)[]
245
+ /** Validate input for an endpoint */
246
+ validateInput<K extends keyof T>(name: K, input: unknown): SafeParseResult<InferInput<T[K]>>
247
+ /** Validate output for a method endpoint */
248
+ validateOutput<K extends keyof T>(name: K, output: unknown): SafeParseResult<unknown>
249
+ }
250
+
251
+ /**
252
+ * Define a protocol with typed endpoints.
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const clientProtocol = defineProtocol({
257
+ * subscribe: method({
258
+ * input: z.object({ sessionId: z.string() }),
259
+ * output: z.void(),
260
+ * error: z.object({ code: z.literal('SESSION_NOT_FOUND') }),
261
+ * }),
262
+ *
263
+ * userMessage: method({
264
+ * input: z.object({ sessionId: z.string(), content: z.string() }),
265
+ * output: z.object({ messageId: z.string() }),
266
+ * error: z.object({ code: z.literal('AGENT_BUSY') }),
267
+ * }),
268
+ * });
269
+ *
270
+ * const serverProtocol = defineProtocol({
271
+ * agentMessage: notification({
272
+ * input: z.object({ sessionId: z.string(), content: z.string() }),
273
+ * }),
274
+ * });
275
+ * ```
276
+ */
277
+ export function defineProtocol<T extends ProtocolDef>(def: T): Protocol<T> {
278
+ return {
279
+ _def: def,
280
+
281
+ getEndpoint<K extends keyof T>(name: K): T[K] {
282
+ return def[name]
283
+ },
284
+
285
+ isMethod(name: keyof T): boolean {
286
+ const endpoint = def[name]
287
+ return endpoint?._type === 'method'
288
+ },
289
+
290
+ isNotification(name: keyof T): boolean {
291
+ const endpoint = def[name]
292
+ return endpoint?._type === 'notification'
293
+ },
294
+
295
+ getMethodNames(): (keyof T)[] {
296
+ return Object.keys(def).filter((key) => def[key]._type === 'method') as (keyof T)[]
297
+ },
298
+
299
+ getNotificationNames(): (keyof T)[] {
300
+ return Object.keys(def).filter((key) => def[key]._type === 'notification') as (keyof T)[]
301
+ },
302
+
303
+ validateInput<K extends keyof T>(name: K, input: unknown): SafeParseResult<InferInput<T[K]>> {
304
+ const endpoint = def[name]
305
+ const result = endpoint.input.safeParse(input)
306
+ if (result.success) {
307
+ return { success: true, data: result.data as InferInput<T[K]> }
308
+ }
309
+ return { success: false, error: { message: result.error.message } }
310
+ },
311
+
312
+ validateOutput<K extends keyof T>(name: K, output: unknown): SafeParseResult<unknown> {
313
+ const endpoint = def[name]
314
+ if (endpoint._type !== 'method') {
315
+ throw new Error(`Endpoint ${String(name)} is not a method`)
316
+ }
317
+ const result = endpoint.output.safeParse(output)
318
+ if (result.success) {
319
+ return { success: true, data: result.data }
320
+ }
321
+ return { success: false, error: { message: result.error.message } }
322
+ },
323
+ }
324
+ }
325
+
326
+ // ============================================================================
327
+ // Type Extraction Helpers
328
+ // ============================================================================
329
+
330
+ /**
331
+ * Extract the protocol definition type from a Protocol.
332
+ */
333
+ export type ExtractProtocolDef<T> = T extends Protocol<infer P> ? P : never
334
+
335
+ /**
336
+ * Create a typed handlers object type for a protocol.
337
+ */
338
+ export type HandlersFor<T extends Protocol<ProtocolDef>> = ProtocolHandlers<ExtractProtocolDef<T>>
339
+
340
+ /**
341
+ * Create a typed caller object type for a protocol.
342
+ */
343
+ export type CallerFor<T extends Protocol<ProtocolDef>> = ProtocolCaller<ExtractProtocolDef<T>>
344
+
345
+ /**
346
+ * Create a typed notifier object type for a protocol.
347
+ */
348
+ export type NotifierFor<T extends Protocol<ProtocolDef>> = ProtocolNotifier<ExtractProtocolDef<T>>
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Result type for explicit error handling without exceptions.
3
+ * Inspired by Rust/Go approach.
4
+ */
5
+
6
+ export type Result<T, E = Error> =
7
+ | { ok: true; value: T }
8
+ | { ok: false; error: E }
9
+
10
+ // Constructors
11
+ export const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value })
12
+ export const Err = <E>(error: E): Result<never, E> => ({ ok: false, error })
13
+
14
+ // Type guards
15
+ export const isOk = <T, E>(result: Result<T, E>): result is { ok: true; value: T } => result.ok
16
+ export const isErr = <T, E>(result: Result<T, E>): result is { ok: false; error: E } => !result.ok
17
+
18
+ // Helper functions
19
+ export const mapResult = <T, U, E>(
20
+ result: Result<T, E>,
21
+ fn: (value: T) => U,
22
+ ): Result<U, E> => result.ok ? Ok(fn(result.value)) : result
23
+
24
+ export const flatMapResult = <T, U, E>(
25
+ result: Result<T, E>,
26
+ fn: (value: T) => Result<U, E>,
27
+ ): Result<U, E> => result.ok ? fn(result.value) : result
28
+
29
+ // Unwrap functions
30
+ export const unwrapOr = <T, E>(result: Result<T, E>, defaultValue: T): T => result.ok ? result.value : defaultValue
31
+
32
+ export const unwrapOrThrow = <T, E>(result: Result<T, E>): T => {
33
+ if (result.ok) return result.value
34
+ throw result.error
35
+ }
@@ -0,0 +1,248 @@
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
+ })