@chaoslabs/ai-sdk 0.0.3 → 0.0.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/README.md +46 -1
- package/dist/__tests__/http-streaming.test.d.ts +15 -0
- package/dist/__tests__/http-streaming.test.js +1401 -0
- package/dist/__tests__/stream.test.d.ts +1 -0
- package/dist/__tests__/stream.test.js +345 -0
- package/dist/__tests__/trivial.test.d.ts +1 -0
- package/dist/__tests__/trivial.test.js +6 -0
- package/dist/client.d.ts +21 -5
- package/dist/client.js +75 -29
- package/dist/http-streaming.d.ts +55 -0
- package/dist/http-streaming.js +359 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +3 -26
- package/dist/schemas.d.ts +97 -405
- package/dist/schemas.js +18 -47
- package/dist/stream.d.ts +32 -0
- package/dist/stream.js +127 -0
- package/dist/types.d.ts +11 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Streaming Tests (TDD - RED Phase)
|
|
3
|
+
*
|
|
4
|
+
* These tests verify the http-based streaming implementation that will replace fetch.
|
|
5
|
+
* Tests are written BEFORE the implementation (TDD RED phase).
|
|
6
|
+
*
|
|
7
|
+
* The implementation will use Node's native http/https modules for streaming
|
|
8
|
+
* instead of fetch to ensure proper incremental streaming behavior.
|
|
9
|
+
*
|
|
10
|
+
* KEY TESTS THAT MUST FAIL INITIALLY:
|
|
11
|
+
* - httpStreamRequest function (doesn't exist yet)
|
|
12
|
+
* - StreamingHttpClient class (doesn't exist yet)
|
|
13
|
+
* - useNativeHttp option (doesn't exist yet)
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from "bun:test";
|
|
16
|
+
import * as http from "node:http";
|
|
17
|
+
import { Chaos, WALLET_MODEL } from "../client";
|
|
18
|
+
import { ChaosError, ChaosTimeoutError } from "../types";
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Dynamic Import for TDD - Module Doesn't Exist Yet
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// These will be undefined until we implement the http streaming module
|
|
23
|
+
let httpStreamRequest;
|
|
24
|
+
let StreamingHttpClient;
|
|
25
|
+
// Try to import the module - it won't exist until implementation
|
|
26
|
+
try {
|
|
27
|
+
// @ts-expect-error - Module doesn't exist yet (TDD RED phase)
|
|
28
|
+
const httpStreaming = await import("../http-streaming");
|
|
29
|
+
httpStreamRequest = httpStreaming.httpStreamRequest;
|
|
30
|
+
StreamingHttpClient = httpStreaming.StreamingHttpClient;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Expected to fail until module is implemented
|
|
34
|
+
httpStreamRequest = undefined;
|
|
35
|
+
StreamingHttpClient = undefined;
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Test Utilities
|
|
39
|
+
// ============================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Creates a mock HTTP server that streams NDJSON responses.
|
|
42
|
+
*/
|
|
43
|
+
function createMockServer(handler) {
|
|
44
|
+
return http.createServer(handler);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates a valid stream message for testing.
|
|
48
|
+
*/
|
|
49
|
+
function createStreamMessage(id, type, content) {
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
type,
|
|
53
|
+
timestamp: Date.now(),
|
|
54
|
+
content,
|
|
55
|
+
context: {
|
|
56
|
+
sessionId: "test-session",
|
|
57
|
+
artifactId: "test-artifact",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates a valid NDJSON line from a stream message.
|
|
63
|
+
*/
|
|
64
|
+
function toNDJSON(msg) {
|
|
65
|
+
return JSON.stringify(msg) + "\n";
|
|
66
|
+
}
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Test Suite: Incremental Streaming
|
|
69
|
+
// ============================================================================
|
|
70
|
+
describe("HTTP Streaming - Incremental Event Delivery", () => {
|
|
71
|
+
let server;
|
|
72
|
+
let serverPort;
|
|
73
|
+
beforeAll(async () => {
|
|
74
|
+
server = createMockServer((req, res) => {
|
|
75
|
+
res.writeHead(200, {
|
|
76
|
+
"Content-Type": "application/x-ndjson",
|
|
77
|
+
"Transfer-Encoding": "chunked",
|
|
78
|
+
});
|
|
79
|
+
// Send messages with delays to simulate real streaming
|
|
80
|
+
const messages = [
|
|
81
|
+
createStreamMessage("1", "agent_status_change", { status: "processing" }),
|
|
82
|
+
createStreamMessage("2", "agent_message", {
|
|
83
|
+
messageId: "m1",
|
|
84
|
+
data: { message: "First message" },
|
|
85
|
+
}),
|
|
86
|
+
createStreamMessage("3", "agent_message", {
|
|
87
|
+
messageId: "m2",
|
|
88
|
+
data: { message: "Second message" },
|
|
89
|
+
}),
|
|
90
|
+
createStreamMessage("4", "report", {
|
|
91
|
+
id: "r1",
|
|
92
|
+
type: "table",
|
|
93
|
+
data: { blockType: "table", title: "Test", tableHeaders: [], tableRows: [] },
|
|
94
|
+
}),
|
|
95
|
+
createStreamMessage("5", "agent_status_change", { status: "done" }),
|
|
96
|
+
];
|
|
97
|
+
let index = 0;
|
|
98
|
+
const sendNext = () => {
|
|
99
|
+
if (index < messages.length) {
|
|
100
|
+
res.write(toNDJSON(messages[index]));
|
|
101
|
+
index++;
|
|
102
|
+
setTimeout(sendNext, 50); // 50ms delay between messages
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
res.end();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
sendNext();
|
|
109
|
+
});
|
|
110
|
+
await new Promise((resolve) => {
|
|
111
|
+
server.listen(0, () => {
|
|
112
|
+
const addr = server.address();
|
|
113
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
114
|
+
resolve();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
afterAll(() => {
|
|
119
|
+
server.close();
|
|
120
|
+
});
|
|
121
|
+
it("fires onStreamEvent callback for each message as data arrives", async () => {
|
|
122
|
+
const chaos = new Chaos({
|
|
123
|
+
apiKey: "test-api-key",
|
|
124
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
125
|
+
timeout: 10000,
|
|
126
|
+
});
|
|
127
|
+
const receivedEvents = [];
|
|
128
|
+
const receivedTimestamps = [];
|
|
129
|
+
await chaos.chat.responses.create({
|
|
130
|
+
model: WALLET_MODEL,
|
|
131
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
132
|
+
metadata: {
|
|
133
|
+
user_id: "test-user",
|
|
134
|
+
session_id: "test-session",
|
|
135
|
+
wallet_id: "0x123",
|
|
136
|
+
},
|
|
137
|
+
onStreamEvent: (event) => {
|
|
138
|
+
receivedEvents.push(event);
|
|
139
|
+
receivedTimestamps.push(Date.now());
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
// Should receive all 5 messages
|
|
143
|
+
expect(receivedEvents.length).toBe(5);
|
|
144
|
+
// Events should arrive incrementally (with time gaps between them)
|
|
145
|
+
// If they arrived all at once, timestamps would be nearly identical
|
|
146
|
+
for (let i = 1; i < receivedTimestamps.length; i++) {
|
|
147
|
+
const timeDiff = receivedTimestamps[i] - receivedTimestamps[i - 1];
|
|
148
|
+
// Should have at least 20ms gap (server sends with 50ms delay)
|
|
149
|
+
expect(timeDiff).toBeGreaterThanOrEqual(20);
|
|
150
|
+
}
|
|
151
|
+
// Verify message types in order
|
|
152
|
+
expect(receivedEvents[0].type).toBe("agent_status_change");
|
|
153
|
+
expect(receivedEvents[1].type).toBe("agent_message");
|
|
154
|
+
expect(receivedEvents[2].type).toBe("agent_message");
|
|
155
|
+
expect(receivedEvents[3].type).toBe("report");
|
|
156
|
+
expect(receivedEvents[4].type).toBe("agent_status_change");
|
|
157
|
+
});
|
|
158
|
+
it("does not buffer all events before calling callback", async () => {
|
|
159
|
+
const chaos = new Chaos({
|
|
160
|
+
apiKey: "test-api-key",
|
|
161
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
162
|
+
timeout: 10000,
|
|
163
|
+
});
|
|
164
|
+
let firstEventTime = null;
|
|
165
|
+
let lastEventTime = null;
|
|
166
|
+
let eventCount = 0;
|
|
167
|
+
const startTime = Date.now();
|
|
168
|
+
await chaos.chat.responses.create({
|
|
169
|
+
model: WALLET_MODEL,
|
|
170
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
171
|
+
metadata: {
|
|
172
|
+
user_id: "test-user",
|
|
173
|
+
session_id: "test-session",
|
|
174
|
+
wallet_id: "0x123",
|
|
175
|
+
},
|
|
176
|
+
onStreamEvent: () => {
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
if (firstEventTime === null) {
|
|
179
|
+
firstEventTime = now;
|
|
180
|
+
}
|
|
181
|
+
lastEventTime = now;
|
|
182
|
+
eventCount++;
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
// Total time should be close to 5 messages * 50ms = 250ms
|
|
186
|
+
const totalTime = lastEventTime - firstEventTime;
|
|
187
|
+
// If events were buffered, totalTime would be near 0
|
|
188
|
+
// With streaming, it should be at least 150ms (accounting for some variance)
|
|
189
|
+
expect(totalTime).toBeGreaterThanOrEqual(150);
|
|
190
|
+
expect(eventCount).toBe(5);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Test Suite: Chunked Data Handling
|
|
195
|
+
// ============================================================================
|
|
196
|
+
describe("HTTP Streaming - Chunked Data Handling", () => {
|
|
197
|
+
let server;
|
|
198
|
+
let serverPort;
|
|
199
|
+
afterEach(() => {
|
|
200
|
+
if (server) {
|
|
201
|
+
server.close();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
it("handles partial JSON lines split across multiple data events", async () => {
|
|
205
|
+
// Create a message that will be split across chunks
|
|
206
|
+
const fullMessage = createStreamMessage("1", "agent_message", {
|
|
207
|
+
messageId: "m1",
|
|
208
|
+
data: { message: "This is a complete message that was split" },
|
|
209
|
+
});
|
|
210
|
+
const fullLine = JSON.stringify(fullMessage);
|
|
211
|
+
// Split the line at various points
|
|
212
|
+
const chunk1 = fullLine.substring(0, 20);
|
|
213
|
+
const chunk2 = fullLine.substring(20, 50);
|
|
214
|
+
const chunk3 = fullLine.substring(50) + "\n";
|
|
215
|
+
server = createMockServer((req, res) => {
|
|
216
|
+
res.writeHead(200, {
|
|
217
|
+
"Content-Type": "application/x-ndjson",
|
|
218
|
+
"Transfer-Encoding": "chunked",
|
|
219
|
+
});
|
|
220
|
+
// Send chunks with delays
|
|
221
|
+
res.write(chunk1);
|
|
222
|
+
setTimeout(() => res.write(chunk2), 20);
|
|
223
|
+
setTimeout(() => res.write(chunk3), 40);
|
|
224
|
+
setTimeout(() => res.end(), 60);
|
|
225
|
+
});
|
|
226
|
+
await new Promise((resolve) => {
|
|
227
|
+
server.listen(0, () => {
|
|
228
|
+
const addr = server.address();
|
|
229
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
230
|
+
resolve();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
const chaos = new Chaos({
|
|
234
|
+
apiKey: "test-api-key",
|
|
235
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
236
|
+
timeout: 10000,
|
|
237
|
+
});
|
|
238
|
+
const receivedEvents = [];
|
|
239
|
+
await chaos.chat.responses.create({
|
|
240
|
+
model: WALLET_MODEL,
|
|
241
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
242
|
+
metadata: {
|
|
243
|
+
user_id: "test-user",
|
|
244
|
+
session_id: "test-session",
|
|
245
|
+
wallet_id: "0x123",
|
|
246
|
+
},
|
|
247
|
+
onStreamEvent: (event) => {
|
|
248
|
+
receivedEvents.push(event);
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
// Should correctly reassemble the split message
|
|
252
|
+
expect(receivedEvents.length).toBe(1);
|
|
253
|
+
expect(receivedEvents[0].id).toBe("1");
|
|
254
|
+
expect(receivedEvents[0].type).toBe("agent_message");
|
|
255
|
+
const content = receivedEvents[0].content;
|
|
256
|
+
expect(content.data.message).toBe("This is a complete message that was split");
|
|
257
|
+
});
|
|
258
|
+
it("handles multiple complete messages in a single chunk", async () => {
|
|
259
|
+
const msg1 = createStreamMessage("1", "agent_status_change", { status: "processing" });
|
|
260
|
+
const msg2 = createStreamMessage("2", "agent_message", {
|
|
261
|
+
messageId: "m1",
|
|
262
|
+
data: { message: "Hello" },
|
|
263
|
+
});
|
|
264
|
+
const msg3 = createStreamMessage("3", "agent_status_change", { status: "done" });
|
|
265
|
+
// All messages in one chunk
|
|
266
|
+
const allMessages = toNDJSON(msg1) + toNDJSON(msg2) + toNDJSON(msg3);
|
|
267
|
+
server = createMockServer((req, res) => {
|
|
268
|
+
res.writeHead(200, {
|
|
269
|
+
"Content-Type": "application/x-ndjson",
|
|
270
|
+
"Transfer-Encoding": "chunked",
|
|
271
|
+
});
|
|
272
|
+
res.write(allMessages);
|
|
273
|
+
res.end();
|
|
274
|
+
});
|
|
275
|
+
await new Promise((resolve) => {
|
|
276
|
+
server.listen(0, () => {
|
|
277
|
+
const addr = server.address();
|
|
278
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
279
|
+
resolve();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
const chaos = new Chaos({
|
|
283
|
+
apiKey: "test-api-key",
|
|
284
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
285
|
+
timeout: 10000,
|
|
286
|
+
});
|
|
287
|
+
const receivedEvents = [];
|
|
288
|
+
await chaos.chat.responses.create({
|
|
289
|
+
model: WALLET_MODEL,
|
|
290
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
291
|
+
metadata: {
|
|
292
|
+
user_id: "test-user",
|
|
293
|
+
session_id: "test-session",
|
|
294
|
+
wallet_id: "0x123",
|
|
295
|
+
},
|
|
296
|
+
onStreamEvent: (event) => {
|
|
297
|
+
receivedEvents.push(event);
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
expect(receivedEvents.length).toBe(3);
|
|
301
|
+
expect(receivedEvents[0].id).toBe("1");
|
|
302
|
+
expect(receivedEvents[1].id).toBe("2");
|
|
303
|
+
expect(receivedEvents[2].id).toBe("3");
|
|
304
|
+
});
|
|
305
|
+
it("handles messages split mid-UTF8 character", async () => {
|
|
306
|
+
// Message with unicode characters that might be split mid-character
|
|
307
|
+
const fullMessage = createStreamMessage("1", "agent_message", {
|
|
308
|
+
messageId: "m1",
|
|
309
|
+
data: { message: "Hello \u{1F4B0} and \u{1F680} tokens!" },
|
|
310
|
+
});
|
|
311
|
+
const fullLine = JSON.stringify(fullMessage) + "\n";
|
|
312
|
+
// Convert to buffer and split at byte level (might split UTF-8 sequences)
|
|
313
|
+
const buffer = Buffer.from(fullLine, "utf8");
|
|
314
|
+
const midPoint = Math.floor(buffer.length / 2);
|
|
315
|
+
const chunk1 = buffer.subarray(0, midPoint);
|
|
316
|
+
const chunk2 = buffer.subarray(midPoint);
|
|
317
|
+
server = createMockServer((req, res) => {
|
|
318
|
+
res.writeHead(200, {
|
|
319
|
+
"Content-Type": "application/x-ndjson",
|
|
320
|
+
"Transfer-Encoding": "chunked",
|
|
321
|
+
});
|
|
322
|
+
res.write(chunk1);
|
|
323
|
+
setTimeout(() => {
|
|
324
|
+
res.write(chunk2);
|
|
325
|
+
res.end();
|
|
326
|
+
}, 30);
|
|
327
|
+
});
|
|
328
|
+
await new Promise((resolve) => {
|
|
329
|
+
server.listen(0, () => {
|
|
330
|
+
const addr = server.address();
|
|
331
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
332
|
+
resolve();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
const chaos = new Chaos({
|
|
336
|
+
apiKey: "test-api-key",
|
|
337
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
338
|
+
timeout: 10000,
|
|
339
|
+
});
|
|
340
|
+
const receivedEvents = [];
|
|
341
|
+
await chaos.chat.responses.create({
|
|
342
|
+
model: WALLET_MODEL,
|
|
343
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
344
|
+
metadata: {
|
|
345
|
+
user_id: "test-user",
|
|
346
|
+
session_id: "test-session",
|
|
347
|
+
wallet_id: "0x123",
|
|
348
|
+
},
|
|
349
|
+
onStreamEvent: (event) => {
|
|
350
|
+
receivedEvents.push(event);
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
expect(receivedEvents.length).toBe(1);
|
|
354
|
+
const content = receivedEvents[0].content;
|
|
355
|
+
expect(content.data.message).toContain("\u{1F4B0}");
|
|
356
|
+
expect(content.data.message).toContain("\u{1F680}");
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// Test Suite: Timeout Handling
|
|
361
|
+
// ============================================================================
|
|
362
|
+
describe("HTTP Streaming - Timeout Handling", () => {
|
|
363
|
+
let server;
|
|
364
|
+
let serverPort;
|
|
365
|
+
afterEach(() => {
|
|
366
|
+
if (server) {
|
|
367
|
+
server.close();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
it("throws ChaosTimeoutError when request exceeds timeout", async () => {
|
|
371
|
+
server = createMockServer((req, res) => {
|
|
372
|
+
res.writeHead(200, {
|
|
373
|
+
"Content-Type": "application/x-ndjson",
|
|
374
|
+
"Transfer-Encoding": "chunked",
|
|
375
|
+
});
|
|
376
|
+
// Send initial data then hang
|
|
377
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "processing" });
|
|
378
|
+
res.write(toNDJSON(msg));
|
|
379
|
+
// Never send more data - let it timeout
|
|
380
|
+
});
|
|
381
|
+
await new Promise((resolve) => {
|
|
382
|
+
server.listen(0, () => {
|
|
383
|
+
const addr = server.address();
|
|
384
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
385
|
+
resolve();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
const chaos = new Chaos({
|
|
389
|
+
apiKey: "test-api-key",
|
|
390
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
391
|
+
timeout: 200, // Very short timeout
|
|
392
|
+
});
|
|
393
|
+
await expect(chaos.chat.responses.create({
|
|
394
|
+
model: WALLET_MODEL,
|
|
395
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
396
|
+
metadata: {
|
|
397
|
+
user_id: "test-user",
|
|
398
|
+
session_id: "test-session",
|
|
399
|
+
wallet_id: "0x123",
|
|
400
|
+
},
|
|
401
|
+
})).rejects.toThrow(ChaosTimeoutError);
|
|
402
|
+
});
|
|
403
|
+
it("includes timeout duration in error message", async () => {
|
|
404
|
+
server = createMockServer((req, res) => {
|
|
405
|
+
res.writeHead(200, {
|
|
406
|
+
"Content-Type": "application/x-ndjson",
|
|
407
|
+
"Transfer-Encoding": "chunked",
|
|
408
|
+
});
|
|
409
|
+
// Hang indefinitely
|
|
410
|
+
});
|
|
411
|
+
await new Promise((resolve) => {
|
|
412
|
+
server.listen(0, () => {
|
|
413
|
+
const addr = server.address();
|
|
414
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
415
|
+
resolve();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
const chaos = new Chaos({
|
|
419
|
+
apiKey: "test-api-key",
|
|
420
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
421
|
+
timeout: 300,
|
|
422
|
+
});
|
|
423
|
+
try {
|
|
424
|
+
await chaos.chat.responses.create({
|
|
425
|
+
model: WALLET_MODEL,
|
|
426
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
427
|
+
metadata: {
|
|
428
|
+
user_id: "test-user",
|
|
429
|
+
session_id: "test-session",
|
|
430
|
+
wallet_id: "0x123",
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
expect.fail("Should have thrown");
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
expect(error).toBeInstanceOf(ChaosTimeoutError);
|
|
437
|
+
expect(error.message).toContain("300");
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
it("does not timeout when data arrives within timeout window", async () => {
|
|
441
|
+
server = createMockServer((req, res) => {
|
|
442
|
+
res.writeHead(200, {
|
|
443
|
+
"Content-Type": "application/x-ndjson",
|
|
444
|
+
"Transfer-Encoding": "chunked",
|
|
445
|
+
});
|
|
446
|
+
// Send messages quickly
|
|
447
|
+
const msg1 = createStreamMessage("1", "agent_status_change", { status: "processing" });
|
|
448
|
+
const msg2 = createStreamMessage("2", "agent_status_change", { status: "done" });
|
|
449
|
+
res.write(toNDJSON(msg1));
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
res.write(toNDJSON(msg2));
|
|
452
|
+
res.end();
|
|
453
|
+
}, 50);
|
|
454
|
+
});
|
|
455
|
+
await new Promise((resolve) => {
|
|
456
|
+
server.listen(0, () => {
|
|
457
|
+
const addr = server.address();
|
|
458
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
459
|
+
resolve();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
const chaos = new Chaos({
|
|
463
|
+
apiKey: "test-api-key",
|
|
464
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
465
|
+
timeout: 5000,
|
|
466
|
+
});
|
|
467
|
+
const receivedEvents = [];
|
|
468
|
+
// Should complete without throwing
|
|
469
|
+
await chaos.chat.responses.create({
|
|
470
|
+
model: WALLET_MODEL,
|
|
471
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
472
|
+
metadata: {
|
|
473
|
+
user_id: "test-user",
|
|
474
|
+
session_id: "test-session",
|
|
475
|
+
wallet_id: "0x123",
|
|
476
|
+
},
|
|
477
|
+
onStreamEvent: (event) => {
|
|
478
|
+
receivedEvents.push(event);
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
expect(receivedEvents.length).toBe(2);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
// ============================================================================
|
|
485
|
+
// Test Suite: HTTP Error Handling
|
|
486
|
+
// ============================================================================
|
|
487
|
+
describe("HTTP Streaming - HTTP Error Handling", () => {
|
|
488
|
+
let server;
|
|
489
|
+
let serverPort;
|
|
490
|
+
afterEach(() => {
|
|
491
|
+
if (server) {
|
|
492
|
+
server.close();
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
it("throws ChaosError with status code for 4xx responses", async () => {
|
|
496
|
+
server = createMockServer((req, res) => {
|
|
497
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
498
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
499
|
+
});
|
|
500
|
+
await new Promise((resolve) => {
|
|
501
|
+
server.listen(0, () => {
|
|
502
|
+
const addr = server.address();
|
|
503
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
504
|
+
resolve();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
const chaos = new Chaos({
|
|
508
|
+
apiKey: "invalid-key",
|
|
509
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
510
|
+
});
|
|
511
|
+
try {
|
|
512
|
+
await chaos.chat.responses.create({
|
|
513
|
+
model: WALLET_MODEL,
|
|
514
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
515
|
+
metadata: {
|
|
516
|
+
user_id: "test-user",
|
|
517
|
+
session_id: "test-session",
|
|
518
|
+
wallet_id: "0x123",
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
expect.fail("Should have thrown");
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
expect(error).toBeInstanceOf(ChaosError);
|
|
525
|
+
expect(error.status).toBe(401);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
it("throws ChaosError with status code for 5xx responses", async () => {
|
|
529
|
+
server = createMockServer((req, res) => {
|
|
530
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
531
|
+
res.end(JSON.stringify({ error: "Service Unavailable" }));
|
|
532
|
+
});
|
|
533
|
+
await new Promise((resolve) => {
|
|
534
|
+
server.listen(0, () => {
|
|
535
|
+
const addr = server.address();
|
|
536
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
537
|
+
resolve();
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
const chaos = new Chaos({
|
|
541
|
+
apiKey: "test-api-key",
|
|
542
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
543
|
+
});
|
|
544
|
+
try {
|
|
545
|
+
await chaos.chat.responses.create({
|
|
546
|
+
model: WALLET_MODEL,
|
|
547
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
548
|
+
metadata: {
|
|
549
|
+
user_id: "test-user",
|
|
550
|
+
session_id: "test-session",
|
|
551
|
+
wallet_id: "0x123",
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
expect.fail("Should have thrown");
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
expect(error).toBeInstanceOf(ChaosError);
|
|
558
|
+
expect(error.status).toBe(503);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
it("includes HTTP status text in error message", async () => {
|
|
562
|
+
server = createMockServer((req, res) => {
|
|
563
|
+
res.writeHead(429, "Too Many Requests", { "Content-Type": "application/json" });
|
|
564
|
+
res.end(JSON.stringify({ error: "Rate limited" }));
|
|
565
|
+
});
|
|
566
|
+
await new Promise((resolve) => {
|
|
567
|
+
server.listen(0, () => {
|
|
568
|
+
const addr = server.address();
|
|
569
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
570
|
+
resolve();
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
const chaos = new Chaos({
|
|
574
|
+
apiKey: "test-api-key",
|
|
575
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
576
|
+
});
|
|
577
|
+
try {
|
|
578
|
+
await chaos.chat.responses.create({
|
|
579
|
+
model: WALLET_MODEL,
|
|
580
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
581
|
+
metadata: {
|
|
582
|
+
user_id: "test-user",
|
|
583
|
+
session_id: "test-session",
|
|
584
|
+
wallet_id: "0x123",
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
expect.fail("Should have thrown");
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
expect(error).toBeInstanceOf(ChaosError);
|
|
591
|
+
expect(error.message).toContain("429");
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
// ============================================================================
|
|
596
|
+
// Test Suite: Connection Error Handling
|
|
597
|
+
// ============================================================================
|
|
598
|
+
describe("HTTP Streaming - Connection Error Handling", () => {
|
|
599
|
+
it("throws ChaosError when server is unreachable", async () => {
|
|
600
|
+
const chaos = new Chaos({
|
|
601
|
+
apiKey: "test-api-key",
|
|
602
|
+
baseUrl: "http://localhost:59999", // Port that is not listening
|
|
603
|
+
timeout: 5000,
|
|
604
|
+
});
|
|
605
|
+
await expect(chaos.chat.responses.create({
|
|
606
|
+
model: WALLET_MODEL,
|
|
607
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
608
|
+
metadata: {
|
|
609
|
+
user_id: "test-user",
|
|
610
|
+
session_id: "test-session",
|
|
611
|
+
wallet_id: "0x123",
|
|
612
|
+
},
|
|
613
|
+
})).rejects.toThrow(ChaosError);
|
|
614
|
+
});
|
|
615
|
+
it("handles connection reset mid-stream", async () => {
|
|
616
|
+
const server = createMockServer((req, res) => {
|
|
617
|
+
res.writeHead(200, {
|
|
618
|
+
"Content-Type": "application/x-ndjson",
|
|
619
|
+
"Transfer-Encoding": "chunked",
|
|
620
|
+
});
|
|
621
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "processing" });
|
|
622
|
+
res.write(toNDJSON(msg));
|
|
623
|
+
// Destroy the socket to simulate connection reset
|
|
624
|
+
setTimeout(() => {
|
|
625
|
+
req.socket?.destroy();
|
|
626
|
+
}, 50);
|
|
627
|
+
});
|
|
628
|
+
await new Promise((resolve) => {
|
|
629
|
+
server.listen(0, () => resolve());
|
|
630
|
+
});
|
|
631
|
+
const addr = server.address();
|
|
632
|
+
const serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
633
|
+
const chaos = new Chaos({
|
|
634
|
+
apiKey: "test-api-key",
|
|
635
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
636
|
+
timeout: 5000,
|
|
637
|
+
});
|
|
638
|
+
try {
|
|
639
|
+
await chaos.chat.responses.create({
|
|
640
|
+
model: WALLET_MODEL,
|
|
641
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
642
|
+
metadata: {
|
|
643
|
+
user_id: "test-user",
|
|
644
|
+
session_id: "test-session",
|
|
645
|
+
wallet_id: "0x123",
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
expect.fail("Should have thrown");
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
expect(error).toBeInstanceOf(ChaosError);
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
server.close();
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
it("handles DNS resolution failure", async () => {
|
|
658
|
+
const chaos = new Chaos({
|
|
659
|
+
apiKey: "test-api-key",
|
|
660
|
+
baseUrl: "http://this-domain-does-not-exist-12345.invalid",
|
|
661
|
+
timeout: 5000,
|
|
662
|
+
});
|
|
663
|
+
await expect(chaos.chat.responses.create({
|
|
664
|
+
model: WALLET_MODEL,
|
|
665
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
666
|
+
metadata: {
|
|
667
|
+
user_id: "test-user",
|
|
668
|
+
session_id: "test-session",
|
|
669
|
+
wallet_id: "0x123",
|
|
670
|
+
},
|
|
671
|
+
})).rejects.toThrow(ChaosError);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
// ============================================================================
|
|
675
|
+
// Test Suite: Protocol Support
|
|
676
|
+
// ============================================================================
|
|
677
|
+
describe("HTTP Streaming - Protocol Support", () => {
|
|
678
|
+
let httpServer;
|
|
679
|
+
let httpPort;
|
|
680
|
+
afterEach(() => {
|
|
681
|
+
if (httpServer) {
|
|
682
|
+
httpServer.close();
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
it("works with http:// URLs", async () => {
|
|
686
|
+
httpServer = createMockServer((req, res) => {
|
|
687
|
+
res.writeHead(200, {
|
|
688
|
+
"Content-Type": "application/x-ndjson",
|
|
689
|
+
"Transfer-Encoding": "chunked",
|
|
690
|
+
});
|
|
691
|
+
const msg1 = createStreamMessage("1", "agent_status_change", { status: "processing" });
|
|
692
|
+
const msg2 = createStreamMessage("2", "agent_status_change", { status: "done" });
|
|
693
|
+
res.write(toNDJSON(msg1));
|
|
694
|
+
res.write(toNDJSON(msg2));
|
|
695
|
+
res.end();
|
|
696
|
+
});
|
|
697
|
+
await new Promise((resolve) => {
|
|
698
|
+
httpServer.listen(0, () => {
|
|
699
|
+
const addr = httpServer.address();
|
|
700
|
+
httpPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
701
|
+
resolve();
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
const chaos = new Chaos({
|
|
705
|
+
apiKey: "test-api-key",
|
|
706
|
+
baseUrl: `http://localhost:${httpPort}`,
|
|
707
|
+
});
|
|
708
|
+
const receivedEvents = [];
|
|
709
|
+
await chaos.chat.responses.create({
|
|
710
|
+
model: WALLET_MODEL,
|
|
711
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
712
|
+
metadata: {
|
|
713
|
+
user_id: "test-user",
|
|
714
|
+
session_id: "test-session",
|
|
715
|
+
wallet_id: "0x123",
|
|
716
|
+
},
|
|
717
|
+
onStreamEvent: (event) => {
|
|
718
|
+
receivedEvents.push(event);
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
expect(receivedEvents.length).toBe(2);
|
|
722
|
+
});
|
|
723
|
+
it("uses correct protocol module based on URL scheme", async () => {
|
|
724
|
+
// This test verifies that http:// URLs use the http module
|
|
725
|
+
// and https:// URLs would use the https module
|
|
726
|
+
// Since we can't easily test https without certificates in unit tests,
|
|
727
|
+
// we verify http works and document that https should work identically
|
|
728
|
+
httpServer = createMockServer((req, res) => {
|
|
729
|
+
// Verify the request came through correctly
|
|
730
|
+
expect(req.method).toBe("POST");
|
|
731
|
+
expect(req.headers["content-type"]).toBe("application/json");
|
|
732
|
+
expect(req.headers["authorization"]).toContain("Bearer");
|
|
733
|
+
res.writeHead(200, {
|
|
734
|
+
"Content-Type": "application/x-ndjson",
|
|
735
|
+
});
|
|
736
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "done" });
|
|
737
|
+
res.write(toNDJSON(msg));
|
|
738
|
+
res.end();
|
|
739
|
+
});
|
|
740
|
+
await new Promise((resolve) => {
|
|
741
|
+
httpServer.listen(0, () => {
|
|
742
|
+
const addr = httpServer.address();
|
|
743
|
+
httpPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
744
|
+
resolve();
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
const chaos = new Chaos({
|
|
748
|
+
apiKey: "test-api-key",
|
|
749
|
+
baseUrl: `http://localhost:${httpPort}`,
|
|
750
|
+
});
|
|
751
|
+
// Should complete without error
|
|
752
|
+
const response = await chaos.chat.responses.create({
|
|
753
|
+
model: WALLET_MODEL,
|
|
754
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
755
|
+
metadata: {
|
|
756
|
+
user_id: "test-user",
|
|
757
|
+
session_id: "test-session",
|
|
758
|
+
wallet_id: "0x123",
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
expect(response).toBeDefined();
|
|
762
|
+
expect(response.status).toBe("completed");
|
|
763
|
+
});
|
|
764
|
+
it("handles URL with port in baseUrl", async () => {
|
|
765
|
+
httpServer = createMockServer((req, res) => {
|
|
766
|
+
res.writeHead(200, {
|
|
767
|
+
"Content-Type": "application/x-ndjson",
|
|
768
|
+
});
|
|
769
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "done" });
|
|
770
|
+
res.write(toNDJSON(msg));
|
|
771
|
+
res.end();
|
|
772
|
+
});
|
|
773
|
+
await new Promise((resolve) => {
|
|
774
|
+
httpServer.listen(0, () => {
|
|
775
|
+
const addr = httpServer.address();
|
|
776
|
+
httpPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
777
|
+
resolve();
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
const chaos = new Chaos({
|
|
781
|
+
apiKey: "test-api-key",
|
|
782
|
+
baseUrl: `http://localhost:${httpPort}`,
|
|
783
|
+
});
|
|
784
|
+
const response = await chaos.chat.responses.create({
|
|
785
|
+
model: WALLET_MODEL,
|
|
786
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
787
|
+
metadata: {
|
|
788
|
+
user_id: "test-user",
|
|
789
|
+
session_id: "test-session",
|
|
790
|
+
wallet_id: "0x123",
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
expect(response).toBeDefined();
|
|
794
|
+
});
|
|
795
|
+
it("handles URL with path in baseUrl", async () => {
|
|
796
|
+
httpServer = createMockServer((req, res) => {
|
|
797
|
+
// The path should include the base path plus the API endpoint
|
|
798
|
+
expect(req.url).toContain("/api");
|
|
799
|
+
res.writeHead(200, {
|
|
800
|
+
"Content-Type": "application/x-ndjson",
|
|
801
|
+
});
|
|
802
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "done" });
|
|
803
|
+
res.write(toNDJSON(msg));
|
|
804
|
+
res.end();
|
|
805
|
+
});
|
|
806
|
+
await new Promise((resolve) => {
|
|
807
|
+
httpServer.listen(0, () => {
|
|
808
|
+
const addr = httpServer.address();
|
|
809
|
+
httpPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
810
|
+
resolve();
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
const chaos = new Chaos({
|
|
814
|
+
apiKey: "test-api-key",
|
|
815
|
+
baseUrl: `http://localhost:${httpPort}/api`,
|
|
816
|
+
});
|
|
817
|
+
const response = await chaos.chat.responses.create({
|
|
818
|
+
model: WALLET_MODEL,
|
|
819
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
820
|
+
metadata: {
|
|
821
|
+
user_id: "test-user",
|
|
822
|
+
session_id: "test-session",
|
|
823
|
+
wallet_id: "0x123",
|
|
824
|
+
},
|
|
825
|
+
});
|
|
826
|
+
expect(response).toBeDefined();
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
// ============================================================================
|
|
830
|
+
// Test Suite: Request Cancellation
|
|
831
|
+
// ============================================================================
|
|
832
|
+
describe("HTTP Streaming - Request Cancellation", () => {
|
|
833
|
+
let server;
|
|
834
|
+
let serverPort;
|
|
835
|
+
afterEach(() => {
|
|
836
|
+
if (server) {
|
|
837
|
+
server.close();
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
it("can cancel an in-progress streaming request", async () => {
|
|
841
|
+
let requestReceived = false;
|
|
842
|
+
server = createMockServer((req, res) => {
|
|
843
|
+
requestReceived = true;
|
|
844
|
+
res.writeHead(200, {
|
|
845
|
+
"Content-Type": "application/x-ndjson",
|
|
846
|
+
"Transfer-Encoding": "chunked",
|
|
847
|
+
});
|
|
848
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "processing" });
|
|
849
|
+
res.write(toNDJSON(msg));
|
|
850
|
+
// Keep sending data slowly
|
|
851
|
+
const interval = setInterval(() => {
|
|
852
|
+
const msg = createStreamMessage("2", "agent_message", {
|
|
853
|
+
messageId: "m1",
|
|
854
|
+
data: { message: "Still processing..." },
|
|
855
|
+
});
|
|
856
|
+
try {
|
|
857
|
+
res.write(toNDJSON(msg));
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
clearInterval(interval);
|
|
861
|
+
}
|
|
862
|
+
}, 100);
|
|
863
|
+
req.on("close", () => {
|
|
864
|
+
clearInterval(interval);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
await new Promise((resolve) => {
|
|
868
|
+
server.listen(0, () => {
|
|
869
|
+
const addr = server.address();
|
|
870
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
871
|
+
resolve();
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
const chaos = new Chaos({
|
|
875
|
+
apiKey: "test-api-key",
|
|
876
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
877
|
+
timeout: 10000,
|
|
878
|
+
});
|
|
879
|
+
const receivedEvents = [];
|
|
880
|
+
// Start the request but cancel it after receiving some events
|
|
881
|
+
const requestPromise = chaos.chat.responses.create({
|
|
882
|
+
model: WALLET_MODEL,
|
|
883
|
+
input: [{ type: "message", role: "user", content: "Test query" }],
|
|
884
|
+
metadata: {
|
|
885
|
+
user_id: "test-user",
|
|
886
|
+
session_id: "test-session",
|
|
887
|
+
wallet_id: "0x123",
|
|
888
|
+
},
|
|
889
|
+
onStreamEvent: (event) => {
|
|
890
|
+
receivedEvents.push(event);
|
|
891
|
+
if (receivedEvents.length >= 2) {
|
|
892
|
+
// Cancel after receiving 2 events
|
|
893
|
+
chaos.chat.responses.cancel();
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
// The request should either throw or complete early
|
|
898
|
+
try {
|
|
899
|
+
await requestPromise;
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
// Cancellation may throw an error, which is acceptable
|
|
903
|
+
expect(error).toBeInstanceOf(Error);
|
|
904
|
+
}
|
|
905
|
+
expect(requestReceived).toBe(true);
|
|
906
|
+
// Should have received some events before cancellation
|
|
907
|
+
expect(receivedEvents.length).toBeGreaterThanOrEqual(1);
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
// ============================================================================
|
|
911
|
+
// Test Suite: Low-Level HTTP Streaming Module (TDD - Will Fail Until Implemented)
|
|
912
|
+
// ============================================================================
|
|
913
|
+
describe("HTTP Streaming Module - httpStreamRequest Function", () => {
|
|
914
|
+
let server;
|
|
915
|
+
let serverPort;
|
|
916
|
+
afterEach(() => {
|
|
917
|
+
if (server) {
|
|
918
|
+
server.close();
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
it("exports httpStreamRequest function", () => {
|
|
922
|
+
// This test will fail because the module doesn't exist yet
|
|
923
|
+
expect(typeof httpStreamRequest).toBe("function");
|
|
924
|
+
});
|
|
925
|
+
it("httpStreamRequest returns an async iterator of chunks", async () => {
|
|
926
|
+
server = createMockServer((req, res) => {
|
|
927
|
+
res.writeHead(200, {
|
|
928
|
+
"Content-Type": "application/x-ndjson",
|
|
929
|
+
"Transfer-Encoding": "chunked",
|
|
930
|
+
});
|
|
931
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "done" });
|
|
932
|
+
res.write(toNDJSON(msg));
|
|
933
|
+
res.end();
|
|
934
|
+
});
|
|
935
|
+
await new Promise((resolve) => {
|
|
936
|
+
server.listen(0, () => {
|
|
937
|
+
const addr = server.address();
|
|
938
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
939
|
+
resolve();
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
const chunks = [];
|
|
943
|
+
const iterator = httpStreamRequest({
|
|
944
|
+
url: `http://localhost:${serverPort}/v1/chat/stream`,
|
|
945
|
+
method: "POST",
|
|
946
|
+
headers: {
|
|
947
|
+
"Content-Type": "application/json",
|
|
948
|
+
Authorization: "Bearer test-key",
|
|
949
|
+
},
|
|
950
|
+
body: JSON.stringify({ query: "test" }),
|
|
951
|
+
timeout: 5000,
|
|
952
|
+
});
|
|
953
|
+
for await (const chunk of iterator) {
|
|
954
|
+
chunks.push(chunk);
|
|
955
|
+
}
|
|
956
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
957
|
+
expect(chunks.join("")).toContain("agent_status_change");
|
|
958
|
+
});
|
|
959
|
+
it("httpStreamRequest yields chunks as they arrive (not buffered)", async () => {
|
|
960
|
+
server = createMockServer((req, res) => {
|
|
961
|
+
res.writeHead(200, {
|
|
962
|
+
"Content-Type": "application/x-ndjson",
|
|
963
|
+
"Transfer-Encoding": "chunked",
|
|
964
|
+
});
|
|
965
|
+
const messages = [
|
|
966
|
+
createStreamMessage("1", "agent_status_change", { status: "processing" }),
|
|
967
|
+
createStreamMessage("2", "agent_message", { data: { message: "Hello" } }),
|
|
968
|
+
createStreamMessage("3", "agent_status_change", { status: "done" }),
|
|
969
|
+
];
|
|
970
|
+
let index = 0;
|
|
971
|
+
const sendNext = () => {
|
|
972
|
+
if (index < messages.length) {
|
|
973
|
+
res.write(toNDJSON(messages[index]));
|
|
974
|
+
index++;
|
|
975
|
+
setTimeout(sendNext, 100);
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
res.end();
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
sendNext();
|
|
982
|
+
});
|
|
983
|
+
await new Promise((resolve) => {
|
|
984
|
+
server.listen(0, () => {
|
|
985
|
+
const addr = server.address();
|
|
986
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
987
|
+
resolve();
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
const chunkTimestamps = [];
|
|
991
|
+
const iterator = httpStreamRequest({
|
|
992
|
+
url: `http://localhost:${serverPort}/v1/chat/stream`,
|
|
993
|
+
method: "POST",
|
|
994
|
+
headers: { "Content-Type": "application/json" },
|
|
995
|
+
body: "{}",
|
|
996
|
+
timeout: 5000,
|
|
997
|
+
});
|
|
998
|
+
for await (const chunk of iterator) {
|
|
999
|
+
chunkTimestamps.push(Date.now());
|
|
1000
|
+
}
|
|
1001
|
+
// Chunks should arrive with time gaps (streaming, not buffered)
|
|
1002
|
+
expect(chunkTimestamps.length).toBeGreaterThanOrEqual(2);
|
|
1003
|
+
for (let i = 1; i < chunkTimestamps.length; i++) {
|
|
1004
|
+
const gap = chunkTimestamps[i] - chunkTimestamps[i - 1];
|
|
1005
|
+
// Should have at least some gap (>50ms) if truly streaming
|
|
1006
|
+
expect(gap).toBeGreaterThanOrEqual(50);
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
it("httpStreamRequest throws on connection error", async () => {
|
|
1010
|
+
await expect((async () => {
|
|
1011
|
+
const iterator = httpStreamRequest({
|
|
1012
|
+
url: "http://localhost:59999/nonexistent",
|
|
1013
|
+
method: "POST",
|
|
1014
|
+
headers: {},
|
|
1015
|
+
body: "{}",
|
|
1016
|
+
timeout: 1000,
|
|
1017
|
+
});
|
|
1018
|
+
for await (const _ of iterator) {
|
|
1019
|
+
// Should not reach here
|
|
1020
|
+
}
|
|
1021
|
+
})()).rejects.toThrow();
|
|
1022
|
+
});
|
|
1023
|
+
it("httpStreamRequest throws on HTTP error status", async () => {
|
|
1024
|
+
server = createMockServer((req, res) => {
|
|
1025
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1026
|
+
res.end(JSON.stringify({ error: "Internal Server Error" }));
|
|
1027
|
+
});
|
|
1028
|
+
await new Promise((resolve) => {
|
|
1029
|
+
server.listen(0, () => {
|
|
1030
|
+
const addr = server.address();
|
|
1031
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
1032
|
+
resolve();
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
await expect((async () => {
|
|
1036
|
+
const iterator = httpStreamRequest({
|
|
1037
|
+
url: `http://localhost:${serverPort}/v1/chat/stream`,
|
|
1038
|
+
method: "POST",
|
|
1039
|
+
headers: {},
|
|
1040
|
+
body: "{}",
|
|
1041
|
+
timeout: 5000,
|
|
1042
|
+
});
|
|
1043
|
+
for await (const _ of iterator) {
|
|
1044
|
+
// Should throw before iterating
|
|
1045
|
+
}
|
|
1046
|
+
})()).rejects.toThrow();
|
|
1047
|
+
});
|
|
1048
|
+
it("httpStreamRequest respects timeout", async () => {
|
|
1049
|
+
server = createMockServer((req, res) => {
|
|
1050
|
+
res.writeHead(200, {
|
|
1051
|
+
"Content-Type": "application/x-ndjson",
|
|
1052
|
+
"Transfer-Encoding": "chunked",
|
|
1053
|
+
});
|
|
1054
|
+
// Send one chunk then hang
|
|
1055
|
+
res.write('{"partial": true}\n');
|
|
1056
|
+
// Never end the response
|
|
1057
|
+
});
|
|
1058
|
+
await new Promise((resolve) => {
|
|
1059
|
+
server.listen(0, () => {
|
|
1060
|
+
const addr = server.address();
|
|
1061
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
1062
|
+
resolve();
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
const startTime = Date.now();
|
|
1066
|
+
await expect((async () => {
|
|
1067
|
+
const iterator = httpStreamRequest({
|
|
1068
|
+
url: `http://localhost:${serverPort}/v1/chat/stream`,
|
|
1069
|
+
method: "POST",
|
|
1070
|
+
headers: {},
|
|
1071
|
+
body: "{}",
|
|
1072
|
+
timeout: 300,
|
|
1073
|
+
});
|
|
1074
|
+
for await (const _ of iterator) {
|
|
1075
|
+
// Should timeout
|
|
1076
|
+
}
|
|
1077
|
+
})()).rejects.toThrow();
|
|
1078
|
+
const elapsed = Date.now() - startTime;
|
|
1079
|
+
// Should timeout around 300ms, with some tolerance
|
|
1080
|
+
expect(elapsed).toBeLessThan(1000);
|
|
1081
|
+
expect(elapsed).toBeGreaterThanOrEqual(250);
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
describe("HTTP Streaming Module - StreamingHttpClient Class", () => {
|
|
1085
|
+
let server;
|
|
1086
|
+
let serverPort;
|
|
1087
|
+
afterEach(() => {
|
|
1088
|
+
if (server) {
|
|
1089
|
+
server.close();
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
it("exports StreamingHttpClient class", () => {
|
|
1093
|
+
expect(typeof StreamingHttpClient).toBe("function");
|
|
1094
|
+
});
|
|
1095
|
+
it("StreamingHttpClient.stream returns async iterator", async () => {
|
|
1096
|
+
server = createMockServer((req, res) => {
|
|
1097
|
+
res.writeHead(200, { "Content-Type": "application/x-ndjson" });
|
|
1098
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "done" });
|
|
1099
|
+
res.write(toNDJSON(msg));
|
|
1100
|
+
res.end();
|
|
1101
|
+
});
|
|
1102
|
+
await new Promise((resolve) => {
|
|
1103
|
+
server.listen(0, () => {
|
|
1104
|
+
const addr = server.address();
|
|
1105
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
1106
|
+
resolve();
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
const client = new StreamingHttpClient({
|
|
1110
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
1111
|
+
timeout: 5000,
|
|
1112
|
+
});
|
|
1113
|
+
const chunks = [];
|
|
1114
|
+
for await (const chunk of client.stream("/v1/chat/stream", {
|
|
1115
|
+
method: "POST",
|
|
1116
|
+
headers: { Authorization: "Bearer test" },
|
|
1117
|
+
body: "{}",
|
|
1118
|
+
})) {
|
|
1119
|
+
chunks.push(chunk);
|
|
1120
|
+
}
|
|
1121
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
1122
|
+
});
|
|
1123
|
+
it("StreamingHttpClient.abort cancels in-progress request", async () => {
|
|
1124
|
+
server = createMockServer((req, res) => {
|
|
1125
|
+
res.writeHead(200, {
|
|
1126
|
+
"Content-Type": "application/x-ndjson",
|
|
1127
|
+
"Transfer-Encoding": "chunked",
|
|
1128
|
+
});
|
|
1129
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "processing" });
|
|
1130
|
+
res.write(toNDJSON(msg));
|
|
1131
|
+
// Keep streaming indefinitely
|
|
1132
|
+
const interval = setInterval(() => {
|
|
1133
|
+
try {
|
|
1134
|
+
res.write('{"keep": "alive"}\n');
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
clearInterval(interval);
|
|
1138
|
+
}
|
|
1139
|
+
}, 50);
|
|
1140
|
+
req.on("close", () => {
|
|
1141
|
+
clearInterval(interval);
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
await new Promise((resolve) => {
|
|
1145
|
+
server.listen(0, () => {
|
|
1146
|
+
const addr = server.address();
|
|
1147
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
1148
|
+
resolve();
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
const client = new StreamingHttpClient({
|
|
1152
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
1153
|
+
timeout: 10000,
|
|
1154
|
+
});
|
|
1155
|
+
let chunkCount = 0;
|
|
1156
|
+
const abortPromise = new Promise((resolve) => {
|
|
1157
|
+
setTimeout(() => {
|
|
1158
|
+
client.abort();
|
|
1159
|
+
resolve();
|
|
1160
|
+
}, 200);
|
|
1161
|
+
});
|
|
1162
|
+
try {
|
|
1163
|
+
for await (const chunk of client.stream("/v1/chat/stream", {
|
|
1164
|
+
method: "POST",
|
|
1165
|
+
body: "{}",
|
|
1166
|
+
})) {
|
|
1167
|
+
chunkCount++;
|
|
1168
|
+
if (chunkCount > 10)
|
|
1169
|
+
break; // Safety limit
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
catch (error) {
|
|
1173
|
+
// Abort should cause an error
|
|
1174
|
+
expect(error).toBeInstanceOf(Error);
|
|
1175
|
+
}
|
|
1176
|
+
await abortPromise;
|
|
1177
|
+
expect(chunkCount).toBeLessThan(10);
|
|
1178
|
+
});
|
|
1179
|
+
it("StreamingHttpClient handles https:// URLs", async () => {
|
|
1180
|
+
// Note: This test documents the expected behavior for HTTPS
|
|
1181
|
+
// In practice, testing HTTPS requires certificates
|
|
1182
|
+
const client = new StreamingHttpClient({
|
|
1183
|
+
baseUrl: "https://example.com",
|
|
1184
|
+
timeout: 5000,
|
|
1185
|
+
});
|
|
1186
|
+
// The client should use https module for https:// URLs
|
|
1187
|
+
// We can verify this by checking internal state or behavior
|
|
1188
|
+
expect(client).toBeDefined();
|
|
1189
|
+
// The actual HTTPS test would require a real HTTPS server
|
|
1190
|
+
// For now, we document that it should work
|
|
1191
|
+
});
|
|
1192
|
+
it("StreamingHttpClient parses NDJSON lines correctly", async () => {
|
|
1193
|
+
server = createMockServer((req, res) => {
|
|
1194
|
+
res.writeHead(200, { "Content-Type": "application/x-ndjson" });
|
|
1195
|
+
const msg1 = createStreamMessage("1", "agent_status_change", { status: "processing" });
|
|
1196
|
+
const msg2 = createStreamMessage("2", "agent_message", {
|
|
1197
|
+
data: { message: "Hello world" },
|
|
1198
|
+
});
|
|
1199
|
+
const msg3 = createStreamMessage("3", "agent_status_change", { status: "done" });
|
|
1200
|
+
// Send with slight delays
|
|
1201
|
+
res.write(toNDJSON(msg1));
|
|
1202
|
+
setTimeout(() => res.write(toNDJSON(msg2)), 30);
|
|
1203
|
+
setTimeout(() => {
|
|
1204
|
+
res.write(toNDJSON(msg3));
|
|
1205
|
+
res.end();
|
|
1206
|
+
}, 60);
|
|
1207
|
+
});
|
|
1208
|
+
await new Promise((resolve) => {
|
|
1209
|
+
server.listen(0, () => {
|
|
1210
|
+
const addr = server.address();
|
|
1211
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
1212
|
+
resolve();
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
const client = new StreamingHttpClient({
|
|
1216
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
1217
|
+
timeout: 5000,
|
|
1218
|
+
});
|
|
1219
|
+
const lines = [];
|
|
1220
|
+
for await (const line of client.streamLines("/v1/chat/stream", {
|
|
1221
|
+
method: "POST",
|
|
1222
|
+
body: "{}",
|
|
1223
|
+
})) {
|
|
1224
|
+
lines.push(line);
|
|
1225
|
+
}
|
|
1226
|
+
expect(lines.length).toBe(3);
|
|
1227
|
+
expect(JSON.parse(lines[0]).id).toBe("1");
|
|
1228
|
+
expect(JSON.parse(lines[1]).id).toBe("2");
|
|
1229
|
+
expect(JSON.parse(lines[2]).id).toBe("3");
|
|
1230
|
+
});
|
|
1231
|
+
});
|
|
1232
|
+
describe("HTTP Streaming Module - Chaos Client Integration", () => {
|
|
1233
|
+
let server;
|
|
1234
|
+
let serverPort;
|
|
1235
|
+
afterEach(() => {
|
|
1236
|
+
if (server) {
|
|
1237
|
+
server.close();
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
it("Chaos client accepts useNativeHttp option in config", () => {
|
|
1241
|
+
// This test will fail until we add the useNativeHttp option
|
|
1242
|
+
const chaos = new Chaos({
|
|
1243
|
+
apiKey: "test-api-key",
|
|
1244
|
+
baseUrl: "http://localhost:3000",
|
|
1245
|
+
// @ts-expect-error - Option doesn't exist yet (TDD RED phase)
|
|
1246
|
+
useNativeHttp: true,
|
|
1247
|
+
});
|
|
1248
|
+
// The config should accept this option
|
|
1249
|
+
expect(chaos).toBeDefined();
|
|
1250
|
+
// @ts-expect-error - Property doesn't exist yet
|
|
1251
|
+
expect(chaos.useNativeHttp).toBe(true);
|
|
1252
|
+
});
|
|
1253
|
+
it("Chaos client uses http module when useNativeHttp is true", async () => {
|
|
1254
|
+
let requestMethod;
|
|
1255
|
+
let connectionHeader;
|
|
1256
|
+
server = createMockServer((req, res) => {
|
|
1257
|
+
requestMethod = req.method;
|
|
1258
|
+
connectionHeader = req.headers["connection"];
|
|
1259
|
+
res.writeHead(200, { "Content-Type": "application/x-ndjson" });
|
|
1260
|
+
const msg = createStreamMessage("1", "agent_status_change", { status: "done" });
|
|
1261
|
+
res.write(toNDJSON(msg));
|
|
1262
|
+
res.end();
|
|
1263
|
+
});
|
|
1264
|
+
await new Promise((resolve) => {
|
|
1265
|
+
server.listen(0, () => {
|
|
1266
|
+
const addr = server.address();
|
|
1267
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
1268
|
+
resolve();
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
const chaos = new Chaos({
|
|
1272
|
+
apiKey: "test-api-key",
|
|
1273
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
1274
|
+
// @ts-expect-error - Option doesn't exist yet (TDD RED phase)
|
|
1275
|
+
useNativeHttp: true,
|
|
1276
|
+
});
|
|
1277
|
+
await chaos.chat.responses.create({
|
|
1278
|
+
model: WALLET_MODEL,
|
|
1279
|
+
input: [{ type: "message", role: "user", content: "Test" }],
|
|
1280
|
+
metadata: {
|
|
1281
|
+
user_id: "test-user",
|
|
1282
|
+
session_id: "test-session",
|
|
1283
|
+
wallet_id: "0x123",
|
|
1284
|
+
},
|
|
1285
|
+
});
|
|
1286
|
+
expect(requestMethod).toBe("POST");
|
|
1287
|
+
// When using native http, we should see the connection header handled differently
|
|
1288
|
+
// This is implementation-specific and may need adjustment
|
|
1289
|
+
});
|
|
1290
|
+
it("getStreamingClient returns StreamingHttpClient instance", () => {
|
|
1291
|
+
const chaos = new Chaos({
|
|
1292
|
+
apiKey: "test-api-key",
|
|
1293
|
+
baseUrl: "http://localhost:3000",
|
|
1294
|
+
});
|
|
1295
|
+
// @ts-expect-error - Method doesn't exist yet
|
|
1296
|
+
const streamingClient = chaos.getStreamingClient();
|
|
1297
|
+
expect(streamingClient).toBeInstanceOf(StreamingHttpClient);
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
describe("HTTP Streaming Module - Edge Cases", () => {
|
|
1301
|
+
let server;
|
|
1302
|
+
let serverPort;
|
|
1303
|
+
afterEach(() => {
|
|
1304
|
+
if (server) {
|
|
1305
|
+
server.close();
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
it("handles empty response body gracefully", async () => {
|
|
1309
|
+
server = createMockServer((req, res) => {
|
|
1310
|
+
res.writeHead(200, { "Content-Type": "application/x-ndjson" });
|
|
1311
|
+
res.end(); // Empty body
|
|
1312
|
+
});
|
|
1313
|
+
await new Promise((resolve) => {
|
|
1314
|
+
server.listen(0, () => {
|
|
1315
|
+
const addr = server.address();
|
|
1316
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
1317
|
+
resolve();
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
const chunks = [];
|
|
1321
|
+
const iterator = httpStreamRequest({
|
|
1322
|
+
url: `http://localhost:${serverPort}/v1/chat/stream`,
|
|
1323
|
+
method: "POST",
|
|
1324
|
+
headers: {},
|
|
1325
|
+
body: "{}",
|
|
1326
|
+
timeout: 5000,
|
|
1327
|
+
});
|
|
1328
|
+
for await (const chunk of iterator) {
|
|
1329
|
+
chunks.push(chunk);
|
|
1330
|
+
}
|
|
1331
|
+
// Should complete without error, with zero or minimal chunks
|
|
1332
|
+
expect(chunks.length).toBeLessThanOrEqual(1);
|
|
1333
|
+
});
|
|
1334
|
+
it("handles very large chunks", async () => {
|
|
1335
|
+
const largeData = "x".repeat(100000); // 100KB of data
|
|
1336
|
+
const largeMessage = createStreamMessage("1", "agent_message", {
|
|
1337
|
+
data: { message: largeData },
|
|
1338
|
+
});
|
|
1339
|
+
server = createMockServer((req, res) => {
|
|
1340
|
+
res.writeHead(200, { "Content-Type": "application/x-ndjson" });
|
|
1341
|
+
res.write(toNDJSON(largeMessage));
|
|
1342
|
+
res.end();
|
|
1343
|
+
});
|
|
1344
|
+
await new Promise((resolve) => {
|
|
1345
|
+
server.listen(0, () => {
|
|
1346
|
+
const addr = server.address();
|
|
1347
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
1348
|
+
resolve();
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1351
|
+
const allData = [];
|
|
1352
|
+
const iterator = httpStreamRequest({
|
|
1353
|
+
url: `http://localhost:${serverPort}/v1/chat/stream`,
|
|
1354
|
+
method: "POST",
|
|
1355
|
+
headers: {},
|
|
1356
|
+
body: "{}",
|
|
1357
|
+
timeout: 10000,
|
|
1358
|
+
});
|
|
1359
|
+
for await (const chunk of iterator) {
|
|
1360
|
+
allData.push(chunk);
|
|
1361
|
+
}
|
|
1362
|
+
const combined = allData.join("");
|
|
1363
|
+
expect(combined).toContain(largeData);
|
|
1364
|
+
});
|
|
1365
|
+
it("handles rapid successive requests", async () => {
|
|
1366
|
+
let requestCount = 0;
|
|
1367
|
+
server = createMockServer((req, res) => {
|
|
1368
|
+
requestCount++;
|
|
1369
|
+
res.writeHead(200, { "Content-Type": "application/x-ndjson" });
|
|
1370
|
+
const msg = createStreamMessage(String(requestCount), "agent_status_change", {
|
|
1371
|
+
status: "done",
|
|
1372
|
+
});
|
|
1373
|
+
res.write(toNDJSON(msg));
|
|
1374
|
+
res.end();
|
|
1375
|
+
});
|
|
1376
|
+
await new Promise((resolve) => {
|
|
1377
|
+
server.listen(0, () => {
|
|
1378
|
+
const addr = server.address();
|
|
1379
|
+
serverPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
1380
|
+
resolve();
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
1383
|
+
const client = new StreamingHttpClient({
|
|
1384
|
+
baseUrl: `http://localhost:${serverPort}`,
|
|
1385
|
+
timeout: 5000,
|
|
1386
|
+
});
|
|
1387
|
+
// Make 5 rapid requests
|
|
1388
|
+
const results = await Promise.all(Array.from({ length: 5 }, async (_, i) => {
|
|
1389
|
+
const chunks = [];
|
|
1390
|
+
for await (const chunk of client.stream("/v1/chat/stream", {
|
|
1391
|
+
method: "POST",
|
|
1392
|
+
body: JSON.stringify({ request: i }),
|
|
1393
|
+
})) {
|
|
1394
|
+
chunks.push(chunk);
|
|
1395
|
+
}
|
|
1396
|
+
return chunks.join("");
|
|
1397
|
+
}));
|
|
1398
|
+
expect(results.length).toBe(5);
|
|
1399
|
+
expect(requestCount).toBe(5);
|
|
1400
|
+
});
|
|
1401
|
+
});
|