@inferencesh/sdk 0.6.15 → 0.6.19

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 (85) hide show
  1. package/dist/agent/provider.d.ts +2 -1
  2. package/dist/api/apps.d.ts +4 -0
  3. package/dist/api/apps.js +6 -0
  4. package/dist/api/index.d.ts +3 -3
  5. package/dist/http/index.d.ts +3 -3
  6. package/dist/index.d.ts +9 -9
  7. package/dist/types.d.ts +30 -4
  8. package/dist/types.js +4 -0
  9. package/package.json +8 -5
  10. package/dist/agent/actions.test.d.ts +0 -1
  11. package/dist/agent/actions.test.js +0 -820
  12. package/dist/agent/api.test.d.ts +0 -1
  13. package/dist/agent/api.test.js +0 -246
  14. package/dist/agent/reducer.test.d.ts +0 -1
  15. package/dist/agent/reducer.test.js +0 -131
  16. package/dist/agent/types.test.d.ts +0 -1
  17. package/dist/agent/types.test.js +0 -75
  18. package/dist/api/agents.test.d.ts +0 -1
  19. package/dist/api/agents.test.js +0 -1014
  20. package/dist/api/api-keys.test.d.ts +0 -1
  21. package/dist/api/api-keys.test.js +0 -44
  22. package/dist/api/apps.test.d.ts +0 -1
  23. package/dist/api/apps.test.js +0 -135
  24. package/dist/api/chats.test.d.ts +0 -1
  25. package/dist/api/chats.test.js +0 -85
  26. package/dist/api/engines.test.d.ts +0 -1
  27. package/dist/api/engines.test.js +0 -133
  28. package/dist/api/files.test.d.ts +0 -1
  29. package/dist/api/files.test.js +0 -256
  30. package/dist/api/flow-runs.test.d.ts +0 -1
  31. package/dist/api/flow-runs.test.js +0 -97
  32. package/dist/api/flows.test.d.ts +0 -1
  33. package/dist/api/flows.test.js +0 -118
  34. package/dist/api/integrations.test.d.ts +0 -1
  35. package/dist/api/integrations.test.js +0 -109
  36. package/dist/api/knowledge.test.d.ts +0 -1
  37. package/dist/api/knowledge.test.js +0 -238
  38. package/dist/api/mcp-servers.test.d.ts +0 -1
  39. package/dist/api/mcp-servers.test.js +0 -98
  40. package/dist/api/projects.test.d.ts +0 -1
  41. package/dist/api/projects.test.js +0 -61
  42. package/dist/api/search.test.d.ts +0 -1
  43. package/dist/api/search.test.js +0 -50
  44. package/dist/api/secrets.test.d.ts +0 -1
  45. package/dist/api/secrets.test.js +0 -61
  46. package/dist/api/sessions.test.d.ts +0 -1
  47. package/dist/api/sessions.test.js +0 -61
  48. package/dist/api/tasks.test.d.ts +0 -1
  49. package/dist/api/tasks.test.js +0 -381
  50. package/dist/api/teams.test.d.ts +0 -1
  51. package/dist/api/teams.test.js +0 -139
  52. package/dist/client.test.d.ts +0 -1
  53. package/dist/client.test.js +0 -446
  54. package/dist/http/client.test.d.ts +0 -1
  55. package/dist/http/client.test.js +0 -435
  56. package/dist/http/errors.test.d.ts +0 -1
  57. package/dist/http/errors.test.js +0 -66
  58. package/dist/http/poll.test.d.ts +0 -1
  59. package/dist/http/poll.test.js +0 -146
  60. package/dist/http/streamable.test.d.ts +0 -4
  61. package/dist/http/streamable.test.js +0 -510
  62. package/dist/integration.test.d.ts +0 -9
  63. package/dist/integration.test.js +0 -107
  64. package/dist/proxy/express.test.d.ts +0 -1
  65. package/dist/proxy/express.test.js +0 -106
  66. package/dist/proxy/hono.test.d.ts +0 -1
  67. package/dist/proxy/hono.test.js +0 -90
  68. package/dist/proxy/index.test.d.ts +0 -1
  69. package/dist/proxy/index.test.js +0 -173
  70. package/dist/proxy/nextjs.test.d.ts +0 -1
  71. package/dist/proxy/nextjs.test.js +0 -125
  72. package/dist/proxy/remix.test.d.ts +0 -1
  73. package/dist/proxy/remix.test.js +0 -66
  74. package/dist/proxy/svelte.test.d.ts +0 -1
  75. package/dist/proxy/svelte.test.js +0 -69
  76. package/dist/sessions.integration.test.d.ts +0 -13
  77. package/dist/sessions.integration.test.js +0 -310
  78. package/dist/stream.test.d.ts +0 -1
  79. package/dist/stream.test.js +0 -363
  80. package/dist/streamable.integration.test.d.ts +0 -11
  81. package/dist/streamable.integration.test.js +0 -150
  82. package/dist/tool-builder.test.d.ts +0 -1
  83. package/dist/tool-builder.test.js +0 -407
  84. package/dist/utils.test.d.ts +0 -1
  85. package/dist/utils.test.js +0 -43
