@trigger.dev/sdk 0.0.0-prerelease-20260220162801 → 0.0.0-prerelease-20260304181730

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 (54) hide show
  1. package/dist/commonjs/v3/ai.d.ts +245 -2
  2. package/dist/commonjs/v3/ai.js +384 -1
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/auth.d.ts +4 -0
  5. package/dist/commonjs/v3/auth.js.map +1 -1
  6. package/dist/commonjs/v3/chat-constants.d.ts +10 -0
  7. package/dist/commonjs/v3/chat-constants.js +14 -0
  8. package/dist/commonjs/v3/chat-constants.js.map +1 -0
  9. package/dist/commonjs/v3/chat-react.d.ts +42 -0
  10. package/dist/commonjs/v3/chat-react.js +63 -0
  11. package/dist/commonjs/v3/chat-react.js.map +1 -0
  12. package/dist/commonjs/v3/chat.d.ts +156 -0
  13. package/dist/commonjs/v3/chat.js +270 -0
  14. package/dist/commonjs/v3/chat.js.map +1 -0
  15. package/dist/commonjs/v3/chat.test.d.ts +1 -0
  16. package/dist/commonjs/v3/chat.test.js +1304 -0
  17. package/dist/commonjs/v3/chat.test.js.map +1 -0
  18. package/dist/commonjs/v3/runs.d.ts +4 -4
  19. package/dist/commonjs/v3/shared.js +10 -9
  20. package/dist/commonjs/v3/shared.js.map +1 -1
  21. package/dist/commonjs/v3/streams.d.ts +34 -2
  22. package/dist/commonjs/v3/streams.js +166 -2
  23. package/dist/commonjs/v3/streams.js.map +1 -1
  24. package/dist/commonjs/v3/wait.d.ts +2 -9
  25. package/dist/commonjs/v3/wait.js +7 -26
  26. package/dist/commonjs/v3/wait.js.map +1 -1
  27. package/dist/commonjs/version.js +1 -1
  28. package/dist/esm/v3/ai.d.ts +245 -2
  29. package/dist/esm/v3/ai.js +385 -2
  30. package/dist/esm/v3/ai.js.map +1 -1
  31. package/dist/esm/v3/auth.d.ts +4 -0
  32. package/dist/esm/v3/auth.js.map +1 -1
  33. package/dist/esm/v3/chat-constants.d.ts +10 -0
  34. package/dist/esm/v3/chat-constants.js +11 -0
  35. package/dist/esm/v3/chat-constants.js.map +1 -0
  36. package/dist/esm/v3/chat-react.d.ts +42 -0
  37. package/dist/esm/v3/chat-react.js +60 -0
  38. package/dist/esm/v3/chat-react.js.map +1 -0
  39. package/dist/esm/v3/chat.d.ts +156 -0
  40. package/dist/esm/v3/chat.js +265 -0
  41. package/dist/esm/v3/chat.js.map +1 -0
  42. package/dist/esm/v3/chat.test.d.ts +1 -0
  43. package/dist/esm/v3/chat.test.js +1302 -0
  44. package/dist/esm/v3/chat.test.js.map +1 -0
  45. package/dist/esm/v3/shared.js +10 -9
  46. package/dist/esm/v3/shared.js.map +1 -1
  47. package/dist/esm/v3/streams.d.ts +34 -2
  48. package/dist/esm/v3/streams.js +167 -3
  49. package/dist/esm/v3/streams.js.map +1 -1
  50. package/dist/esm/v3/wait.d.ts +2 -9
  51. package/dist/esm/v3/wait.js +2 -22
  52. package/dist/esm/v3/wait.js.map +1 -1
  53. package/dist/esm/version.js +1 -1
  54. package/package.json +40 -5
