@comprehend/telemetry-node 0.1.3 → 0.1.4
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.
- package/.claude/settings.local.json +3 -2
- package/DEVELOPMENT.md +69 -0
- package/README.md +127 -0
- package/dist/ComprehendDevSpanProcessor.js +1 -0
- package/dist/ComprehendDevSpanProcessor.test.d.ts +1 -0
- package/dist/ComprehendDevSpanProcessor.test.js +674 -0
- package/dist/WebSocketConnection.test.d.ts +1 -0
- package/dist/WebSocketConnection.test.js +406 -0
- package/jest.config.js +1 -0
- package/package.json +2 -2
- package/src/ComprehendDevSpanProcessor.test.ts +822 -0
- package/src/ComprehendDevSpanProcessor.ts +1 -0
- package/src/WebSocketConnection.test.ts +535 -0
|
@@ -144,6 +144,7 @@ export class ComprehendDevSpanProcessor implements SpanProcessor {
|
|
|
144
144
|
...(environment ? { environment } : {})
|
|
145
145
|
};
|
|
146
146
|
this.ingestMessage(message);
|
|
147
|
+
return newService;
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
private processHTTPRoute(service: ObservedService, route: string, method: string, span: ReadableSpan): void {
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { WebSocketConnection } from './WebSocketConnection';
|
|
2
|
+
import {
|
|
3
|
+
InitMessage,
|
|
4
|
+
NewObservedServiceMessage,
|
|
5
|
+
NewObservedHttpRouteMessage,
|
|
6
|
+
ObservationMessage,
|
|
7
|
+
HttpServerObservation,
|
|
8
|
+
ObservationOutputMessage
|
|
9
|
+
} from './wire-protocol';
|
|
10
|
+
|
|
11
|
+
// Mock the ws WebSocket library
|
|
12
|
+
jest.mock('ws');
|
|
13
|
+
|
|
14
|
+
import WebSocket from 'ws';
|
|
15
|
+
|
|
16
|
+
const MockedWebSocket = WebSocket as jest.MockedClass<typeof WebSocket>;
|
|
17
|
+
|
|
18
|
+
// Mock WebSocket instance
|
|
19
|
+
interface MockWebSocketInstance {
|
|
20
|
+
on: jest.Mock;
|
|
21
|
+
send: jest.Mock;
|
|
22
|
+
close: jest.Mock;
|
|
23
|
+
readyState: number;
|
|
24
|
+
OPEN: number;
|
|
25
|
+
CLOSED: number;
|
|
26
|
+
// Event handlers that tests can trigger
|
|
27
|
+
_triggerOpen: () => void;
|
|
28
|
+
_triggerMessage: (data: string) => void;
|
|
29
|
+
_triggerClose: (code: number, reason: string) => void;
|
|
30
|
+
_triggerError: (error: Error) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('WebSocketConnection', () => {
|
|
34
|
+
let mockSocket: MockWebSocketInstance;
|
|
35
|
+
let connection: WebSocketConnection;
|
|
36
|
+
let logMessages: string[];
|
|
37
|
+
let mockLogger: jest.Mock;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
// Reset all mocks
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
jest.clearAllTimers();
|
|
43
|
+
jest.useFakeTimers();
|
|
44
|
+
|
|
45
|
+
logMessages = [];
|
|
46
|
+
mockLogger = jest.fn((message: string) => {
|
|
47
|
+
logMessages.push(message);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Create mock WebSocket instance with event handling
|
|
51
|
+
mockSocket = {
|
|
52
|
+
on: jest.fn(),
|
|
53
|
+
send: jest.fn(),
|
|
54
|
+
close: jest.fn(),
|
|
55
|
+
readyState: WebSocket.CLOSED,
|
|
56
|
+
OPEN: WebSocket.OPEN,
|
|
57
|
+
CLOSED: WebSocket.CLOSED,
|
|
58
|
+
_triggerOpen: function() { /* will be set up below */ },
|
|
59
|
+
_triggerMessage: function(data: string) { /* will be set up below */ },
|
|
60
|
+
_triggerClose: function(code: number, reason: string) { /* will be set up below */ },
|
|
61
|
+
_triggerError: function(error: Error) { /* will be set up below */ }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Set up event handler storage and triggering
|
|
65
|
+
const eventHandlers: Record<string, Function> = {};
|
|
66
|
+
mockSocket.on.mockImplementation((event: string, handler: Function) => {
|
|
67
|
+
eventHandlers[event] = handler;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
mockSocket._triggerOpen = () => {
|
|
71
|
+
mockSocket.readyState = WebSocket.OPEN;
|
|
72
|
+
eventHandlers['open']?.();
|
|
73
|
+
};
|
|
74
|
+
mockSocket._triggerMessage = (data: string) => {
|
|
75
|
+
const buffer = Buffer.from(data);
|
|
76
|
+
eventHandlers['message']?.(buffer);
|
|
77
|
+
};
|
|
78
|
+
mockSocket._triggerClose = (code: number, reason: string) => {
|
|
79
|
+
mockSocket.readyState = WebSocket.CLOSED;
|
|
80
|
+
eventHandlers['close']?.(code, Buffer.from(reason));
|
|
81
|
+
};
|
|
82
|
+
mockSocket._triggerError = (error: Error) => {
|
|
83
|
+
eventHandlers['error']?.(error);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Mock WebSocket constructor to return our mock instance
|
|
87
|
+
MockedWebSocket.mockImplementation(() => mockSocket as any);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
jest.useRealTimers();
|
|
92
|
+
if (connection) {
|
|
93
|
+
connection.close();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('Connection Establishment', () => {
|
|
98
|
+
it('should create WebSocket connection with correct URL', () => {
|
|
99
|
+
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
100
|
+
|
|
101
|
+
expect(MockedWebSocket).toHaveBeenCalledWith('wss://ingestion.comprehend.dev/test-org/observations');
|
|
102
|
+
expect(mockSocket.on).toHaveBeenCalledWith('open', expect.any(Function));
|
|
103
|
+
expect(mockSocket.on).toHaveBeenCalledWith('message', expect.any(Function));
|
|
104
|
+
expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
105
|
+
expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should send init message on connection open', () => {
|
|
109
|
+
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
110
|
+
|
|
111
|
+
mockSocket._triggerOpen();
|
|
112
|
+
|
|
113
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify({
|
|
114
|
+
event: 'init',
|
|
115
|
+
protocolVersion: 1,
|
|
116
|
+
token: 'test-token'
|
|
117
|
+
}));
|
|
118
|
+
expect(logMessages).toContain('WebSocket connected. Sending init/auth message.');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should work without logger', () => {
|
|
122
|
+
connection = new WebSocketConnection('test-org', 'test-token');
|
|
123
|
+
|
|
124
|
+
mockSocket._triggerOpen();
|
|
125
|
+
|
|
126
|
+
expect(MockedWebSocket).toHaveBeenCalled();
|
|
127
|
+
expect(mockSocket.send).toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('Authorization Flow', () => {
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
134
|
+
mockSocket._triggerOpen();
|
|
135
|
+
jest.clearAllMocks(); // Clear the init message send
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle authorization acknowledgment', () => {
|
|
139
|
+
const authAck: ObservationOutputMessage = {
|
|
140
|
+
type: 'ack-authorized'
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
mockSocket._triggerMessage(JSON.stringify(authAck));
|
|
144
|
+
|
|
145
|
+
expect(logMessages).toContain('Authorization acknowledged by server.');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should replay queued messages after authorization', () => {
|
|
149
|
+
// Queue some messages before authorization
|
|
150
|
+
const serviceMessage: NewObservedServiceMessage = {
|
|
151
|
+
event: 'new-entity',
|
|
152
|
+
type: 'service',
|
|
153
|
+
hash: 'test-hash-1',
|
|
154
|
+
name: 'test-service'
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const observationMessage: ObservationMessage = {
|
|
158
|
+
event: 'observations',
|
|
159
|
+
seq: 1,
|
|
160
|
+
observations: [{
|
|
161
|
+
type: 'http-server',
|
|
162
|
+
subject: 'test-subject',
|
|
163
|
+
timestamp: [1700000000, 0],
|
|
164
|
+
path: '/test',
|
|
165
|
+
status: 200,
|
|
166
|
+
duration: [0, 100000000]
|
|
167
|
+
} as HttpServerObservation]
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
connection.sendMessage(serviceMessage);
|
|
171
|
+
connection.sendMessage(observationMessage);
|
|
172
|
+
|
|
173
|
+
// Should not send immediately (not authorized yet)
|
|
174
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
175
|
+
|
|
176
|
+
// Now authorize
|
|
177
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
178
|
+
|
|
179
|
+
// Should replay both messages
|
|
180
|
+
expect(mockSocket.send).toHaveBeenCalledTimes(2);
|
|
181
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
|
|
182
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should send messages immediately when already authorized', () => {
|
|
186
|
+
// First authorize
|
|
187
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
188
|
+
jest.clearAllMocks();
|
|
189
|
+
|
|
190
|
+
const serviceMessage: NewObservedServiceMessage = {
|
|
191
|
+
event: 'new-entity',
|
|
192
|
+
type: 'service',
|
|
193
|
+
hash: 'test-hash',
|
|
194
|
+
name: 'test-service'
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
connection.sendMessage(serviceMessage);
|
|
198
|
+
|
|
199
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('Message Acknowledgment Handling', () => {
|
|
204
|
+
beforeEach(() => {
|
|
205
|
+
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
206
|
+
mockSocket._triggerOpen();
|
|
207
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
208
|
+
jest.clearAllMocks();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should handle entity/interaction acknowledgments', () => {
|
|
212
|
+
const serviceMessage: NewObservedServiceMessage = {
|
|
213
|
+
event: 'new-entity',
|
|
214
|
+
type: 'service',
|
|
215
|
+
hash: 'service-hash',
|
|
216
|
+
name: 'test-service'
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const routeMessage: NewObservedHttpRouteMessage = {
|
|
220
|
+
event: 'new-entity',
|
|
221
|
+
type: 'http-route',
|
|
222
|
+
hash: 'route-hash',
|
|
223
|
+
parent: 'service-hash',
|
|
224
|
+
method: 'GET',
|
|
225
|
+
route: '/test'
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Send messages
|
|
229
|
+
connection.sendMessage(serviceMessage);
|
|
230
|
+
connection.sendMessage(routeMessage);
|
|
231
|
+
|
|
232
|
+
// Acknowledge the service message
|
|
233
|
+
mockSocket._triggerMessage(JSON.stringify({
|
|
234
|
+
type: 'ack-observed',
|
|
235
|
+
hash: 'service-hash'
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
// Clear previous calls before reconnection test
|
|
239
|
+
mockSocket.send.mockClear();
|
|
240
|
+
|
|
241
|
+
// If we reconnect, only the route message should be replayed
|
|
242
|
+
mockSocket._triggerClose(1000, 'test close');
|
|
243
|
+
jest.advanceTimersByTime(1000);
|
|
244
|
+
mockSocket._triggerOpen();
|
|
245
|
+
|
|
246
|
+
// Clear the init message that gets sent on reconnection
|
|
247
|
+
mockSocket.send.mockClear();
|
|
248
|
+
|
|
249
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
250
|
+
|
|
251
|
+
// Should only send the unacknowledged route message
|
|
252
|
+
expect(mockSocket.send).toHaveBeenCalledTimes(1);
|
|
253
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(routeMessage));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should handle observation acknowledgments', () => {
|
|
257
|
+
const observationMessage1: ObservationMessage = {
|
|
258
|
+
event: 'observations',
|
|
259
|
+
seq: 1,
|
|
260
|
+
observations: []
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const observationMessage2: ObservationMessage = {
|
|
264
|
+
event: 'observations',
|
|
265
|
+
seq: 2,
|
|
266
|
+
observations: []
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Send messages
|
|
270
|
+
connection.sendMessage(observationMessage1);
|
|
271
|
+
connection.sendMessage(observationMessage2);
|
|
272
|
+
|
|
273
|
+
// Acknowledge first observation
|
|
274
|
+
mockSocket._triggerMessage(JSON.stringify({
|
|
275
|
+
type: 'ack-observations',
|
|
276
|
+
seq: 1
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
// Clear previous calls before reconnection test
|
|
280
|
+
mockSocket.send.mockClear();
|
|
281
|
+
|
|
282
|
+
// Reconnect - only second observation should be replayed
|
|
283
|
+
mockSocket._triggerClose(1000, 'test close');
|
|
284
|
+
jest.advanceTimersByTime(1000);
|
|
285
|
+
mockSocket._triggerOpen();
|
|
286
|
+
|
|
287
|
+
// Clear the init message that gets sent on reconnection
|
|
288
|
+
mockSocket.send.mockClear();
|
|
289
|
+
|
|
290
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
291
|
+
|
|
292
|
+
// Should only send the unacknowledged second observation message
|
|
293
|
+
expect(mockSocket.send).toHaveBeenCalledTimes(1);
|
|
294
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage2));
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('Reconnection Logic', () => {
|
|
299
|
+
beforeEach(() => {
|
|
300
|
+
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
301
|
+
mockSocket._triggerOpen();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should reconnect automatically on close', () => {
|
|
305
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
306
|
+
|
|
307
|
+
mockSocket._triggerClose(1000, 'Normal closure');
|
|
308
|
+
|
|
309
|
+
expect(logMessages).toContain('WebSocket disconnected. Code: 1000, Reason: Normal closure');
|
|
310
|
+
|
|
311
|
+
// Should schedule reconnection
|
|
312
|
+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
|
|
313
|
+
|
|
314
|
+
// Advance timer to trigger reconnection
|
|
315
|
+
jest.advanceTimersByTime(1000);
|
|
316
|
+
|
|
317
|
+
// Should create new WebSocket connection
|
|
318
|
+
expect(MockedWebSocket).toHaveBeenCalledTimes(2);
|
|
319
|
+
|
|
320
|
+
setTimeoutSpy.mockRestore();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should reset authorization state on close', () => {
|
|
324
|
+
// First authorize
|
|
325
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
326
|
+
|
|
327
|
+
// Send a message (should send immediately)
|
|
328
|
+
const testMessage: NewObservedServiceMessage = {
|
|
329
|
+
event: 'new-entity',
|
|
330
|
+
type: 'service',
|
|
331
|
+
hash: 'test-hash',
|
|
332
|
+
name: 'test-service'
|
|
333
|
+
};
|
|
334
|
+
connection.sendMessage(testMessage);
|
|
335
|
+
expect(mockSocket.send).toHaveBeenCalled();
|
|
336
|
+
|
|
337
|
+
jest.clearAllMocks();
|
|
338
|
+
|
|
339
|
+
// Close connection
|
|
340
|
+
mockSocket._triggerClose(1000, 'test');
|
|
341
|
+
|
|
342
|
+
// Send another message - should queue (not send immediately)
|
|
343
|
+
connection.sendMessage(testMessage);
|
|
344
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should not reconnect when explicitly closed', () => {
|
|
348
|
+
connection.close();
|
|
349
|
+
mockSocket._triggerClose(1000, 'Explicit close');
|
|
350
|
+
|
|
351
|
+
// Should not schedule reconnection
|
|
352
|
+
jest.advanceTimersByTime(5000);
|
|
353
|
+
expect(MockedWebSocket).toHaveBeenCalledTimes(1); // Only initial connection
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('Error Handling', () => {
|
|
358
|
+
beforeEach(() => {
|
|
359
|
+
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should log WebSocket errors', () => {
|
|
363
|
+
const testError = new Error('Connection failed');
|
|
364
|
+
mockSocket._triggerError(testError);
|
|
365
|
+
|
|
366
|
+
expect(logMessages).toContain('WebSocket encountered an error: Connection failed');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should handle malformed server messages gracefully', () => {
|
|
370
|
+
mockSocket._triggerOpen();
|
|
371
|
+
mockSocket._triggerMessage('{invalid json}');
|
|
372
|
+
|
|
373
|
+
// Check that an error message was logged (exact message varies by Node version)
|
|
374
|
+
const errorLogExists = logMessages.some(msg =>
|
|
375
|
+
msg.startsWith('Error parsing message from server:') &&
|
|
376
|
+
msg.includes('JSON')
|
|
377
|
+
);
|
|
378
|
+
expect(errorLogExists).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should handle non-string error objects in message parsing', () => {
|
|
382
|
+
mockSocket._triggerOpen();
|
|
383
|
+
|
|
384
|
+
// Mock JSON.parse to throw a non-Error object
|
|
385
|
+
const originalParse = JSON.parse;
|
|
386
|
+
JSON.parse = jest.fn().mockImplementation(() => {
|
|
387
|
+
throw 'string error';
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
mockSocket._triggerMessage('{}');
|
|
391
|
+
|
|
392
|
+
expect(logMessages).toContain('Error parsing message from server: string error');
|
|
393
|
+
|
|
394
|
+
// Restore original JSON.parse
|
|
395
|
+
JSON.parse = originalParse;
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should not send messages when socket is not open', () => {
|
|
399
|
+
mockSocket.readyState = WebSocket.CLOSED;
|
|
400
|
+
mockSocket._triggerOpen();
|
|
401
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
402
|
+
|
|
403
|
+
// Clear the init and any previous messages
|
|
404
|
+
mockSocket.send.mockClear();
|
|
405
|
+
|
|
406
|
+
// Force socket to closed state
|
|
407
|
+
mockSocket.readyState = WebSocket.CLOSED;
|
|
408
|
+
|
|
409
|
+
const testMessage: NewObservedServiceMessage = {
|
|
410
|
+
event: 'new-entity',
|
|
411
|
+
type: 'service',
|
|
412
|
+
hash: 'test-hash',
|
|
413
|
+
name: 'test-service'
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
connection.sendMessage(testMessage);
|
|
417
|
+
|
|
418
|
+
// Should not call send when socket is closed
|
|
419
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe('Connection Lifecycle', () => {
|
|
424
|
+
it('should close WebSocket connection properly', () => {
|
|
425
|
+
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
426
|
+
|
|
427
|
+
connection.close();
|
|
428
|
+
|
|
429
|
+
expect(mockSocket.close).toHaveBeenCalled();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should handle close when socket is null', () => {
|
|
433
|
+
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
434
|
+
|
|
435
|
+
// Simulate socket being null
|
|
436
|
+
(connection as any).socket = null;
|
|
437
|
+
|
|
438
|
+
expect(() => connection.close()).not.toThrow();
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
describe('Message Types and Queuing', () => {
|
|
443
|
+
beforeEach(() => {
|
|
444
|
+
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
445
|
+
mockSocket._triggerOpen();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should queue new-entity messages', () => {
|
|
449
|
+
// Clear the initial init message
|
|
450
|
+
mockSocket.send.mockClear();
|
|
451
|
+
|
|
452
|
+
const serviceMessage: NewObservedServiceMessage = {
|
|
453
|
+
event: 'new-entity',
|
|
454
|
+
type: 'service',
|
|
455
|
+
hash: 'service-hash',
|
|
456
|
+
name: 'test-service'
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
connection.sendMessage(serviceMessage);
|
|
460
|
+
|
|
461
|
+
// Should be queued, not sent immediately (not authorized)
|
|
462
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
463
|
+
|
|
464
|
+
// Authorize and check replay
|
|
465
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
466
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should queue new-interaction messages', () => {
|
|
470
|
+
// Clear the initial init message
|
|
471
|
+
mockSocket.send.mockClear();
|
|
472
|
+
|
|
473
|
+
const httpRequestMessage = {
|
|
474
|
+
event: 'new-interaction' as const,
|
|
475
|
+
type: 'http-request' as const,
|
|
476
|
+
hash: 'request-hash',
|
|
477
|
+
from: 'service-hash',
|
|
478
|
+
to: 'http-service-hash'
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
connection.sendMessage(httpRequestMessage);
|
|
482
|
+
|
|
483
|
+
// Should be queued
|
|
484
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
485
|
+
|
|
486
|
+
// Authorize and check replay
|
|
487
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
488
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(httpRequestMessage));
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should queue observation messages', () => {
|
|
492
|
+
// Clear the initial init message
|
|
493
|
+
mockSocket.send.mockClear();
|
|
494
|
+
|
|
495
|
+
const observationMessage: ObservationMessage = {
|
|
496
|
+
event: 'observations',
|
|
497
|
+
seq: 1,
|
|
498
|
+
observations: [{
|
|
499
|
+
type: 'http-server',
|
|
500
|
+
subject: 'route-hash',
|
|
501
|
+
timestamp: [1700000000, 0],
|
|
502
|
+
path: '/test',
|
|
503
|
+
status: 200,
|
|
504
|
+
duration: [0, 50000000]
|
|
505
|
+
} as HttpServerObservation]
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
connection.sendMessage(observationMessage);
|
|
509
|
+
|
|
510
|
+
// Should be queued
|
|
511
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
512
|
+
|
|
513
|
+
// Authorize and check replay
|
|
514
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
515
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage));
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should not queue init messages in unacknowledged maps', () => {
|
|
519
|
+
// This is implicit - init messages are sent via sendRaw, not sendMessage
|
|
520
|
+
// and don't go through the queuing logic
|
|
521
|
+
|
|
522
|
+
const initMessage: InitMessage = {
|
|
523
|
+
event: 'init',
|
|
524
|
+
protocolVersion: 1,
|
|
525
|
+
token: 'test-token'
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// Manually test sendRaw behavior (private method testing)
|
|
529
|
+
mockSocket.readyState = WebSocket.OPEN;
|
|
530
|
+
(connection as any).sendRaw(initMessage);
|
|
531
|
+
|
|
532
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(initMessage));
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
});
|