@@ -1,310 +0,0 @@
1
- /**
2
- * Sessions integration tests for @inferencesh/sdk
3
- *
4
- * These tests hit the real API and require INFERENCE_API_KEY to be set.
5
- * Run with: npm run test:integration
6
- *
7
- * Prerequisites:
8
- * - API and scheduler must be running
9
- * - Test app deployed: infsh/session-test
10
- *
11
- * @jest-environment node
12
- */
13
- import { Inference } from './index';
14
- import { TaskStatusCompleted } from './types';
15
- const API_KEY = process.env.INFERENCE_API_KEY;
16
- const BASE_URL = process.env.INFERENCE_BASE_URL || 'https://api.inference.sh';
17
- // Session test app with multi-function support
18
- const SESSION_TEST_APP = 'infsh/session-test';
19
- const describeIfApiKey = API_KEY ? describe : describe.skip;
20
- describeIfApiKey('Sessions Integration Tests', () => {
21
- let client;
22
- beforeAll(() => {
23
- client = new Inference({
24
- apiKey: API_KEY,
25
- baseUrl: BASE_URL,
26
- });
27
- });
28
- // ==========================================================================
29
- // Session Creation Tests
30
- // ==========================================================================
31
- describe('Session Creation', () => {
32
- it('should create a new session and return real session ID', async () => {
33
- const result = await client.run({
34
- app: SESSION_TEST_APP,
35
- function: 'set_value',
36
- input: { key: 'test_create', value: 'hello' },
37
- session: 'new',
38
- });
39
- expect(result.status).toBe(TaskStatusCompleted);
40
- expect(result.session_id).toBeDefined();
41
- expect(result.session_id).not.toBe('new');
42
- }, 60000);
43
- it('should return correct output data', async () => {
44
- const result = await client.run({
45
- app: SESSION_TEST_APP,
46
- function: 'set_value',
47
- input: { key: 'output_test', value: 'test_value' },
48
- session: 'new',
49
- });
50
- expect(result.status).toBe(TaskStatusCompleted);
51
- const output = result.output;
52
- expect(output.success).toBe(true);
53
- expect(output.key).toBe('output_test');
54
- expect(output.value).toBe('test_value');
55
- }, 60000);
56
- });
57
- // ==========================================================================
58
- // Session Continuity Tests
59
- // ==========================================================================
60
- describe('Session Continuity', () => {
61
- it('should persist state across calls in same session', async () => {
62
- // Create session with initial value
63
- const result1 = await client.run({
64
- app: SESSION_TEST_APP,
65
- function: 'set_value',
66
- input: { key: 'persist_key', value: 'persist_value' },
67
- session: 'new',
68
- });
69
- expect(result1.status).toBe(TaskStatusCompleted);
70
- const sessionId = result1.session_id;
71
- // Retrieve value from same session
72
- const result2 = await client.run({
73
- app: SESSION_TEST_APP,
74
- function: 'get_value',
75
- input: { key: 'persist_key' },
76
- session: sessionId,
77
- });
78
- expect(result2.status).toBe(TaskStatusCompleted);
79
- expect(result2.session_id).toBe(sessionId);
80
- const output = result2.output;
81
- expect(output.found).toBe(true);
82
- expect(output.value).toBe('persist_value');
83
- }, 120000);
84
- it('should accumulate multiple values in session', async () => {
85
- // Create session
86
- const result1 = await client.run({
87
- app: SESSION_TEST_APP,
88
- function: 'set_value',
89
- input: { key: 'key1', value: 'value1' },
90
- session: 'new',
91
- });
92
- expect(result1.status).toBe(TaskStatusCompleted);
93
- const sessionId = result1.session_id;
94
- // Add second value
95
- const result2 = await client.run({
96
- app: SESSION_TEST_APP,
97
- function: 'set_value',
98
- input: { key: 'key2', value: 'value2' },
99
- session: sessionId,
100
- });
101
- expect(result2.status).toBe(TaskStatusCompleted);
102
- // Get all state
103
- const result3 = await client.run({
104
- app: SESSION_TEST_APP,
105
- function: 'get_all',
106
- input: {},
107
- session: sessionId,
108
- });
109
- expect(result3.status).toBe(TaskStatusCompleted);
110
- const output = result3.output;
111
- expect(output.state.key1).toBe('value1');
112
- expect(output.state.key2).toBe('value2');
113
- }, 120000);
114
- });
115
- // ==========================================================================
116
- // Multi-Function Routing Tests
117
- // ==========================================================================
118
- describe('Multi-Function Routing', () => {
119
- it('should route to increment function correctly', async () => {
120
- const result = await client.run({
121
- app: SESSION_TEST_APP,
122
- function: 'increment',
123
- input: { key: 'counter', amount: 5 },
124
- session: 'new',
125
- });
126
- expect(result.status).toBe(TaskStatusCompleted);
127
- const output = result.output;
128
- expect(output.key).toBe('counter');
129
- expect(output.previous).toBe(0);
130
- expect(output.current).toBe(5);
131
- }, 60000);
132
- it('should handle different input schemas per function', async () => {
133
- // Create session
134
- const result1 = await client.run({
135
- app: SESSION_TEST_APP,
136
- function: 'set_value',
137
- input: { key: 'schema_test', value: 'v1' },
138
- session: 'new',
139
- });
140
- expect(result1.status).toBe(TaskStatusCompleted);
141
- const sessionId = result1.session_id;
142
- // get_value has different schema (no 'value' field)
143
- const result2 = await client.run({
144
- app: SESSION_TEST_APP,
145
- function: 'get_value',
146
- input: { key: 'schema_test' },
147
- session: sessionId,
148
- });
149
- expect(result2.status).toBe(TaskStatusCompleted);
150
- // increment has different schema (amount field)
151
- const result3 = await client.run({
152
- app: SESSION_TEST_APP,
153
- function: 'increment',
154
- input: { key: 'cnt', amount: 10 },
155
- session: sessionId,
156
- });
157
- expect(result3.status).toBe(TaskStatusCompleted);
158
- // get_all has empty schema
159
- const result4 = await client.run({
160
- app: SESSION_TEST_APP,
161
- function: 'get_all',
162
- input: {},
163
- session: sessionId,
164
- });
165
- expect(result4.status).toBe(TaskStatusCompleted);
166
- }, 120000);
167
- });
168
- // ==========================================================================
169
- // Session Isolation Tests
170
- // ==========================================================================
171
- describe('Session Isolation', () => {
172
- it('should create different sessions for different "new" requests', async () => {
173
- // Create first session
174
- const result1 = await client.run({
175
- app: SESSION_TEST_APP,
176
- function: 'set_value',
177
- input: { key: 'isolated', value: 'session1' },
178
- session: 'new',
179
- });
180
- expect(result1.status).toBe(TaskStatusCompleted);
181
- const session1 = result1.session_id;
182
- // Create second session
183
- const result2 = await client.run({
184
- app: SESSION_TEST_APP,
185
- function: 'set_value',
186
- input: { key: 'isolated', value: 'session2' },
187
- session: 'new',
188
- });
189
- expect(result2.status).toBe(TaskStatusCompleted);
190
- const session2 = result2.session_id;
191
- // Sessions should be different
192
- expect(session1).not.toBe(session2);
193
- }, 120000);
194
- });
195
- // ==========================================================================
196
- // Sessions API Tests
197
- // ==========================================================================
198
- describe('Sessions API', () => {
199
- it('should get session info', async () => {
200
- // Create a session
201
- const result = await client.run({
202
- app: SESSION_TEST_APP,
203
- function: 'set_value',
204
- input: { key: 'api_test', value: 'hello' },
205
- session: 'new',
206
- });
207
- expect(result.status).toBe(TaskStatusCompleted);
208
- const sessionId = result.session_id;
209
- // Get session info via API
210
- const sessionInfo = await client.sessions.get(sessionId);
211
- // API returns flattened structure (Go embedded structs)
212
- expect(sessionInfo.id).toBe(sessionId);
213
- expect(sessionInfo.status).toBe('active');
214
- expect(sessionInfo.call_count).toBeGreaterThanOrEqual(1);
215
- }, 60000);
216
- it('should list sessions', async () => {
217
- const sessions = await client.sessions.list();
218
- expect(Array.isArray(sessions)).toBe(true);
219
- // Should have at least one session from previous tests
220
- }, 30000);
221
- it('should keepalive a session', async () => {
222
- // Create a session
223
- const result = await client.run({
224
- app: SESSION_TEST_APP,
225
- function: 'set_value',
226
- input: { key: 'keepalive_test', value: 'hello' },
227
- session: 'new',
228
- });
229
- expect(result.status).toBe(TaskStatusCompleted);
230
- const sessionId = result.session_id;
231
- // Keepalive
232
- const updated = await client.sessions.keepalive(sessionId);
233
- // API returns flattened structure (Go embedded structs)
234
- expect(updated.id).toBe(sessionId);
235
- expect(updated.status).toBe('active');
236
- }, 60000);
237
- it('should end a session', async () => {
238
- // Create a session
239
- const result = await client.run({
240
- app: SESSION_TEST_APP,
241
- function: 'set_value',
242
- input: { key: 'end_test', value: 'goodbye' },
243
- session: 'new',
244
- });
245
- expect(result.status).toBe(TaskStatusCompleted);
246
- const sessionId = result.session_id;
247
- // End the session
248
- await client.sessions.end(sessionId);
249
- // Trying to use ended session should fail
250
- await expect(client.run({
251
- app: SESSION_TEST_APP,
252
- function: 'get_value',
253
- input: { key: 'end_test' },
254
- session: sessionId,
255
- })).rejects.toThrow();
256
- }, 60000);
257
- });
258
- // ==========================================================================
259
- // Error Cases
260
- // ==========================================================================
261
- describe('Error Cases', () => {
262
- it('should fail with invalid session ID', async () => {
263
- await expect(client.run({
264
- app: SESSION_TEST_APP,
265
- function: 'get_value',
266
- input: { key: 'foo' },
267
- session: 'sess_invalid_does_not_exist_12345',
268
- })).rejects.toThrow();
269
- }, 30000);
270
- });
271
- // ==========================================================================
272
- // Performance Tests
273
- // ==========================================================================
274
- describe('Performance', () => {
275
- it('should be faster on warm worker', async () => {
276
- // Create session (cold start)
277
- const result1 = await client.run({
278
- app: SESSION_TEST_APP,
279
- function: 'set_value',
280
- input: { key: 'perf_test', value: '1' },
281
- session: 'new',
282
- });
283
- expect(result1.status).toBe(TaskStatusCompleted);
284
- const sessionId = result1.session_id;
285
- // Second call should be fast (warm worker)
286
- const start = Date.now();
287
- const result2 = await client.run({
288
- app: SESSION_TEST_APP,
289
- function: 'set_value',
290
- input: { key: 'perf_test', value: '2' },
291
- session: sessionId,
292
- });
293
- const elapsed = Date.now() - start;
294
- expect(result2.status).toBe(TaskStatusCompleted);
295
- // Log for visibility
296
- console.log(`Warm worker call took: ${elapsed}ms`);
297
- // Should be under 2 seconds for warm worker
298
- expect(elapsed).toBeLessThan(2000);
299
- }, 120000);
300
- });
301
- });
302
- // Placeholder test that always runs
303
- describe('Sessions Integration Test Setup', () => {
304
- it('should have API key check', () => {
305
- if (!API_KEY) {
306
- console.log('⚠️ Skipping sessions integration tests - INFERENCE_API_KEY not set');
307
- }
308
- expect(true).toBe(true);
309
- });
310
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,363 +0,0 @@
1
- import { StreamManager } from './http/stream';
2
- describe('StreamManager', () => {
3
- let mockEventSource;
4
- beforeEach(() => {
5
- mockEventSource = {
6
- onmessage: null,
7
- onerror: null,
8
- close: jest.fn(),
9
- };
10
- });
11
- describe('constructor', () => {
12
- it('should create instance with default options', () => {
13
- const manager = new StreamManager({
14
- createEventSource: async () => mockEventSource,
15
- });
16
- expect(manager).toBeDefined();
17
- });
18
- });
19
- describe('connect', () => {
20
- it('should call createEventSource', async () => {
21
- const createEventSource = jest
22
- .fn()
23
- .mockResolvedValue(mockEventSource);
24
- const manager = new StreamManager({ createEventSource });
25
- await manager.connect();
26
- expect(createEventSource).toHaveBeenCalled();
27
- });
28
- it('should call onStart when connected', async () => {
29
- const onStart = jest.fn();
30
- const manager = new StreamManager({
31
- createEventSource: async () => mockEventSource,
32
- onStart,
33
- });
34
- await manager.connect();
35
- expect(onStart).toHaveBeenCalled();
36
- });
37
- it('should not call onStart when createEventSource returns null', async () => {
38
- const onStart = jest.fn();
39
- const manager = new StreamManager({
40
- createEventSource: async () => null,
41
- onStart,
42
- });
43
- await manager.connect();
44
- expect(onStart).not.toHaveBeenCalled();
45
- });
46
- it('should parse JSON messages and call onData', async () => {
47
- const onData = jest.fn();
48
- const manager = new StreamManager({
49
- createEventSource: async () => mockEventSource,
50
- onData,
51
- });
52
- await manager.connect();
53
- // Simulate receiving a message
54
- const testData = { status: 'running', id: 'task-123' };
55
- mockEventSource.onmessage?.({ data: JSON.stringify(testData) });
56
- expect(onData).toHaveBeenCalledWith(testData);
57
- });
58
- it('should extract data from partial data wrapper and call onData', async () => {
59
- const onData = jest.fn();
60
- const manager = new StreamManager({
61
- createEventSource: async () => mockEventSource,
62
- onData,
63
- });
64
- await manager.connect();
65
- // Simulate receiving a partial data wrapper from server
66
- const innerData = { status: 7, id: 'task-123', logs: ['log1'] };
67
- const partialWrapper = { data: innerData, fields: ['status', 'logs'] };
68
- mockEventSource.onmessage?.({ data: JSON.stringify(partialWrapper) });
69
- // onData should receive the extracted inner data, not the wrapper
70
- expect(onData).toHaveBeenCalledWith(innerData);
71
- });
72
- it('should call onPartialData with data and fields for partial updates', async () => {
73
- const onData = jest.fn();
74
- const onPartialData = jest.fn();
75
- const manager = new StreamManager({
76
- createEventSource: async () => mockEventSource,
77
- onData,
78
- onPartialData,
79
- });
80
- await manager.connect();
81
- // Simulate receiving a partial data wrapper
82
- const innerData = { status: 7, id: 'task-123' };
83
- const fields = ['status'];
84
- const partialWrapper = { data: innerData, fields };
85
- mockEventSource.onmessage?.({ data: JSON.stringify(partialWrapper) });
86
- // When onPartialData is provided, only it is called (not onData)
87
- expect(onPartialData).toHaveBeenCalledWith(innerData, fields);
88
- expect(onData).not.toHaveBeenCalled();
89
- });
90
- it('should not call onPartialData for non-partial data', async () => {
91
- const onData = jest.fn();
92
- const onPartialData = jest.fn();
93
- const manager = new StreamManager({
94
- createEventSource: async () => mockEventSource,
95
- onData,
96
- onPartialData,
97
- });
98
- await manager.connect();
99
- // Regular data without partial wrapper
100
- const regularData = { status: 9, id: 'task-123' };
101
- mockEventSource.onmessage?.({ data: JSON.stringify(regularData) });
102
- expect(onPartialData).not.toHaveBeenCalled();
103
- expect(onData).toHaveBeenCalledWith(regularData);
104
- });
105
- it('should call onError for invalid JSON', async () => {
106
- const onError = jest.fn();
107
- const manager = new StreamManager({
108
- createEventSource: async () => mockEventSource,
109
- onError,
110
- });
111
- await manager.connect();
112
- mockEventSource.onmessage?.({ data: 'invalid json' });
113
- expect(onError).toHaveBeenCalled();
114
- });
115
- });
116
- describe('stop', () => {
117
- it('should close the event source', async () => {
118
- const onStop = jest.fn();
119
- const manager = new StreamManager({
120
- createEventSource: async () => mockEventSource,
121
- onStop,
122
- });
123
- await manager.connect();
124
- manager.stop();
125
- expect(mockEventSource.close).toHaveBeenCalled();
126
- expect(onStop).toHaveBeenCalled();
127
- });
128
- it('should not process messages after stop', async () => {
129
- const onData = jest.fn();
130
- const manager = new StreamManager({
131
- createEventSource: async () => mockEventSource,
132
- onData,
133
- });
134
- await manager.connect();
135
- manager.stop();
136
- mockEventSource.onmessage?.({ data: '{"status":"running"}' });
137
- expect(onData).not.toHaveBeenCalled();
138
- });
139
- });
140
- describe('addEventListener', () => {
141
- it('should deliver typed SSE events to registered listeners', async () => {
142
- const typedListeners = {};
143
- const eventSource = {
144
- onmessage: null,
145
- onerror: null,
146
- close: jest.fn(),
147
- addEventListener: (eventName, handler) => {
148
- const listeners = typedListeners[eventName] || new Set();
149
- listeners.add(handler);
150
- typedListeners[eventName] = listeners;
151
- },
152
- };
153
- const manager = new StreamManager({
154
- createEventSource: async () => eventSource,
155
- });
156
- const onChat = jest.fn();
157
- manager.addEventListener('chats', onChat);
158
- await manager.connect();
159
- const payload = { id: 'chat-1', status: 'open' };
160
- typedListeners.chats?.forEach((handler) => handler({ data: JSON.stringify(payload) }));
161
- expect(onChat).toHaveBeenCalledWith(payload);
162
- });
163
- it('should unwrap partial wrappers for typed events', async () => {
164
- const typedListeners = {};
165
- const eventSource = {
166
- onmessage: null,
167
- onerror: null,
168
- close: jest.fn(),
169
- addEventListener: (eventName, handler) => {
170
- const listeners = typedListeners[eventName] || new Set();
171
- listeners.add(handler);
172
- typedListeners[eventName] = listeners;
173
- },
174
- };
175
- const manager = new StreamManager({
176
- createEventSource: async () => eventSource,
177
- });
178
- const onMessage = jest.fn();
179
- manager.addEventListener('chat_messages', onMessage);
180
- await manager.connect();
181
- const inner = { id: 'msg-1', order: 1 };
182
- typedListeners.chat_messages?.forEach((handler) => handler({
183
- data: JSON.stringify({ data: inner, fields: ['order'] }),
184
- }));
185
- expect(onMessage).toHaveBeenCalledWith(inner);
186
- });
187
- });
188
- describe('reconnection', () => {
189
- it('should attempt reconnection on error when autoReconnect is true', async () => {
190
- jest.useFakeTimers();
191
- const createEventSource = jest
192
- .fn()
193
- .mockResolvedValue(mockEventSource);
194
- const manager = new StreamManager({
195
- createEventSource,
196
- autoReconnect: true,
197
- reconnectDelayMs: 100,
198
- });
199
- await manager.connect();
200
- expect(createEventSource).toHaveBeenCalledTimes(1);
201
- // Simulate error
202
- mockEventSource.onerror?.({});
203
- // Fast-forward past reconnect delay
204
- jest.advanceTimersByTime(100);
205
- await Promise.resolve(); // Flush promises
206
- expect(createEventSource).toHaveBeenCalledTimes(2);
207
- jest.useRealTimers();
208
- });
209
- it('should not reconnect when autoReconnect is false', async () => {
210
- const createEventSource = jest
211
- .fn()
212
- .mockResolvedValue(mockEventSource);
213
- const manager = new StreamManager({
214
- createEventSource,
215
- autoReconnect: false,
216
- });
217
- await manager.connect();
218
- mockEventSource.onerror?.({});
219
- // Wait a bit
220
- await new Promise((r) => setTimeout(r, 50));
221
- expect(createEventSource).toHaveBeenCalledTimes(1);
222
- });
223
- it('should stop reconnecting after maxReconnects initial failures', async () => {
224
- jest.useFakeTimers();
225
- const onError = jest.fn();
226
- const createEventSource = jest
227
- .fn()
228
- .mockResolvedValue(mockEventSource);
229
- const manager = new StreamManager({
230
- createEventSource,
231
- autoReconnect: true,
232
- maxReconnects: 2,
233
- reconnectDelayMs: 100,
234
- onError,
235
- });
236
- await manager.connect();
237
- mockEventSource.onerror?.({});
238
- jest.advanceTimersByTime(100);
239
- await Promise.resolve();
240
- expect(createEventSource).toHaveBeenCalledTimes(2);
241
- mockEventSource.onerror?.({});
242
- jest.advanceTimersByTime(100);
243
- await Promise.resolve();
244
- expect(createEventSource).toHaveBeenCalledTimes(3);
245
- mockEventSource.onerror?.({});
246
- jest.advanceTimersByTime(100);
247
- await Promise.resolve();
248
- expect(createEventSource).toHaveBeenCalledTimes(3);
249
- jest.useRealTimers();
250
- });
251
- it('should call onError and stop when createEventSource throws', async () => {
252
- jest.useFakeTimers();
253
- const onError = jest.fn();
254
- const createEventSource = jest
255
- .fn()
256
- .mockRejectedValue(new Error('connection refused'));
257
- const manager = new StreamManager({
258
- createEventSource,
259
- autoReconnect: false,
260
- onError,
261
- });
262
- await manager.connect();
263
- expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'connection refused' }));
264
- jest.useRealTimers();
265
- });
266
- });
267
- describe('stopAfter and clearStopTimeout', () => {
268
- it('should stop after the configured delay', async () => {
269
- jest.useFakeTimers();
270
- const onStop = jest.fn();
271
- const manager = new StreamManager({
272
- createEventSource: async () => mockEventSource,
273
- onStop,
274
- });
275
- await manager.connect();
276
- manager.stopAfter(5000);
277
- jest.advanceTimersByTime(5000);
278
- expect(mockEventSource.close).toHaveBeenCalled();
279
- expect(onStop).toHaveBeenCalled();
280
- jest.useRealTimers();
281
- });
282
- it('should cancel a pending stop when clearStopTimeout is called', async () => {
283
- jest.useFakeTimers();
284
- const manager = new StreamManager({
285
- createEventSource: async () => mockEventSource,
286
- });
287
- await manager.connect();
288
- manager.stopAfter(5000);
289
- manager.clearStopTimeout();
290
- jest.advanceTimersByTime(5000);
291
- expect(mockEventSource.close).not.toHaveBeenCalled();
292
- jest.useRealTimers();
293
- });
294
- });
295
- describe('addEventListener lifecycle', () => {
296
- it('should register listeners on an existing connection when added after connect', async () => {
297
- const typedListeners = {};
298
- const eventSource = {
299
- onmessage: null,
300
- onerror: null,
301
- close: jest.fn(),
302
- addEventListener: (eventName, handler) => {
303
- const listeners = typedListeners[eventName] || new Set();
304
- listeners.add(handler);
305
- typedListeners[eventName] = listeners;
306
- },
307
- };
308
- const manager = new StreamManager({
309
- createEventSource: async () => eventSource,
310
- });
311
- await manager.connect();
312
- const onChat = jest.fn();
313
- manager.addEventListener('chats', onChat);
314
- const payload = { id: 'chat-2' };
315
- typedListeners.chats?.forEach((handler) => handler({ data: JSON.stringify(payload) }));
316
- expect(onChat).toHaveBeenCalledWith(payload);
317
- });
318
- it('should remove a listener when the cleanup function is called', async () => {
319
- const typedListeners = {};
320
- const eventSource = {
321
- onmessage: null,
322
- onerror: null,
323
- close: jest.fn(),
324
- addEventListener: (eventName, handler) => {
325
- const listeners = typedListeners[eventName] || new Set();
326
- listeners.add(handler);
327
- typedListeners[eventName] = listeners;
328
- },
329
- };
330
- const manager = new StreamManager({
331
- createEventSource: async () => eventSource,
332
- });
333
- const onChat = jest.fn();
334
- const unsubscribe = manager.addEventListener('chats', onChat);
335
- await manager.connect();
336
- unsubscribe();
337
- typedListeners.chats?.forEach((handler) => handler({ data: '{"id":"chat-3"}' }));
338
- expect(onChat).not.toHaveBeenCalled();
339
- });
340
- it('should call onError when typed event payload is invalid JSON', async () => {
341
- const typedListeners = {};
342
- const eventSource = {
343
- onmessage: null,
344
- onerror: null,
345
- close: jest.fn(),
346
- addEventListener: (eventName, handler) => {
347
- const listeners = typedListeners[eventName] || new Set();
348
- listeners.add(handler);
349
- typedListeners[eventName] = listeners;
350
- },
351
- };
352
- const onError = jest.fn();
353
- const manager = new StreamManager({
354
- createEventSource: async () => eventSource,
355
- onError,
356
- });
357
- manager.addEventListener('chats', jest.fn());
358
- await manager.connect();
359
- typedListeners.chats?.forEach((handler) => handler({ data: 'not-json' }));
360
- expect(onError).toHaveBeenCalled();
361
- });
362
- });
363
- });
@@ -1,11 +0,0 @@
1
- /**
2
- * Integration tests for streamable HTTP client
3
- *
4
- * Tests NDJSON streaming against the real API.
5
- * Run with: INFERENCE_API_KEY=xxx npm run test:integration
6
- *
7
- * Note: Format negotiation tests may be skipped if server doesn't support NDJSON yet.
8
- *
9
- * @jest-environment node
10
- */
11
- export {};