@posthog/agent 2.3.657 → 2.3.658
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/dist/agent.js +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.js +1 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.d.ts +4 -0
- package/dist/server/agent-server.js +708 -4
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +716 -7
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +3 -3
- package/src/server/agent-server.test.ts +241 -0
- package/src/server/agent-server.ts +55 -3
- package/src/server/bin.ts +12 -0
- package/src/server/event-stream-sender.test.ts +74 -0
- package/src/server/event-stream-sender.ts +101 -52
- package/src/server/streaming-upload.ts +160 -0
- package/src/server/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/agent",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.658",
|
|
4
4
|
"repository": "https://github.com/PostHog/code",
|
|
5
5
|
"description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
6
6
|
"exports": {
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
"typescript": "^5.5.0",
|
|
108
108
|
"vitest": "^2.1.8",
|
|
109
109
|
"@posthog/shared": "1.0.0",
|
|
110
|
-
"@posthog/
|
|
111
|
-
"@posthog/
|
|
110
|
+
"@posthog/enricher": "1.0.0",
|
|
111
|
+
"@posthog/git": "1.0.0"
|
|
112
112
|
},
|
|
113
113
|
"dependencies": {
|
|
114
114
|
"@agentclientprotocol/sdk": "0.19.0",
|
|
@@ -203,6 +203,247 @@ describe("AgentServer HTTP Mode", () => {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
describe("turn completion", () => {
|
|
206
|
+
function stubSessionCleanup(testServer: unknown): {
|
|
207
|
+
cleanupSession: (options?: {
|
|
208
|
+
completeEventStream?: boolean;
|
|
209
|
+
}) => Promise<void>;
|
|
210
|
+
eventStreamSender: {
|
|
211
|
+
enqueue: ReturnType<typeof vi.fn>;
|
|
212
|
+
stop: ReturnType<typeof vi.fn>;
|
|
213
|
+
};
|
|
214
|
+
} {
|
|
215
|
+
const cleanupServer = testServer as {
|
|
216
|
+
session: unknown;
|
|
217
|
+
eventStreamSender: {
|
|
218
|
+
enqueue: ReturnType<typeof vi.fn>;
|
|
219
|
+
stop: ReturnType<typeof vi.fn>;
|
|
220
|
+
};
|
|
221
|
+
captureCheckpointState: ReturnType<typeof vi.fn>;
|
|
222
|
+
cleanupSession: (options?: {
|
|
223
|
+
completeEventStream?: boolean;
|
|
224
|
+
}) => Promise<void>;
|
|
225
|
+
};
|
|
226
|
+
cleanupServer.captureCheckpointState = vi.fn(async () => {});
|
|
227
|
+
cleanupServer.eventStreamSender = {
|
|
228
|
+
enqueue: vi.fn(),
|
|
229
|
+
stop: vi.fn(async () => {}),
|
|
230
|
+
};
|
|
231
|
+
cleanupServer.session = {
|
|
232
|
+
payload: { run_id: "run-1" },
|
|
233
|
+
pendingHandoffGitState: undefined,
|
|
234
|
+
logWriter: { flush: vi.fn(async () => {}) },
|
|
235
|
+
acpConnection: { cleanup: vi.fn(async () => {}) },
|
|
236
|
+
sseController: { close: vi.fn() },
|
|
237
|
+
};
|
|
238
|
+
return cleanupServer;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
it("keeps event ingest open for non-terminal session cleanup", async () => {
|
|
242
|
+
const testServer = stubSessionCleanup(createServer());
|
|
243
|
+
|
|
244
|
+
await testServer.cleanupSession();
|
|
245
|
+
|
|
246
|
+
expect(testServer.eventStreamSender.enqueue).not.toHaveBeenCalled();
|
|
247
|
+
expect(testServer.eventStreamSender.stop).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("stops event ingest for terminal session cleanup without fake task completion", async () => {
|
|
251
|
+
const testServer = stubSessionCleanup(createServer());
|
|
252
|
+
|
|
253
|
+
await testServer.cleanupSession({ completeEventStream: true });
|
|
254
|
+
|
|
255
|
+
expect(testServer.eventStreamSender.enqueue).not.toHaveBeenCalled();
|
|
256
|
+
expect(testServer.eventStreamSender.stop).toHaveBeenCalledOnce();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("writes terminal failure status before completing event ingest", async () => {
|
|
260
|
+
const order: string[] = [];
|
|
261
|
+
const testServer = new AgentServer({
|
|
262
|
+
port,
|
|
263
|
+
jwtPublicKey: TEST_PUBLIC_KEY,
|
|
264
|
+
repositoryPath: repo.path,
|
|
265
|
+
apiUrl: "http://localhost:8000",
|
|
266
|
+
apiKey: "test-api-key",
|
|
267
|
+
projectId: 1,
|
|
268
|
+
mode: "interactive",
|
|
269
|
+
taskId: "test-task-id",
|
|
270
|
+
runId: "test-run-id",
|
|
271
|
+
}) as unknown as {
|
|
272
|
+
eventStreamSender: {
|
|
273
|
+
enqueue: (event: Record<string, unknown>) => void;
|
|
274
|
+
stop: () => Promise<void>;
|
|
275
|
+
};
|
|
276
|
+
posthogAPI: {
|
|
277
|
+
updateTaskRun: (
|
|
278
|
+
taskId: string,
|
|
279
|
+
runId: string,
|
|
280
|
+
payload: Record<string, unknown>,
|
|
281
|
+
) => Promise<unknown>;
|
|
282
|
+
};
|
|
283
|
+
signalTaskComplete(
|
|
284
|
+
payload: JwtPayload,
|
|
285
|
+
stopReason: string,
|
|
286
|
+
errorMessage?: string,
|
|
287
|
+
): Promise<void>;
|
|
288
|
+
};
|
|
289
|
+
testServer.eventStreamSender = {
|
|
290
|
+
enqueue: vi.fn(() => {
|
|
291
|
+
order.push("enqueue");
|
|
292
|
+
}),
|
|
293
|
+
stop: vi.fn(async () => {
|
|
294
|
+
order.push("stop");
|
|
295
|
+
}),
|
|
296
|
+
};
|
|
297
|
+
testServer.posthogAPI = {
|
|
298
|
+
updateTaskRun: vi.fn(async () => {
|
|
299
|
+
order.push("update");
|
|
300
|
+
return {};
|
|
301
|
+
}),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
await testServer.signalTaskComplete(
|
|
305
|
+
{
|
|
306
|
+
run_id: "run-1",
|
|
307
|
+
task_id: "task-1",
|
|
308
|
+
team_id: 1,
|
|
309
|
+
user_id: 1,
|
|
310
|
+
distinct_id: "distinct-id",
|
|
311
|
+
mode: "interactive",
|
|
312
|
+
},
|
|
313
|
+
"error",
|
|
314
|
+
"boom",
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
expect(order).toEqual(["enqueue", "update", "stop"]);
|
|
318
|
+
expect(testServer.eventStreamSender.enqueue).toHaveBeenCalledWith(
|
|
319
|
+
expect.objectContaining({
|
|
320
|
+
type: "notification",
|
|
321
|
+
notification: expect.objectContaining({
|
|
322
|
+
method: "_posthog/error",
|
|
323
|
+
params: expect.objectContaining({ error: "boom" }),
|
|
324
|
+
}),
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
expect(testServer.posthogAPI.updateTaskRun).toHaveBeenCalledWith(
|
|
328
|
+
"task-1",
|
|
329
|
+
"run-1",
|
|
330
|
+
{
|
|
331
|
+
status: "failed",
|
|
332
|
+
error_message: "boom",
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("still stops event ingest when terminal failure status update fails", async () => {
|
|
338
|
+
const testServer = new AgentServer({
|
|
339
|
+
port,
|
|
340
|
+
jwtPublicKey: TEST_PUBLIC_KEY,
|
|
341
|
+
repositoryPath: repo.path,
|
|
342
|
+
apiUrl: "http://localhost:8000",
|
|
343
|
+
apiKey: "test-api-key",
|
|
344
|
+
projectId: 1,
|
|
345
|
+
mode: "interactive",
|
|
346
|
+
taskId: "test-task-id",
|
|
347
|
+
runId: "test-run-id",
|
|
348
|
+
}) as unknown as {
|
|
349
|
+
eventStreamSender: {
|
|
350
|
+
enqueue: (event: Record<string, unknown>) => void;
|
|
351
|
+
stop: () => Promise<void>;
|
|
352
|
+
};
|
|
353
|
+
posthogAPI: {
|
|
354
|
+
updateTaskRun: (
|
|
355
|
+
taskId: string,
|
|
356
|
+
runId: string,
|
|
357
|
+
payload: Record<string, unknown>,
|
|
358
|
+
) => Promise<unknown>;
|
|
359
|
+
};
|
|
360
|
+
signalTaskComplete(
|
|
361
|
+
payload: JwtPayload,
|
|
362
|
+
stopReason: string,
|
|
363
|
+
errorMessage?: string,
|
|
364
|
+
): Promise<void>;
|
|
365
|
+
};
|
|
366
|
+
testServer.eventStreamSender = {
|
|
367
|
+
enqueue: vi.fn(),
|
|
368
|
+
stop: vi.fn(async () => {}),
|
|
369
|
+
};
|
|
370
|
+
testServer.posthogAPI = {
|
|
371
|
+
updateTaskRun: vi.fn(async () => {
|
|
372
|
+
throw new Error("update failed");
|
|
373
|
+
}),
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
await testServer.signalTaskComplete(
|
|
377
|
+
{
|
|
378
|
+
run_id: "run-1",
|
|
379
|
+
task_id: "task-1",
|
|
380
|
+
team_id: 1,
|
|
381
|
+
user_id: 1,
|
|
382
|
+
distinct_id: "distinct-id",
|
|
383
|
+
mode: "interactive",
|
|
384
|
+
},
|
|
385
|
+
"error",
|
|
386
|
+
"boom",
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
expect(testServer.eventStreamSender.enqueue).toHaveBeenCalledOnce();
|
|
390
|
+
expect(testServer.posthogAPI.updateTaskRun).toHaveBeenCalledOnce();
|
|
391
|
+
expect(testServer.eventStreamSender.stop).toHaveBeenCalledOnce();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("leaves event ingest open for non-error stop reasons", async () => {
|
|
395
|
+
const testServer = new AgentServer({
|
|
396
|
+
port,
|
|
397
|
+
jwtPublicKey: TEST_PUBLIC_KEY,
|
|
398
|
+
repositoryPath: repo.path,
|
|
399
|
+
apiUrl: "http://localhost:8000",
|
|
400
|
+
apiKey: "test-api-key",
|
|
401
|
+
projectId: 1,
|
|
402
|
+
mode: "interactive",
|
|
403
|
+
taskId: "test-task-id",
|
|
404
|
+
runId: "test-run-id",
|
|
405
|
+
}) as unknown as {
|
|
406
|
+
eventStreamSender: {
|
|
407
|
+
enqueue: (event: Record<string, unknown>) => void;
|
|
408
|
+
stop: () => Promise<void>;
|
|
409
|
+
};
|
|
410
|
+
posthogAPI: {
|
|
411
|
+
updateTaskRun: (
|
|
412
|
+
taskId: string,
|
|
413
|
+
runId: string,
|
|
414
|
+
payload: Record<string, unknown>,
|
|
415
|
+
) => Promise<unknown>;
|
|
416
|
+
};
|
|
417
|
+
signalTaskComplete(
|
|
418
|
+
payload: JwtPayload,
|
|
419
|
+
stopReason: string,
|
|
420
|
+
): Promise<void>;
|
|
421
|
+
};
|
|
422
|
+
testServer.eventStreamSender = {
|
|
423
|
+
enqueue: vi.fn(),
|
|
424
|
+
stop: vi.fn(async () => {}),
|
|
425
|
+
};
|
|
426
|
+
testServer.posthogAPI = {
|
|
427
|
+
updateTaskRun: vi.fn(async () => ({})),
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
await testServer.signalTaskComplete(
|
|
431
|
+
{
|
|
432
|
+
run_id: "run-1",
|
|
433
|
+
task_id: "task-1",
|
|
434
|
+
team_id: 1,
|
|
435
|
+
user_id: 1,
|
|
436
|
+
distinct_id: "distinct-id",
|
|
437
|
+
mode: "interactive",
|
|
438
|
+
},
|
|
439
|
+
"end_turn",
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
expect(testServer.eventStreamSender.enqueue).not.toHaveBeenCalled();
|
|
443
|
+
expect(testServer.eventStreamSender.stop).not.toHaveBeenCalled();
|
|
444
|
+
expect(testServer.posthogAPI.updateTaskRun).not.toHaveBeenCalled();
|
|
445
|
+
});
|
|
446
|
+
|
|
206
447
|
it("persists structured turn completion notifications", () => {
|
|
207
448
|
const appendRawLine = vi.fn();
|
|
208
449
|
const testServer = new AgentServer({
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
normalizeCloudPromptContent,
|
|
60
60
|
promptBlocksToText,
|
|
61
61
|
} from "./cloud-prompt";
|
|
62
|
+
import { TaskRunEventStreamSender } from "./event-stream-sender";
|
|
62
63
|
import { type JwtPayload, JwtValidationError, validateJwt } from "./jwt";
|
|
63
64
|
import {
|
|
64
65
|
handoffLocalGitStateSchema,
|
|
@@ -233,6 +234,7 @@ export class AgentServer {
|
|
|
233
234
|
private session: ActiveSession | null = null;
|
|
234
235
|
private app: Hono;
|
|
235
236
|
private posthogAPI: PostHogAPIClient;
|
|
237
|
+
private eventStreamSender: TaskRunEventStreamSender | null = null;
|
|
236
238
|
private questionRelayedToSlack = false;
|
|
237
239
|
private detectedPrUrl: string | null = null;
|
|
238
240
|
private lastReportedBranch: string | null = null;
|
|
@@ -297,6 +299,17 @@ export class AgentServer {
|
|
|
297
299
|
getApiKey: () => config.apiKey,
|
|
298
300
|
userAgent: `posthog/cloud.hog.dev; version: ${config.version ?? packageJson.version}`,
|
|
299
301
|
});
|
|
302
|
+
if (config.eventIngestToken) {
|
|
303
|
+
this.eventStreamSender = new TaskRunEventStreamSender({
|
|
304
|
+
apiUrl: config.apiUrl,
|
|
305
|
+
projectId: config.projectId,
|
|
306
|
+
taskId: config.taskId,
|
|
307
|
+
runId: config.runId,
|
|
308
|
+
token: config.eventIngestToken,
|
|
309
|
+
logger: this.logger.child("EventIngest"),
|
|
310
|
+
streamWindowMs: config.eventIngestStreamWindowMs,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
300
313
|
this.app = this.createApp();
|
|
301
314
|
}
|
|
302
315
|
|
|
@@ -560,7 +573,9 @@ export class AgentServer {
|
|
|
560
573
|
this.logger.debug("Stopping agent server...");
|
|
561
574
|
|
|
562
575
|
if (this.session) {
|
|
563
|
-
await this.cleanupSession();
|
|
576
|
+
await this.cleanupSession({ completeEventStream: true });
|
|
577
|
+
} else {
|
|
578
|
+
await this.eventStreamSender?.stop();
|
|
564
579
|
}
|
|
565
580
|
|
|
566
581
|
if (this.server) {
|
|
@@ -1798,6 +1813,12 @@ ${signedCommitInstructions}
|
|
|
1798
1813
|
|
|
1799
1814
|
const status = "failed";
|
|
1800
1815
|
|
|
1816
|
+
this.enqueueTaskTerminalEvent(POSTHOG_NOTIFICATIONS.ERROR, {
|
|
1817
|
+
source: "agent_server",
|
|
1818
|
+
stopReason,
|
|
1819
|
+
error: errorMessage ?? "Agent error",
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1801
1822
|
try {
|
|
1802
1823
|
await this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {
|
|
1803
1824
|
status,
|
|
@@ -1806,9 +1827,28 @@ ${signedCommitInstructions}
|
|
|
1806
1827
|
this.logger.debug("Task completion signaled", { status, stopReason });
|
|
1807
1828
|
} catch (error) {
|
|
1808
1829
|
this.logger.error("Failed to signal task completion", error);
|
|
1830
|
+
} finally {
|
|
1831
|
+
await this.eventStreamSender?.stop();
|
|
1809
1832
|
}
|
|
1810
1833
|
}
|
|
1811
1834
|
|
|
1835
|
+
private enqueueTaskTerminalEvent(
|
|
1836
|
+
method:
|
|
1837
|
+
| typeof POSTHOG_NOTIFICATIONS.TASK_COMPLETE
|
|
1838
|
+
| typeof POSTHOG_NOTIFICATIONS.ERROR,
|
|
1839
|
+
params: Record<string, unknown>,
|
|
1840
|
+
): void {
|
|
1841
|
+
this.eventStreamSender?.enqueue({
|
|
1842
|
+
type: "notification",
|
|
1843
|
+
timestamp: new Date().toISOString(),
|
|
1844
|
+
notification: {
|
|
1845
|
+
jsonrpc: "2.0",
|
|
1846
|
+
method,
|
|
1847
|
+
params,
|
|
1848
|
+
},
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1812
1852
|
private configureEnvironment({
|
|
1813
1853
|
isInternal = false,
|
|
1814
1854
|
originProduct,
|
|
@@ -2224,7 +2264,11 @@ ${signedCommitInstructions}
|
|
|
2224
2264
|
}
|
|
2225
2265
|
}
|
|
2226
2266
|
|
|
2227
|
-
private async cleanupSession(
|
|
2267
|
+
private async cleanupSession({
|
|
2268
|
+
completeEventStream = false,
|
|
2269
|
+
}: {
|
|
2270
|
+
completeEventStream?: boolean;
|
|
2271
|
+
} = {}): Promise<void> {
|
|
2228
2272
|
if (!this.session) return;
|
|
2229
2273
|
|
|
2230
2274
|
this.logger.debug("Cleaning up session");
|
|
@@ -2263,6 +2307,10 @@ ${signedCommitInstructions}
|
|
|
2263
2307
|
this.session.sseController.close();
|
|
2264
2308
|
}
|
|
2265
2309
|
|
|
2310
|
+
if (completeEventStream) {
|
|
2311
|
+
await this.eventStreamSender?.stop();
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2266
2314
|
this.pendingEvents = [];
|
|
2267
2315
|
this.lastReportedBranch = null;
|
|
2268
2316
|
this.session = null;
|
|
@@ -2346,9 +2394,13 @@ ${signedCommitInstructions}
|
|
|
2346
2394
|
}
|
|
2347
2395
|
|
|
2348
2396
|
private broadcastEvent(event: Record<string, unknown>): void {
|
|
2397
|
+
if (!this.session) return;
|
|
2398
|
+
|
|
2399
|
+
this.eventStreamSender?.enqueue(event);
|
|
2400
|
+
|
|
2349
2401
|
if (this.session?.sseController) {
|
|
2350
2402
|
this.sendSseEvent(this.session.sseController, event);
|
|
2351
|
-
} else
|
|
2403
|
+
} else {
|
|
2352
2404
|
// Buffer events during initialization (sseController not yet attached)
|
|
2353
2405
|
this.pendingEvents.push(event);
|
|
2354
2406
|
}
|
package/src/server/bin.ts
CHANGED
|
@@ -32,6 +32,15 @@ const envSchema = z.object({
|
|
|
32
32
|
POSTHOG_CODE_REASONING_EFFORT: z
|
|
33
33
|
.enum(["low", "medium", "high", "xhigh", "max"])
|
|
34
34
|
.optional(),
|
|
35
|
+
POSTHOG_TASK_RUN_EVENT_INGEST_TOKEN: z.string().min(1).optional(),
|
|
36
|
+
POSTHOG_TASK_RUN_EVENT_INGEST_STREAM_WINDOW_MS: z
|
|
37
|
+
.string()
|
|
38
|
+
.regex(
|
|
39
|
+
/^[1-9]\d*$/,
|
|
40
|
+
"POSTHOG_TASK_RUN_EVENT_INGEST_STREAM_WINDOW_MS must be a positive integer",
|
|
41
|
+
)
|
|
42
|
+
.transform((value) => parseInt(value, 10))
|
|
43
|
+
.optional(),
|
|
35
44
|
});
|
|
36
45
|
|
|
37
46
|
const program = new Command();
|
|
@@ -148,6 +157,9 @@ program
|
|
|
148
157
|
const server = new AgentServer({
|
|
149
158
|
port: parseInt(options.port, 10),
|
|
150
159
|
jwtPublicKey: env.JWT_PUBLIC_KEY,
|
|
160
|
+
eventIngestToken: env.POSTHOG_TASK_RUN_EVENT_INGEST_TOKEN,
|
|
161
|
+
eventIngestStreamWindowMs:
|
|
162
|
+
env.POSTHOG_TASK_RUN_EVENT_INGEST_STREAM_WINDOW_MS,
|
|
151
163
|
repositoryPath: options.repositoryPath,
|
|
152
164
|
apiUrl: env.POSTHOG_API_URL,
|
|
153
165
|
apiKey: env.POSTHOG_PERSONAL_API_KEY,
|
|
@@ -67,6 +67,46 @@ function responseForBody(body: string, lastAcceptedSeq = 0): Response {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
type StreamingRequestInit = RequestInit & { duplex: "half" };
|
|
71
|
+
|
|
72
|
+
function createFetchStreamingUpload({
|
|
73
|
+
url,
|
|
74
|
+
headers,
|
|
75
|
+
abortController,
|
|
76
|
+
}: {
|
|
77
|
+
url: string;
|
|
78
|
+
headers: Record<string, string>;
|
|
79
|
+
abortController: AbortController;
|
|
80
|
+
}) {
|
|
81
|
+
const bodyStream = new TransformStream<Uint8Array, Uint8Array>();
|
|
82
|
+
const writer = bodyStream.writable.getWriter();
|
|
83
|
+
const requestInit: StreamingRequestInit = {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers,
|
|
86
|
+
body: bodyStream.readable as BodyInit,
|
|
87
|
+
signal: abortController.signal,
|
|
88
|
+
duplex: "half",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
write(chunk: Uint8Array): Promise<void> {
|
|
93
|
+
return writer.write(chunk);
|
|
94
|
+
},
|
|
95
|
+
close(): Promise<void> {
|
|
96
|
+
return writer.close();
|
|
97
|
+
},
|
|
98
|
+
async abort(): Promise<void> {
|
|
99
|
+
abortController.abort();
|
|
100
|
+
try {
|
|
101
|
+
await writer.abort();
|
|
102
|
+
} catch {
|
|
103
|
+
// The fetch mock may have already closed the body reader.
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
responsePromise: fetch(url, requestInit),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
70
110
|
function createSender(
|
|
71
111
|
options: Partial<
|
|
72
112
|
ConstructorParameters<typeof TaskRunEventStreamSender>[0]
|
|
@@ -79,6 +119,7 @@ function createSender(
|
|
|
79
119
|
runId: "run-1",
|
|
80
120
|
token: "ingest-token",
|
|
81
121
|
logger: new Logger({ debug: false }),
|
|
122
|
+
createStreamingUpload: createFetchStreamingUpload,
|
|
82
123
|
...options,
|
|
83
124
|
});
|
|
84
125
|
}
|
|
@@ -198,6 +239,39 @@ describe("TaskRunEventStreamSender", () => {
|
|
|
198
239
|
]);
|
|
199
240
|
});
|
|
200
241
|
|
|
242
|
+
it("closes an idle active ingest request after the stream window elapses", async () => {
|
|
243
|
+
const requestBodies: string[] = [];
|
|
244
|
+
let activeStreamClosed = false;
|
|
245
|
+
const fetchMock = vi.fn(
|
|
246
|
+
async (_url: string | URL | Request, init?: RequestInit) => {
|
|
247
|
+
if (!init?.body || typeof init.body === "string") {
|
|
248
|
+
return responseForBody(await readRequestBody(init));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const body = await readRequestBody(init);
|
|
252
|
+
activeStreamClosed = true;
|
|
253
|
+
requestBodies.push(body);
|
|
254
|
+
return responseForBody(body);
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
258
|
+
|
|
259
|
+
const sender = createSender({ flushDelayMs: 0, streamWindowMs: 5 });
|
|
260
|
+
|
|
261
|
+
sender.enqueue({ type: "notification", notification: { method: "first" } });
|
|
262
|
+
await waitFor(() => fetchMock.mock.calls.length === 2);
|
|
263
|
+
expect(activeStreamClosed).toBe(false);
|
|
264
|
+
|
|
265
|
+
await waitFor(() => activeStreamClosed, 200);
|
|
266
|
+
expect(eventSequences(requestBodies[0])).toEqual([1]);
|
|
267
|
+
expect(completionSequences(requestBodies[0])).toEqual([]);
|
|
268
|
+
|
|
269
|
+
await sender.stop();
|
|
270
|
+
|
|
271
|
+
expect(eventSequences(requestBodies[1])).toEqual([]);
|
|
272
|
+
expect(completionSequences(requestBodies[1])).toEqual([1]);
|
|
273
|
+
});
|
|
274
|
+
|
|
201
275
|
it("aborts a stuck ingest response after closing the request body", async () => {
|
|
202
276
|
let aborted = false;
|
|
203
277
|
const fetchMock = vi.fn(
|