@@ -0,0 +1,1304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const chat_js_1 = require("./chat.js");
5
+ // Helper: encode text as SSE format
6
+ function sseEncode(chunks) {
7
+ return chunks.map((chunk, i) => `id: ${i}\ndata: ${JSON.stringify(chunk)}\n\n`).join("");
8
+ }
9
+ // Helper: create a ReadableStream from SSE text
10
+ function createSSEStream(sseText) {
11
+ const encoder = new TextEncoder();
12
+ return new ReadableStream({
13
+ start(controller) {
14
+ controller.enqueue(encoder.encode(sseText));
15
+ controller.close();
16
+ },
17
+ });
18
+ }
19
+ // Helper: create test UIMessages with unique IDs
20
+ let messageIdCounter = 0;
21
+ function createUserMessage(text) {
22
+ return {
23
+ id: `msg-user-${++messageIdCounter}`,
24
+ role: "user",
25
+ parts: [{ type: "text", text }],
26
+ };
27
+ }
28
+ function createAssistantMessage(text) {
29
+ return {
30
+ id: `msg-assistant-${++messageIdCounter}`,
31
+ role: "assistant",
32
+ parts: [{ type: "text", text }],
33
+ };
34
+ }
35
+ // Sample UIMessageChunks as the AI SDK would produce
36
+ const sampleChunks = [
37
+ { type: "text-start", id: "part-1" },
38
+ { type: "text-delta", id: "part-1", delta: "Hello" },
39
+ { type: "text-delta", id: "part-1", delta: " world" },
40
+ { type: "text-delta", id: "part-1", delta: "!" },
41
+ { type: "text-end", id: "part-1" },
42
+ ];
43
+ (0, vitest_1.describe)("TriggerChatTransport", () => {
44
+ let originalFetch;
45
+ (0, vitest_1.beforeEach)(() => {
46
+ originalFetch = global.fetch;
47
+ });
48
+ (0, vitest_1.afterEach)(() => {
49
+ global.fetch = originalFetch;
50
+ vitest_1.vi.restoreAllMocks();
51
+ });
52
+ (0, vitest_1.describe)("constructor", () => {
53
+ (0, vitest_1.it)("should create transport with required options", () => {
54
+ const transport = new chat_js_1.TriggerChatTransport({
55
+ task: "my-chat-task",
56
+ accessToken: "test-token",
57
+ });
58
+ (0, vitest_1.expect)(transport).toBeInstanceOf(chat_js_1.TriggerChatTransport);
59
+ });
60
+ (0, vitest_1.it)("should accept optional configuration", () => {
61
+ const transport = new chat_js_1.TriggerChatTransport({
62
+ task: "my-chat-task",
63
+ accessToken: "test-token",
64
+ baseURL: "https://custom.trigger.dev",
65
+ streamKey: "custom-stream",
66
+ headers: { "X-Custom": "value" },
67
+ });
68
+ (0, vitest_1.expect)(transport).toBeInstanceOf(chat_js_1.TriggerChatTransport);
69
+ });
70
+ (0, vitest_1.it)("should accept a function for accessToken", () => {
71
+ let tokenCallCount = 0;
72
+ const transport = new chat_js_1.TriggerChatTransport({
73
+ task: "my-chat-task",
74
+ accessToken: () => {
75
+ tokenCallCount++;
76
+ return `dynamic-token-${tokenCallCount}`;
77
+ },
78
+ });
79
+ (0, vitest_1.expect)(transport).toBeInstanceOf(chat_js_1.TriggerChatTransport);
80
+ });
81
+ });
82
+ (0, vitest_1.describe)("sendMessages", () => {
83
+ (0, vitest_1.it)("should trigger the task and return a ReadableStream of UIMessageChunks", async () => {
84
+ const triggerRunId = "run_abc123";
85
+ const publicToken = "pub_token_xyz";
86
+ // Mock fetch to handle both the trigger request and the SSE stream request
87
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
88
+ const urlStr = typeof url === "string" ? url : url.toString();
89
+ // Handle the task trigger request
90
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
91
+ return new Response(JSON.stringify({ id: triggerRunId }), {
92
+ status: 200,
93
+ headers: {
94
+ "content-type": "application/json",
95
+ "x-trigger-jwt": publicToken,
96
+ },
97
+ });
98
+ }
99
+ // Handle the SSE stream request
100
+ if (urlStr.includes("/realtime/v1/streams/")) {
101
+ const sseText = sseEncode(sampleChunks);
102
+ return new Response(createSSEStream(sseText), {
103
+ status: 200,
104
+ headers: {
105
+ "content-type": "text/event-stream",
106
+ "X-Stream-Version": "v1",
107
+ },
108
+ });
109
+ }
110
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
111
+ });
112
+ const transport = new chat_js_1.TriggerChatTransport({
113
+ task: "my-chat-task",
114
+ accessToken: "test-token",
115
+ baseURL: "https://api.test.trigger.dev",
116
+ });
117
+ const messages = [createUserMessage("Hello!")];
118
+ const stream = await transport.sendMessages({
119
+ trigger: "submit-message",
120
+ chatId: "chat-1",
121
+ messageId: undefined,
122
+ messages,
123
+ abortSignal: undefined,
124
+ });
125
+ (0, vitest_1.expect)(stream).toBeInstanceOf(ReadableStream);
126
+ // Read all chunks from the stream
127
+ const reader = stream.getReader();
128
+ const receivedChunks = [];
129
+ while (true) {
130
+ const { done, value } = await reader.read();
131
+ if (done)
132
+ break;
133
+ receivedChunks.push(value);
134
+ }
135
+ (0, vitest_1.expect)(receivedChunks).toHaveLength(sampleChunks.length);
136
+ (0, vitest_1.expect)(receivedChunks[0]).toEqual({ type: "text-start", id: "part-1" });
137
+ (0, vitest_1.expect)(receivedChunks[1]).toEqual({ type: "text-delta", id: "part-1", delta: "Hello" });
138
+ (0, vitest_1.expect)(receivedChunks[4]).toEqual({ type: "text-end", id: "part-1" });
139
+ });
140
+ (0, vitest_1.it)("should send the correct payload to the trigger API", async () => {
141
+ const fetchSpy = vitest_1.vi.fn().mockImplementation(async (url, init) => {
142
+ const urlStr = typeof url === "string" ? url : url.toString();
143
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
144
+ return new Response(JSON.stringify({ id: "run_test" }), {
145
+ status: 200,
146
+ headers: {
147
+ "content-type": "application/json",
148
+ "x-trigger-jwt": "pub_token",
149
+ },
150
+ });
151
+ }
152
+ if (urlStr.includes("/realtime/v1/streams/")) {
153
+ return new Response(createSSEStream(""), {
154
+ status: 200,
155
+ headers: {
156
+ "content-type": "text/event-stream",
157
+ "X-Stream-Version": "v1",
158
+ },
159
+ });
160
+ }
161
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
162
+ });
163
+ global.fetch = fetchSpy;
164
+ const transport = new chat_js_1.TriggerChatTransport({
165
+ task: "my-chat-task",
166
+ accessToken: "test-token",
167
+ baseURL: "https://api.test.trigger.dev",
168
+ });
169
+ const messages = [createUserMessage("Hello!")];
170
+ await transport.sendMessages({
171
+ trigger: "submit-message",
172
+ chatId: "chat-123",
173
+ messageId: undefined,
174
+ messages,
175
+ abortSignal: undefined,
176
+ metadata: { custom: "data" },
177
+ });
178
+ // Verify the trigger fetch call
179
+ const triggerCall = fetchSpy.mock.calls.find((call) => (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger"));
180
+ (0, vitest_1.expect)(triggerCall).toBeDefined();
181
+ const triggerUrl = typeof triggerCall[0] === "string" ? triggerCall[0] : triggerCall[0].toString();
182
+ (0, vitest_1.expect)(triggerUrl).toContain("/api/v1/tasks/my-chat-task/trigger");
183
+ const triggerBody = JSON.parse(triggerCall[1]?.body);
184
+ const payload = triggerBody.payload;
185
+ (0, vitest_1.expect)(payload.messages).toEqual(messages);
186
+ (0, vitest_1.expect)(payload.chatId).toBe("chat-123");
187
+ (0, vitest_1.expect)(payload.trigger).toBe("submit-message");
188
+ (0, vitest_1.expect)(payload.metadata).toEqual({ custom: "data" });
189
+ });
190
+ (0, vitest_1.it)("should use the correct stream URL with custom streamKey", async () => {
191
+ const fetchSpy = vitest_1.vi.fn().mockImplementation(async (url) => {
192
+ const urlStr = typeof url === "string" ? url : url.toString();
193
+ if (urlStr.includes("/trigger")) {
194
+ return new Response(JSON.stringify({ id: "run_custom" }), {
195
+ status: 200,
196
+ headers: {
197
+ "content-type": "application/json",
198
+ "x-trigger-jwt": "token",
199
+ },
200
+ });
201
+ }
202
+ if (urlStr.includes("/realtime/v1/streams/")) {
203
+ return new Response(createSSEStream(""), {
204
+ status: 200,
205
+ headers: {
206
+ "content-type": "text/event-stream",
207
+ "X-Stream-Version": "v1",
208
+ },
209
+ });
210
+ }
211
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
212
+ });
213
+ global.fetch = fetchSpy;
214
+ const transport = new chat_js_1.TriggerChatTransport({
215
+ task: "my-task",
216
+ accessToken: "token",
217
+ baseURL: "https://api.test.trigger.dev",
218
+ streamKey: "my-custom-stream",
219
+ });
220
+ await transport.sendMessages({
221
+ trigger: "submit-message",
222
+ chatId: "chat-1",
223
+ messageId: undefined,
224
+ messages: [createUserMessage("test")],
225
+ abortSignal: undefined,
226
+ });
227
+ // Verify the stream URL uses the custom stream key
228
+ const streamCall = fetchSpy.mock.calls.find((call) => (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/realtime/v1/streams/"));
229
+ (0, vitest_1.expect)(streamCall).toBeDefined();
230
+ const streamUrl = typeof streamCall[0] === "string" ? streamCall[0] : streamCall[0].toString();
231
+ (0, vitest_1.expect)(streamUrl).toContain("/realtime/v1/streams/run_custom/my-custom-stream");
232
+ });
233
+ (0, vitest_1.it)("should include extra headers in stream requests", async () => {
234
+ const fetchSpy = vitest_1.vi.fn().mockImplementation(async (url) => {
235
+ const urlStr = typeof url === "string" ? url : url.toString();
236
+ if (urlStr.includes("/trigger")) {
237
+ return new Response(JSON.stringify({ id: "run_hdrs" }), {
238
+ status: 200,
239
+ headers: {
240
+ "content-type": "application/json",
241
+ "x-trigger-jwt": "token",
242
+ },
243
+ });
244
+ }
245
+ if (urlStr.includes("/realtime/v1/streams/")) {
246
+ return new Response(createSSEStream(""), {
247
+ status: 200,
248
+ headers: {
249
+ "content-type": "text/event-stream",
250
+ "X-Stream-Version": "v1",
251
+ },
252
+ });
253
+ }
254
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
255
+ });
256
+ global.fetch = fetchSpy;
257
+ const transport = new chat_js_1.TriggerChatTransport({
258
+ task: "my-task",
259
+ accessToken: "token",
260
+ baseURL: "https://api.test.trigger.dev",
261
+ headers: { "X-Custom-Header": "custom-value" },
262
+ });
263
+ await transport.sendMessages({
264
+ trigger: "submit-message",
265
+ chatId: "chat-1",
266
+ messageId: undefined,
267
+ messages: [createUserMessage("test")],
268
+ abortSignal: undefined,
269
+ });
270
+ // Verify the stream request includes custom headers
271
+ const streamCall = fetchSpy.mock.calls.find((call) => (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/realtime/v1/streams/"));
272
+ (0, vitest_1.expect)(streamCall).toBeDefined();
273
+ const requestHeaders = streamCall[1]?.headers;
274
+ (0, vitest_1.expect)(requestHeaders["X-Custom-Header"]).toBe("custom-value");
275
+ });
276
+ });
277
+ (0, vitest_1.describe)("reconnectToStream", () => {
278
+ (0, vitest_1.it)("should return null when no session exists for chatId", async () => {
279
+ const transport = new chat_js_1.TriggerChatTransport({
280
+ task: "my-task",
281
+ accessToken: "token",
282
+ });
283
+ const result = await transport.reconnectToStream({
284
+ chatId: "nonexistent-chat",
285
+ });
286
+ (0, vitest_1.expect)(result).toBeNull();
287
+ });
288
+ (0, vitest_1.it)("should reconnect to an existing session", async () => {
289
+ const triggerRunId = "run_reconnect";
290
+ const publicToken = "pub_reconnect_token";
291
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
292
+ const urlStr = typeof url === "string" ? url : url.toString();
293
+ if (urlStr.includes("/trigger")) {
294
+ return new Response(JSON.stringify({ id: triggerRunId }), {
295
+ status: 200,
296
+ headers: {
297
+ "content-type": "application/json",
298
+ "x-trigger-jwt": publicToken,
299
+ },
300
+ });
301
+ }
302
+ if (urlStr.includes("/realtime/v1/streams/")) {
303
+ const chunks = [
304
+ { type: "text-start", id: "part-1" },
305
+ { type: "text-delta", id: "part-1", delta: "Reconnected!" },
306
+ { type: "text-end", id: "part-1" },
307
+ ];
308
+ return new Response(createSSEStream(sseEncode(chunks)), {
309
+ status: 200,
310
+ headers: {
311
+ "content-type": "text/event-stream",
312
+ "X-Stream-Version": "v1",
313
+ },
314
+ });
315
+ }
316
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
317
+ });
318
+ const transport = new chat_js_1.TriggerChatTransport({
319
+ task: "my-task",
320
+ accessToken: "token",
321
+ baseURL: "https://api.test.trigger.dev",
322
+ });
323
+ // First, send messages to establish a session
324
+ await transport.sendMessages({
325
+ trigger: "submit-message",
326
+ chatId: "chat-reconnect",
327
+ messageId: undefined,
328
+ messages: [createUserMessage("Hello")],
329
+ abortSignal: undefined,
330
+ });
331
+ // Now reconnect
332
+ const stream = await transport.reconnectToStream({
333
+ chatId: "chat-reconnect",
334
+ });
335
+ (0, vitest_1.expect)(stream).toBeInstanceOf(ReadableStream);
336
+ // Read the stream
337
+ const reader = stream.getReader();
338
+ const receivedChunks = [];
339
+ while (true) {
340
+ const { done, value } = await reader.read();
341
+ if (done)
342
+ break;
343
+ receivedChunks.push(value);
344
+ }
345
+ (0, vitest_1.expect)(receivedChunks.length).toBeGreaterThan(0);
346
+ });
347
+ });
348
+ (0, vitest_1.describe)("createChatTransport", () => {
349
+ (0, vitest_1.it)("should create a TriggerChatTransport instance", () => {
350
+ const transport = (0, chat_js_1.createChatTransport)({
351
+ task: "my-task",
352
+ accessToken: "token",
353
+ });
354
+ (0, vitest_1.expect)(transport).toBeInstanceOf(chat_js_1.TriggerChatTransport);
355
+ });
356
+ (0, vitest_1.it)("should pass options through to the transport", () => {
357
+ const transport = (0, chat_js_1.createChatTransport)({
358
+ task: "custom-task",
359
+ accessToken: "custom-token",
360
+ baseURL: "https://custom.example.com",
361
+ streamKey: "custom-key",
362
+ headers: { "X-Test": "value" },
363
+ });
364
+ (0, vitest_1.expect)(transport).toBeInstanceOf(chat_js_1.TriggerChatTransport);
365
+ });
366
+ });
367
+ (0, vitest_1.describe)("publicAccessToken from trigger response", () => {
368
+ (0, vitest_1.it)("should use x-trigger-jwt from trigger response as the stream auth token", async () => {
369
+ const fetchSpy = vitest_1.vi.fn().mockImplementation(async (url, init) => {
370
+ const urlStr = typeof url === "string" ? url : url.toString();
371
+ if (urlStr.includes("/trigger")) {
372
+ // Return with x-trigger-jwt header — this public token should be
373
+ // used for the subsequent stream subscription request.
374
+ return new Response(JSON.stringify({ id: "run_pat" }), {
375
+ status: 200,
376
+ headers: {
377
+ "content-type": "application/json",
378
+ "x-trigger-jwt": "server-generated-public-token",
379
+ },
380
+ });
381
+ }
382
+ if (urlStr.includes("/realtime/v1/streams/")) {
383
+ // Verify the Authorization header uses the server-generated token
384
+ const authHeader = init?.headers?.["Authorization"];
385
+ (0, vitest_1.expect)(authHeader).toBe("Bearer server-generated-public-token");
386
+ const chunks = [
387
+ { type: "text-start", id: "p1" },
388
+ { type: "text-end", id: "p1" },
389
+ ];
390
+ return new Response(createSSEStream(sseEncode(chunks)), {
391
+ status: 200,
392
+ headers: {
393
+ "content-type": "text/event-stream",
394
+ "X-Stream-Version": "v1",
395
+ },
396
+ });
397
+ }
398
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
399
+ });
400
+ global.fetch = fetchSpy;
401
+ const transport = new chat_js_1.TriggerChatTransport({
402
+ task: "my-task",
403
+ accessToken: "caller-token",
404
+ baseURL: "https://api.test.trigger.dev",
405
+ });
406
+ const stream = await transport.sendMessages({
407
+ trigger: "submit-message",
408
+ chatId: "chat-pat",
409
+ messageId: undefined,
410
+ messages: [createUserMessage("test")],
411
+ abortSignal: undefined,
412
+ });
413
+ // Consume the stream
414
+ const reader = stream.getReader();
415
+ while (true) {
416
+ const { done } = await reader.read();
417
+ if (done)
418
+ break;
419
+ }
420
+ // Verify the stream subscription used the public token, not the caller token
421
+ const streamCall = fetchSpy.mock.calls.find((call) => (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/realtime/v1/streams/"));
422
+ (0, vitest_1.expect)(streamCall).toBeDefined();
423
+ const streamHeaders = streamCall[1]?.headers;
424
+ (0, vitest_1.expect)(streamHeaders["Authorization"]).toBe("Bearer server-generated-public-token");
425
+ });
426
+ });
427
+ (0, vitest_1.describe)("error handling", () => {
428
+ (0, vitest_1.it)("should propagate trigger API errors", async () => {
429
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
430
+ const urlStr = typeof url === "string" ? url : url.toString();
431
+ if (urlStr.includes("/trigger")) {
432
+ return new Response(JSON.stringify({ error: "Task not found" }), {
433
+ status: 404,
434
+ headers: { "content-type": "application/json" },
435
+ });
436
+ }
437
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
438
+ });
439
+ const transport = new chat_js_1.TriggerChatTransport({
440
+ task: "nonexistent-task",
441
+ accessToken: "token",
442
+ baseURL: "https://api.test.trigger.dev",
443
+ });
444
+ await (0, vitest_1.expect)(transport.sendMessages({
445
+ trigger: "submit-message",
446
+ chatId: "chat-error",
447
+ messageId: undefined,
448
+ messages: [createUserMessage("test")],
449
+ abortSignal: undefined,
450
+ })).rejects.toThrow();
451
+ });
452
+ });
453
+ (0, vitest_1.describe)("abort signal", () => {
454
+ (0, vitest_1.it)("should close the stream gracefully when aborted", async () => {
455
+ let streamResolve;
456
+ const streamWait = new Promise((resolve) => {
457
+ streamResolve = resolve;
458
+ });
459
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
460
+ const urlStr = typeof url === "string" ? url : url.toString();
461
+ if (urlStr.includes("/trigger")) {
462
+ return new Response(JSON.stringify({ id: "run_abort" }), {
463
+ status: 200,
464
+ headers: {
465
+ "content-type": "application/json",
466
+ "x-trigger-jwt": "token",
467
+ },
468
+ });
469
+ }
470
+ if (urlStr.includes("/realtime/v1/streams/")) {
471
+ // Create a slow stream that waits before sending data
472
+ const stream = new ReadableStream({
473
+ async start(controller) {
474
+ const encoder = new TextEncoder();
475
+ controller.enqueue(encoder.encode(`id: 0\ndata: ${JSON.stringify({ type: "text-start", id: "p1" })}\n\n`));
476
+ // Wait for the test to signal it's done
477
+ await streamWait;
478
+ controller.close();
479
+ },
480
+ });
481
+ return new Response(stream, {
482
+ status: 200,
483
+ headers: {
484
+ "content-type": "text/event-stream",
485
+ "X-Stream-Version": "v1",
486
+ },
487
+ });
488
+ }
489
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
490
+ });
491
+ const abortController = new AbortController();
492
+ const transport = new chat_js_1.TriggerChatTransport({
493
+ task: "my-task",
494
+ accessToken: "token",
495
+ baseURL: "https://api.test.trigger.dev",
496
+ });
497
+ const stream = await transport.sendMessages({
498
+ trigger: "submit-message",
499
+ chatId: "chat-abort",
500
+ messageId: undefined,
501
+ messages: [createUserMessage("test")],
502
+ abortSignal: abortController.signal,
503
+ });
504
+ // Read the first chunk
505
+ const reader = stream.getReader();
506
+ const first = await reader.read();
507
+ (0, vitest_1.expect)(first.done).toBe(false);
508
+ // Abort and clean up
509
+ abortController.abort();
510
+ streamResolve?.();
511
+ // The stream should close — reading should return done
512
+ const next = await reader.read();
513
+ (0, vitest_1.expect)(next.done).toBe(true);
514
+ });
515
+ });
516
+ (0, vitest_1.describe)("multiple sessions", () => {
517
+ (0, vitest_1.it)("should track multiple chat sessions independently", async () => {
518
+ let callCount = 0;
519
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
520
+ const urlStr = typeof url === "string" ? url : url.toString();
521
+ if (urlStr.includes("/trigger")) {
522
+ callCount++;
523
+ return new Response(JSON.stringify({ id: `run_multi_${callCount}` }), {
524
+ status: 200,
525
+ headers: {
526
+ "content-type": "application/json",
527
+ "x-trigger-jwt": `token_${callCount}`,
528
+ },
529
+ });
530
+ }
531
+ if (urlStr.includes("/realtime/v1/streams/")) {
532
+ return new Response(createSSEStream(""), {
533
+ status: 200,
534
+ headers: {
535
+ "content-type": "text/event-stream",
536
+ "X-Stream-Version": "v1",
537
+ },
538
+ });
539
+ }
540
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
541
+ });
542
+ const transport = new chat_js_1.TriggerChatTransport({
543
+ task: "my-task",
544
+ accessToken: "token",
545
+ baseURL: "https://api.test.trigger.dev",
546
+ });
547
+ // Start two independent chat sessions
548
+ await transport.sendMessages({
549
+ trigger: "submit-message",
550
+ chatId: "session-a",
551
+ messageId: undefined,
552
+ messages: [createUserMessage("Hello A")],
553
+ abortSignal: undefined,
554
+ });
555
+ await transport.sendMessages({
556
+ trigger: "submit-message",
557
+ chatId: "session-b",
558
+ messageId: undefined,
559
+ messages: [createUserMessage("Hello B")],
560
+ abortSignal: undefined,
561
+ });
562
+ // Both sessions should be independently reconnectable
563
+ const streamA = await transport.reconnectToStream({ chatId: "session-a" });
564
+ const streamB = await transport.reconnectToStream({ chatId: "session-b" });
565
+ const streamC = await transport.reconnectToStream({ chatId: "nonexistent" });
566
+ (0, vitest_1.expect)(streamA).toBeInstanceOf(ReadableStream);
567
+ (0, vitest_1.expect)(streamB).toBeInstanceOf(ReadableStream);
568
+ (0, vitest_1.expect)(streamC).toBeNull();
569
+ });
570
+ });
571
+ (0, vitest_1.describe)("dynamic accessToken", () => {
572
+ (0, vitest_1.it)("should call the accessToken function for each sendMessages call", async () => {
573
+ let tokenCallCount = 0;
574
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
575
+ const urlStr = typeof url === "string" ? url : url.toString();
576
+ if (urlStr.includes("/trigger")) {
577
+ return new Response(JSON.stringify({ id: `run_dyn_${tokenCallCount}` }), {
578
+ status: 200,
579
+ headers: {
580
+ "content-type": "application/json",
581
+ "x-trigger-jwt": "stream-token",
582
+ },
583
+ });
584
+ }
585
+ if (urlStr.includes("/realtime/v1/streams/")) {
586
+ const chunks = [
587
+ { type: "text-start", id: "p1" },
588
+ { type: "text-end", id: "p1" },
589
+ ];
590
+ return new Response(createSSEStream(sseEncode(chunks)), {
591
+ status: 200,
592
+ headers: {
593
+ "content-type": "text/event-stream",
594
+ "X-Stream-Version": "v1",
595
+ },
596
+ });
597
+ }
598
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
599
+ });
600
+ const transport = new chat_js_1.TriggerChatTransport({
601
+ task: "my-task",
602
+ accessToken: () => {
603
+ tokenCallCount++;
604
+ return `dynamic-token-${tokenCallCount}`;
605
+ },
606
+ baseURL: "https://api.test.trigger.dev",
607
+ });
608
+ // First call — the token function should be invoked
609
+ await transport.sendMessages({
610
+ trigger: "submit-message",
611
+ chatId: "chat-dyn-1",
612
+ messageId: undefined,
613
+ messages: [createUserMessage("first")],
614
+ abortSignal: undefined,
615
+ });
616
+ const firstCount = tokenCallCount;
617
+ (0, vitest_1.expect)(firstCount).toBeGreaterThanOrEqual(1);
618
+ // Second call — the token function should be invoked again
619
+ await transport.sendMessages({
620
+ trigger: "submit-message",
621
+ chatId: "chat-dyn-2",
622
+ messageId: undefined,
623
+ messages: [createUserMessage("second")],
624
+ abortSignal: undefined,
625
+ });
626
+ // Token function was called at least once more
627
+ (0, vitest_1.expect)(tokenCallCount).toBeGreaterThan(firstCount);
628
+ });
629
+ });
630
+ (0, vitest_1.describe)("body merging", () => {
631
+ (0, vitest_1.it)("should merge ChatRequestOptions.body into the task payload", async () => {
632
+ const fetchSpy = vitest_1.vi.fn().mockImplementation(async (url) => {
633
+ const urlStr = typeof url === "string" ? url : url.toString();
634
+ if (urlStr.includes("/trigger")) {
635
+ return new Response(JSON.stringify({ id: "run_body" }), {
636
+ status: 200,
637
+ headers: {
638
+ "content-type": "application/json",
639
+ "x-trigger-jwt": "token",
640
+ },
641
+ });
642
+ }
643
+ if (urlStr.includes("/realtime/v1/streams/")) {
644
+ return new Response(createSSEStream(""), {
645
+ status: 200,
646
+ headers: {
647
+ "content-type": "text/event-stream",
648
+ "X-Stream-Version": "v1",
649
+ },
650
+ });
651
+ }
652
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
653
+ });
654
+ global.fetch = fetchSpy;
655
+ const transport = new chat_js_1.TriggerChatTransport({
656
+ task: "my-task",
657
+ accessToken: "token",
658
+ baseURL: "https://api.test.trigger.dev",
659
+ });
660
+ await transport.sendMessages({
661
+ trigger: "submit-message",
662
+ chatId: "chat-body",
663
+ messageId: undefined,
664
+ messages: [createUserMessage("test")],
665
+ abortSignal: undefined,
666
+ body: { systemPrompt: "You are helpful", temperature: 0.7 },
667
+ });
668
+ const triggerCall = fetchSpy.mock.calls.find((call) => (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger"));
669
+ const triggerBody = JSON.parse(triggerCall[1]?.body);
670
+ const payload = triggerBody.payload;
671
+ // body properties should be merged into the payload
672
+ (0, vitest_1.expect)(payload.systemPrompt).toBe("You are helpful");
673
+ (0, vitest_1.expect)(payload.temperature).toBe(0.7);
674
+ // Standard fields should still be present
675
+ (0, vitest_1.expect)(payload.chatId).toBe("chat-body");
676
+ (0, vitest_1.expect)(payload.trigger).toBe("submit-message");
677
+ });
678
+ });
679
+ (0, vitest_1.describe)("message types", () => {
680
+ (0, vitest_1.it)("should handle regenerate-message trigger", async () => {
681
+ const fetchSpy = vitest_1.vi.fn().mockImplementation(async (url) => {
682
+ const urlStr = typeof url === "string" ? url : url.toString();
683
+ if (urlStr.includes("/trigger")) {
684
+ return new Response(JSON.stringify({ id: "run_regen" }), {
685
+ status: 200,
686
+ headers: {
687
+ "content-type": "application/json",
688
+ "x-trigger-jwt": "token",
689
+ },
690
+ });
691
+ }
692
+ if (urlStr.includes("/realtime/v1/streams/")) {
693
+ return new Response(createSSEStream(""), {
694
+ status: 200,
695
+ headers: {
696
+ "content-type": "text/event-stream",
697
+ "X-Stream-Version": "v1",
698
+ },
699
+ });
700
+ }
701
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
702
+ });
703
+ global.fetch = fetchSpy;
704
+ const transport = new chat_js_1.TriggerChatTransport({
705
+ task: "my-task",
706
+ accessToken: "token",
707
+ baseURL: "https://api.test.trigger.dev",
708
+ });
709
+ const messages = [
710
+ createUserMessage("Hello!"),
711
+ createAssistantMessage("Hi there!"),
712
+ ];
713
+ await transport.sendMessages({
714
+ trigger: "regenerate-message",
715
+ chatId: "chat-regen",
716
+ messageId: "msg-to-regen",
717
+ messages,
718
+ abortSignal: undefined,
719
+ });
720
+ // Verify the payload includes the regenerate trigger type and messageId
721
+ const triggerCall = fetchSpy.mock.calls.find((call) => (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger"));
722
+ const triggerBody = JSON.parse(triggerCall[1]?.body);
723
+ const payload = triggerBody.payload;
724
+ (0, vitest_1.expect)(payload.trigger).toBe("regenerate-message");
725
+ (0, vitest_1.expect)(payload.messageId).toBe("msg-to-regen");
726
+ });
727
+ });
728
+ (0, vitest_1.describe)("lastEventId tracking", () => {
729
+ (0, vitest_1.it)("should pass lastEventId to SSE subscription on subsequent turns", async () => {
730
+ const controlChunk = {
731
+ type: "__trigger_waitpoint_ready",
732
+ tokenId: "wp_token_eid",
733
+ publicAccessToken: "wp_access_eid",
734
+ };
735
+ let triggerCallCount = 0;
736
+ const streamFetchCalls = [];
737
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
738
+ const urlStr = typeof url === "string" ? url : url.toString();
739
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
740
+ triggerCallCount++;
741
+ return new Response(JSON.stringify({ id: "run_eid" }), {
742
+ status: 200,
743
+ headers: {
744
+ "content-type": "application/json",
745
+ "x-trigger-jwt": "pub_token_eid",
746
+ },
747
+ });
748
+ }
749
+ if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) {
750
+ return new Response(JSON.stringify({ success: true }), {
751
+ status: 200,
752
+ headers: { "content-type": "application/json" },
753
+ });
754
+ }
755
+ if (urlStr.includes("/realtime/v1/streams/")) {
756
+ streamFetchCalls.push({
757
+ url: urlStr,
758
+ headers: init?.headers ?? {},
759
+ });
760
+ const chunks = [
761
+ ...sampleChunks,
762
+ { type: "finish", id: "part-1" },
763
+ controlChunk,
764
+ ];
765
+ return new Response(createSSEStream(sseEncode(chunks)), {
766
+ status: 200,
767
+ headers: {
768
+ "content-type": "text/event-stream",
769
+ "X-Stream-Version": "v1",
770
+ },
771
+ });
772
+ }
773
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
774
+ });
775
+ const transport = new chat_js_1.TriggerChatTransport({
776
+ task: "my-task",
777
+ accessToken: "token",
778
+ baseURL: "https://api.test.trigger.dev",
779
+ });
780
+ // First message — triggers a new run
781
+ const stream1 = await transport.sendMessages({
782
+ trigger: "submit-message",
783
+ chatId: "chat-eid",
784
+ messageId: undefined,
785
+ messages: [createUserMessage("Hello")],
786
+ abortSignal: undefined,
787
+ });
788
+ const reader1 = stream1.getReader();
789
+ while (true) {
790
+ const { done } = await reader1.read();
791
+ if (done)
792
+ break;
793
+ }
794
+ // Second message — completes the waitpoint
795
+ const stream2 = await transport.sendMessages({
796
+ trigger: "submit-message",
797
+ chatId: "chat-eid",
798
+ messageId: undefined,
799
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("What's up?")],
800
+ abortSignal: undefined,
801
+ });
802
+ const reader2 = stream2.getReader();
803
+ while (true) {
804
+ const { done } = await reader2.read();
805
+ if (done)
806
+ break;
807
+ }
808
+ // The second stream subscription should include a Last-Event-ID header
809
+ (0, vitest_1.expect)(streamFetchCalls.length).toBe(2);
810
+ const secondStreamHeaders = streamFetchCalls[1].headers;
811
+ // SSEStreamSubscription passes lastEventId as the Last-Event-ID header
812
+ (0, vitest_1.expect)(secondStreamHeaders["Last-Event-ID"]).toBeDefined();
813
+ });
814
+ });
815
+ (0, vitest_1.describe)("AbortController cleanup", () => {
816
+ (0, vitest_1.it)("should terminate SSE connection after intercepting control chunk", async () => {
817
+ const controlChunk = {
818
+ type: "__trigger_waitpoint_ready",
819
+ tokenId: "wp_token_abort",
820
+ publicAccessToken: "wp_access_abort",
821
+ };
822
+ let streamAborted = false;
823
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
824
+ const urlStr = typeof url === "string" ? url : url.toString();
825
+ if (urlStr.includes("/trigger")) {
826
+ return new Response(JSON.stringify({ id: "run_abort_cleanup" }), {
827
+ status: 200,
828
+ headers: {
829
+ "content-type": "application/json",
830
+ "x-trigger-jwt": "pub_token",
831
+ },
832
+ });
833
+ }
834
+ if (urlStr.includes("/realtime/v1/streams/")) {
835
+ // Track abort signal
836
+ const signal = init?.signal;
837
+ if (signal) {
838
+ signal.addEventListener("abort", () => {
839
+ streamAborted = true;
840
+ });
841
+ }
842
+ const chunks = [
843
+ ...sampleChunks,
844
+ { type: "finish", id: "part-1" },
845
+ controlChunk,
846
+ ];
847
+ return new Response(createSSEStream(sseEncode(chunks)), {
848
+ status: 200,
849
+ headers: {
850
+ "content-type": "text/event-stream",
851
+ "X-Stream-Version": "v1",
852
+ },
853
+ });
854
+ }
855
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
856
+ });
857
+ const transport = new chat_js_1.TriggerChatTransport({
858
+ task: "my-task",
859
+ accessToken: "token",
860
+ baseURL: "https://api.test.trigger.dev",
861
+ });
862
+ const stream = await transport.sendMessages({
863
+ trigger: "submit-message",
864
+ chatId: "chat-abort-cleanup",
865
+ messageId: undefined,
866
+ messages: [createUserMessage("Hello")],
867
+ abortSignal: undefined,
868
+ });
869
+ // Consume all chunks
870
+ const reader = stream.getReader();
871
+ while (true) {
872
+ const { done } = await reader.read();
873
+ if (done)
874
+ break;
875
+ }
876
+ // The internal AbortController should have aborted the fetch
877
+ (0, vitest_1.expect)(streamAborted).toBe(true);
878
+ });
879
+ });
880
+ (0, vitest_1.describe)("async accessToken", () => {
881
+ (0, vitest_1.it)("should accept an async function for accessToken", async () => {
882
+ let tokenCallCount = 0;
883
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
884
+ const urlStr = typeof url === "string" ? url : url.toString();
885
+ if (urlStr.includes("/trigger")) {
886
+ return new Response(JSON.stringify({ id: `run_async_${tokenCallCount}` }), {
887
+ status: 200,
888
+ headers: {
889
+ "content-type": "application/json",
890
+ "x-trigger-jwt": "stream-token",
891
+ },
892
+ });
893
+ }
894
+ if (urlStr.includes("/realtime/v1/streams/")) {
895
+ const chunks = [
896
+ { type: "text-start", id: "p1" },
897
+ { type: "text-end", id: "p1" },
898
+ ];
899
+ return new Response(createSSEStream(sseEncode(chunks)), {
900
+ status: 200,
901
+ headers: {
902
+ "content-type": "text/event-stream",
903
+ "X-Stream-Version": "v1",
904
+ },
905
+ });
906
+ }
907
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
908
+ });
909
+ const transport = new chat_js_1.TriggerChatTransport({
910
+ task: "my-task",
911
+ accessToken: async () => {
912
+ tokenCallCount++;
913
+ // Simulate async work (e.g. server action)
914
+ await new Promise((r) => setTimeout(r, 1));
915
+ return `async-token-${tokenCallCount}`;
916
+ },
917
+ baseURL: "https://api.test.trigger.dev",
918
+ });
919
+ await transport.sendMessages({
920
+ trigger: "submit-message",
921
+ chatId: "chat-async",
922
+ messageId: undefined,
923
+ messages: [createUserMessage("Hello")],
924
+ abortSignal: undefined,
925
+ });
926
+ (0, vitest_1.expect)(tokenCallCount).toBe(1);
927
+ });
928
+ (0, vitest_1.it)("should resolve async token for waitpoint completion flow", async () => {
929
+ const controlChunk = {
930
+ type: "__trigger_waitpoint_ready",
931
+ tokenId: "wp_token_async",
932
+ publicAccessToken: "wp_access_async",
933
+ };
934
+ let tokenCallCount = 0;
935
+ let completeWaitpointCalled = false;
936
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
937
+ const urlStr = typeof url === "string" ? url : url.toString();
938
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
939
+ return new Response(JSON.stringify({ id: "run_async_wp" }), {
940
+ status: 200,
941
+ headers: {
942
+ "content-type": "application/json",
943
+ "x-trigger-jwt": "stream-token",
944
+ },
945
+ });
946
+ }
947
+ if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) {
948
+ completeWaitpointCalled = true;
949
+ return new Response(JSON.stringify({ success: true }), {
950
+ status: 200,
951
+ headers: { "content-type": "application/json" },
952
+ });
953
+ }
954
+ if (urlStr.includes("/realtime/v1/streams/")) {
955
+ const chunks = [
956
+ ...sampleChunks,
957
+ { type: "finish", id: "part-1" },
958
+ controlChunk,
959
+ ];
960
+ return new Response(createSSEStream(sseEncode(chunks)), {
961
+ status: 200,
962
+ headers: {
963
+ "content-type": "text/event-stream",
964
+ "X-Stream-Version": "v1",
965
+ },
966
+ });
967
+ }
968
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
969
+ });
970
+ const transport = new chat_js_1.TriggerChatTransport({
971
+ task: "my-task",
972
+ accessToken: async () => {
973
+ tokenCallCount++;
974
+ await new Promise((r) => setTimeout(r, 1));
975
+ return `async-wp-token-${tokenCallCount}`;
976
+ },
977
+ baseURL: "https://api.test.trigger.dev",
978
+ });
979
+ // First message — triggers a new run (calls async token)
980
+ const stream1 = await transport.sendMessages({
981
+ trigger: "submit-message",
982
+ chatId: "chat-async-wp",
983
+ messageId: undefined,
984
+ messages: [createUserMessage("Hello")],
985
+ abortSignal: undefined,
986
+ });
987
+ const reader1 = stream1.getReader();
988
+ while (true) {
989
+ const { done } = await reader1.read();
990
+ if (done)
991
+ break;
992
+ }
993
+ const firstTokenCount = tokenCallCount;
994
+ // Second message — should complete waitpoint (does NOT call async token)
995
+ const stream2 = await transport.sendMessages({
996
+ trigger: "submit-message",
997
+ chatId: "chat-async-wp",
998
+ messageId: undefined,
999
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("More")],
1000
+ abortSignal: undefined,
1001
+ });
1002
+ const reader2 = stream2.getReader();
1003
+ while (true) {
1004
+ const { done } = await reader2.read();
1005
+ if (done)
1006
+ break;
1007
+ }
1008
+ // Token function should NOT have been called again for the waitpoint path
1009
+ (0, vitest_1.expect)(tokenCallCount).toBe(firstTokenCount);
1010
+ (0, vitest_1.expect)(completeWaitpointCalled).toBe(true);
1011
+ });
1012
+ });
1013
+ (0, vitest_1.describe)("single-run mode (waitpoint loop)", () => {
1014
+ (0, vitest_1.it)("should store waitpoint token from control chunk and not forward it to consumer", async () => {
1015
+ const controlChunk = {
1016
+ type: "__trigger_waitpoint_ready",
1017
+ tokenId: "wp_token_123",
1018
+ publicAccessToken: "wp_access_abc",
1019
+ };
1020
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1021
+ const urlStr = typeof url === "string" ? url : url.toString();
1022
+ if (urlStr.includes("/trigger")) {
1023
+ return new Response(JSON.stringify({ id: "run_single" }), {
1024
+ status: 200,
1025
+ headers: {
1026
+ "content-type": "application/json",
1027
+ "x-trigger-jwt": "pub_token",
1028
+ },
1029
+ });
1030
+ }
1031
+ if (urlStr.includes("/realtime/v1/streams/")) {
1032
+ const chunks = [
1033
+ ...sampleChunks,
1034
+ { type: "finish", id: "part-1" },
1035
+ controlChunk,
1036
+ ];
1037
+ return new Response(createSSEStream(sseEncode(chunks)), {
1038
+ status: 200,
1039
+ headers: {
1040
+ "content-type": "text/event-stream",
1041
+ "X-Stream-Version": "v1",
1042
+ },
1043
+ });
1044
+ }
1045
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1046
+ });
1047
+ const transport = new chat_js_1.TriggerChatTransport({
1048
+ task: "my-task",
1049
+ accessToken: "token",
1050
+ baseURL: "https://api.test.trigger.dev",
1051
+ });
1052
+ const stream = await transport.sendMessages({
1053
+ trigger: "submit-message",
1054
+ chatId: "chat-single",
1055
+ messageId: undefined,
1056
+ messages: [createUserMessage("Hello")],
1057
+ abortSignal: undefined,
1058
+ });
1059
+ // Read all chunks — the control chunk should NOT appear
1060
+ const reader = stream.getReader();
1061
+ const receivedChunks = [];
1062
+ while (true) {
1063
+ const { done, value } = await reader.read();
1064
+ if (done)
1065
+ break;
1066
+ receivedChunks.push(value);
1067
+ }
1068
+ // All AI SDK chunks should be forwarded
1069
+ (0, vitest_1.expect)(receivedChunks.length).toBe(sampleChunks.length + 1); // +1 for the finish chunk
1070
+ // Control chunk should not be in the output
1071
+ (0, vitest_1.expect)(receivedChunks.every((c) => c.type !== "__trigger_waitpoint_ready")).toBe(true);
1072
+ });
1073
+ (0, vitest_1.it)("should complete waitpoint token on second message instead of triggering a new run", async () => {
1074
+ const controlChunk = {
1075
+ type: "__trigger_waitpoint_ready",
1076
+ tokenId: "wp_token_456",
1077
+ publicAccessToken: "wp_access_def",
1078
+ };
1079
+ let triggerCallCount = 0;
1080
+ let completeWaitpointCalled = false;
1081
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
1082
+ const urlStr = typeof url === "string" ? url : url.toString();
1083
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
1084
+ triggerCallCount++;
1085
+ return new Response(JSON.stringify({ id: "run_resume" }), {
1086
+ status: 200,
1087
+ headers: {
1088
+ "content-type": "application/json",
1089
+ "x-trigger-jwt": "pub_token",
1090
+ },
1091
+ });
1092
+ }
1093
+ // Handle waitpoint token completion
1094
+ if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) {
1095
+ completeWaitpointCalled = true;
1096
+ return new Response(JSON.stringify({ success: true }), {
1097
+ status: 200,
1098
+ headers: { "content-type": "application/json" },
1099
+ });
1100
+ }
1101
+ if (urlStr.includes("/realtime/v1/streams/")) {
1102
+ const chunks = [
1103
+ ...sampleChunks,
1104
+ { type: "finish", id: "part-1" },
1105
+ controlChunk,
1106
+ ];
1107
+ return new Response(createSSEStream(sseEncode(chunks)), {
1108
+ status: 200,
1109
+ headers: {
1110
+ "content-type": "text/event-stream",
1111
+ "X-Stream-Version": "v1",
1112
+ },
1113
+ });
1114
+ }
1115
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1116
+ });
1117
+ const transport = new chat_js_1.TriggerChatTransport({
1118
+ task: "my-task",
1119
+ accessToken: "token",
1120
+ baseURL: "https://api.test.trigger.dev",
1121
+ });
1122
+ // First message — triggers a new run
1123
+ const stream1 = await transport.sendMessages({
1124
+ trigger: "submit-message",
1125
+ chatId: "chat-resume",
1126
+ messageId: undefined,
1127
+ messages: [createUserMessage("Hello")],
1128
+ abortSignal: undefined,
1129
+ });
1130
+ // Consume stream to capture the control chunk
1131
+ const reader1 = stream1.getReader();
1132
+ while (true) {
1133
+ const { done } = await reader1.read();
1134
+ if (done)
1135
+ break;
1136
+ }
1137
+ (0, vitest_1.expect)(triggerCallCount).toBe(1);
1138
+ // Second message — should complete the waitpoint instead of triggering
1139
+ const stream2 = await transport.sendMessages({
1140
+ trigger: "submit-message",
1141
+ chatId: "chat-resume",
1142
+ messageId: undefined,
1143
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("How are you?")],
1144
+ abortSignal: undefined,
1145
+ });
1146
+ // Consume second stream
1147
+ const reader2 = stream2.getReader();
1148
+ while (true) {
1149
+ const { done } = await reader2.read();
1150
+ if (done)
1151
+ break;
1152
+ }
1153
+ // Should NOT have triggered a second run
1154
+ (0, vitest_1.expect)(triggerCallCount).toBe(1);
1155
+ // Should have completed the waitpoint
1156
+ (0, vitest_1.expect)(completeWaitpointCalled).toBe(true);
1157
+ });
1158
+ (0, vitest_1.it)("should fall back to triggering a new run if stream closes without control chunk", async () => {
1159
+ let triggerCallCount = 0;
1160
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1161
+ const urlStr = typeof url === "string" ? url : url.toString();
1162
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
1163
+ triggerCallCount++;
1164
+ return new Response(JSON.stringify({ id: `run_fallback_${triggerCallCount}` }), {
1165
+ status: 200,
1166
+ headers: {
1167
+ "content-type": "application/json",
1168
+ "x-trigger-jwt": "pub_token",
1169
+ },
1170
+ });
1171
+ }
1172
+ if (urlStr.includes("/realtime/v1/streams/")) {
1173
+ // No control chunk — stream just ends after the finish
1174
+ const chunks = [
1175
+ { type: "text-start", id: "p1" },
1176
+ { type: "text-delta", id: "p1", delta: "Hello" },
1177
+ { type: "text-end", id: "p1" },
1178
+ ];
1179
+ return new Response(createSSEStream(sseEncode(chunks)), {
1180
+ status: 200,
1181
+ headers: {
1182
+ "content-type": "text/event-stream",
1183
+ "X-Stream-Version": "v1",
1184
+ },
1185
+ });
1186
+ }
1187
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1188
+ });
1189
+ const transport = new chat_js_1.TriggerChatTransport({
1190
+ task: "my-task",
1191
+ accessToken: "token",
1192
+ baseURL: "https://api.test.trigger.dev",
1193
+ });
1194
+ // First message
1195
+ const stream1 = await transport.sendMessages({
1196
+ trigger: "submit-message",
1197
+ chatId: "chat-fallback",
1198
+ messageId: undefined,
1199
+ messages: [createUserMessage("Hello")],
1200
+ abortSignal: undefined,
1201
+ });
1202
+ const reader1 = stream1.getReader();
1203
+ while (true) {
1204
+ const { done } = await reader1.read();
1205
+ if (done)
1206
+ break;
1207
+ }
1208
+ (0, vitest_1.expect)(triggerCallCount).toBe(1);
1209
+ // Second message — no waitpoint token stored, should trigger a new run
1210
+ await transport.sendMessages({
1211
+ trigger: "submit-message",
1212
+ chatId: "chat-fallback",
1213
+ messageId: undefined,
1214
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("Again")],
1215
+ abortSignal: undefined,
1216
+ });
1217
+ // Should have triggered a second run
1218
+ (0, vitest_1.expect)(triggerCallCount).toBe(2);
1219
+ });
1220
+ (0, vitest_1.it)("should fall back to new run when completing waitpoint fails", async () => {
1221
+ const controlChunk = {
1222
+ type: "__trigger_waitpoint_ready",
1223
+ tokenId: "wp_token_fail",
1224
+ publicAccessToken: "wp_access_fail",
1225
+ };
1226
+ let triggerCallCount = 0;
1227
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1228
+ const urlStr = typeof url === "string" ? url : url.toString();
1229
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
1230
+ triggerCallCount++;
1231
+ return new Response(JSON.stringify({ id: `run_fail_${triggerCallCount}` }), {
1232
+ status: 200,
1233
+ headers: {
1234
+ "content-type": "application/json",
1235
+ "x-trigger-jwt": "pub_token",
1236
+ },
1237
+ });
1238
+ }
1239
+ // Waitpoint completion fails
1240
+ if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) {
1241
+ return new Response(JSON.stringify({ error: "Token expired" }), {
1242
+ status: 400,
1243
+ headers: { "content-type": "application/json" },
1244
+ });
1245
+ }
1246
+ if (urlStr.includes("/realtime/v1/streams/")) {
1247
+ // First call has control chunk, subsequent calls don't
1248
+ const chunks = [
1249
+ ...sampleChunks,
1250
+ { type: "finish", id: "part-1" },
1251
+ ];
1252
+ if (triggerCallCount <= 1) {
1253
+ chunks.push(controlChunk);
1254
+ }
1255
+ return new Response(createSSEStream(sseEncode(chunks)), {
1256
+ status: 200,
1257
+ headers: {
1258
+ "content-type": "text/event-stream",
1259
+ "X-Stream-Version": "v1",
1260
+ },
1261
+ });
1262
+ }
1263
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1264
+ });
1265
+ const transport = new chat_js_1.TriggerChatTransport({
1266
+ task: "my-task",
1267
+ accessToken: "token",
1268
+ baseURL: "https://api.test.trigger.dev",
1269
+ });
1270
+ // First message
1271
+ const stream1 = await transport.sendMessages({
1272
+ trigger: "submit-message",
1273
+ chatId: "chat-fail",
1274
+ messageId: undefined,
1275
+ messages: [createUserMessage("Hello")],
1276
+ abortSignal: undefined,
1277
+ });
1278
+ const reader1 = stream1.getReader();
1279
+ while (true) {
1280
+ const { done } = await reader1.read();
1281
+ if (done)
1282
+ break;
1283
+ }
1284
+ (0, vitest_1.expect)(triggerCallCount).toBe(1);
1285
+ // Second message — waitpoint completion will fail, should fall back to new run
1286
+ const stream2 = await transport.sendMessages({
1287
+ trigger: "submit-message",
1288
+ chatId: "chat-fail",
1289
+ messageId: undefined,
1290
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("Again")],
1291
+ abortSignal: undefined,
1292
+ });
1293
+ const reader2 = stream2.getReader();
1294
+ while (true) {
1295
+ const { done } = await reader2.read();
1296
+ if (done)
1297
+ break;
1298
+ }
1299
+ // Should have triggered a second run as fallback
1300
+ (0, vitest_1.expect)(triggerCallCount).toBe(2);
1301
+ });
1302
+ });
1303
+ });
1304
+ //# sourceMappingURL=chat.test.js.map