@trigger.dev/sdk 0.0.0-prerelease-20260302145933 → 0.0.0-prerelease-20260305142821

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 (38) hide show
  1. package/dist/commonjs/v3/ai.d.ts +347 -2
  2. package/dist/commonjs/v3/ai.js +563 -1
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat-constants.d.ts +10 -0
  5. package/dist/commonjs/v3/chat-constants.js +14 -0
  6. package/dist/commonjs/v3/chat-constants.js.map +1 -0
  7. package/dist/commonjs/v3/chat-react.d.ts +45 -0
  8. package/dist/commonjs/v3/chat-react.js +71 -0
  9. package/dist/commonjs/v3/chat-react.js.map +1 -0
  10. package/dist/commonjs/v3/chat.d.ts +241 -0
  11. package/dist/commonjs/v3/chat.js +343 -0
  12. package/dist/commonjs/v3/chat.js.map +1 -0
  13. package/dist/commonjs/v3/chat.test.d.ts +1 -0
  14. package/dist/commonjs/v3/chat.test.js +1557 -0
  15. package/dist/commonjs/v3/chat.test.js.map +1 -0
  16. package/dist/commonjs/v3/runs.d.ts +3 -3
  17. package/dist/commonjs/v3/streams.js +27 -17
  18. package/dist/commonjs/v3/streams.js.map +1 -1
  19. package/dist/commonjs/version.js +1 -1
  20. package/dist/esm/v3/ai.d.ts +347 -2
  21. package/dist/esm/v3/ai.js +564 -2
  22. package/dist/esm/v3/ai.js.map +1 -1
  23. package/dist/esm/v3/chat-constants.d.ts +10 -0
  24. package/dist/esm/v3/chat-constants.js +11 -0
  25. package/dist/esm/v3/chat-constants.js.map +1 -0
  26. package/dist/esm/v3/chat-react.d.ts +45 -0
  27. package/dist/esm/v3/chat-react.js +68 -0
  28. package/dist/esm/v3/chat-react.js.map +1 -0
  29. package/dist/esm/v3/chat.d.ts +241 -0
  30. package/dist/esm/v3/chat.js +338 -0
  31. package/dist/esm/v3/chat.js.map +1 -0
  32. package/dist/esm/v3/chat.test.d.ts +1 -0
  33. package/dist/esm/v3/chat.test.js +1555 -0
  34. package/dist/esm/v3/chat.test.js.map +1 -0
  35. package/dist/esm/v3/streams.js +27 -17
  36. package/dist/esm/v3/streams.js.map +1 -1
  37. package/dist/esm/version.js +1 -1
  38. package/package.json +40 -5
@@ -0,0 +1,1557 @@
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
+ const turnCompleteChunk = { type: "__trigger_turn_complete" };
520
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
521
+ const urlStr = typeof url === "string" ? url : url.toString();
522
+ if (urlStr.includes("/trigger")) {
523
+ callCount++;
524
+ return new Response(JSON.stringify({ id: `run_multi_${callCount}` }), {
525
+ status: 200,
526
+ headers: {
527
+ "content-type": "application/json",
528
+ "x-trigger-jwt": `token_${callCount}`,
529
+ },
530
+ });
531
+ }
532
+ if (urlStr.includes("/realtime/v1/streams/")) {
533
+ // Include turn-complete chunk so the session is preserved
534
+ const chunks = [...sampleChunks, turnCompleteChunk];
535
+ return new Response(createSSEStream(sseEncode(chunks)), {
536
+ status: 200,
537
+ headers: {
538
+ "content-type": "text/event-stream",
539
+ "X-Stream-Version": "v1",
540
+ },
541
+ });
542
+ }
543
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
544
+ });
545
+ const transport = new chat_js_1.TriggerChatTransport({
546
+ task: "my-task",
547
+ accessToken: "token",
548
+ baseURL: "https://api.test.trigger.dev",
549
+ });
550
+ // Start two independent chat sessions and consume the streams
551
+ const s1 = await transport.sendMessages({
552
+ trigger: "submit-message",
553
+ chatId: "session-a",
554
+ messageId: undefined,
555
+ messages: [createUserMessage("Hello A")],
556
+ abortSignal: undefined,
557
+ });
558
+ const r1 = s1.getReader();
559
+ while (!(await r1.read()).done) { }
560
+ const s2 = await transport.sendMessages({
561
+ trigger: "submit-message",
562
+ chatId: "session-b",
563
+ messageId: undefined,
564
+ messages: [createUserMessage("Hello B")],
565
+ abortSignal: undefined,
566
+ });
567
+ const r2 = s2.getReader();
568
+ while (!(await r2.read()).done) { }
569
+ // Both sessions should be independently reconnectable
570
+ const streamA = await transport.reconnectToStream({ chatId: "session-a" });
571
+ const streamB = await transport.reconnectToStream({ chatId: "session-b" });
572
+ const streamC = await transport.reconnectToStream({ chatId: "nonexistent" });
573
+ (0, vitest_1.expect)(streamA).toBeInstanceOf(ReadableStream);
574
+ (0, vitest_1.expect)(streamB).toBeInstanceOf(ReadableStream);
575
+ (0, vitest_1.expect)(streamC).toBeNull();
576
+ });
577
+ });
578
+ (0, vitest_1.describe)("dynamic accessToken", () => {
579
+ (0, vitest_1.it)("should call the accessToken function for each sendMessages call", async () => {
580
+ let tokenCallCount = 0;
581
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
582
+ const urlStr = typeof url === "string" ? url : url.toString();
583
+ if (urlStr.includes("/trigger")) {
584
+ return new Response(JSON.stringify({ id: `run_dyn_${tokenCallCount}` }), {
585
+ status: 200,
586
+ headers: {
587
+ "content-type": "application/json",
588
+ "x-trigger-jwt": "stream-token",
589
+ },
590
+ });
591
+ }
592
+ if (urlStr.includes("/realtime/v1/streams/")) {
593
+ const chunks = [
594
+ { type: "text-start", id: "p1" },
595
+ { type: "text-end", id: "p1" },
596
+ ];
597
+ return new Response(createSSEStream(sseEncode(chunks)), {
598
+ status: 200,
599
+ headers: {
600
+ "content-type": "text/event-stream",
601
+ "X-Stream-Version": "v1",
602
+ },
603
+ });
604
+ }
605
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
606
+ });
607
+ const transport = new chat_js_1.TriggerChatTransport({
608
+ task: "my-task",
609
+ accessToken: () => {
610
+ tokenCallCount++;
611
+ return `dynamic-token-${tokenCallCount}`;
612
+ },
613
+ baseURL: "https://api.test.trigger.dev",
614
+ });
615
+ // First call — the token function should be invoked
616
+ await transport.sendMessages({
617
+ trigger: "submit-message",
618
+ chatId: "chat-dyn-1",
619
+ messageId: undefined,
620
+ messages: [createUserMessage("first")],
621
+ abortSignal: undefined,
622
+ });
623
+ const firstCount = tokenCallCount;
624
+ (0, vitest_1.expect)(firstCount).toBeGreaterThanOrEqual(1);
625
+ // Second call — the token function should be invoked again
626
+ await transport.sendMessages({
627
+ trigger: "submit-message",
628
+ chatId: "chat-dyn-2",
629
+ messageId: undefined,
630
+ messages: [createUserMessage("second")],
631
+ abortSignal: undefined,
632
+ });
633
+ // Token function was called at least once more
634
+ (0, vitest_1.expect)(tokenCallCount).toBeGreaterThan(firstCount);
635
+ });
636
+ });
637
+ (0, vitest_1.describe)("body merging", () => {
638
+ (0, vitest_1.it)("should merge ChatRequestOptions.body into the task payload", async () => {
639
+ const fetchSpy = vitest_1.vi.fn().mockImplementation(async (url) => {
640
+ const urlStr = typeof url === "string" ? url : url.toString();
641
+ if (urlStr.includes("/trigger")) {
642
+ return new Response(JSON.stringify({ id: "run_body" }), {
643
+ status: 200,
644
+ headers: {
645
+ "content-type": "application/json",
646
+ "x-trigger-jwt": "token",
647
+ },
648
+ });
649
+ }
650
+ if (urlStr.includes("/realtime/v1/streams/")) {
651
+ return new Response(createSSEStream(""), {
652
+ status: 200,
653
+ headers: {
654
+ "content-type": "text/event-stream",
655
+ "X-Stream-Version": "v1",
656
+ },
657
+ });
658
+ }
659
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
660
+ });
661
+ global.fetch = fetchSpy;
662
+ const transport = new chat_js_1.TriggerChatTransport({
663
+ task: "my-task",
664
+ accessToken: "token",
665
+ baseURL: "https://api.test.trigger.dev",
666
+ });
667
+ await transport.sendMessages({
668
+ trigger: "submit-message",
669
+ chatId: "chat-body",
670
+ messageId: undefined,
671
+ messages: [createUserMessage("test")],
672
+ abortSignal: undefined,
673
+ body: { systemPrompt: "You are helpful", temperature: 0.7 },
674
+ });
675
+ const triggerCall = fetchSpy.mock.calls.find((call) => (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger"));
676
+ const triggerBody = JSON.parse(triggerCall[1]?.body);
677
+ const payload = triggerBody.payload;
678
+ // body properties should be merged into the payload
679
+ (0, vitest_1.expect)(payload.systemPrompt).toBe("You are helpful");
680
+ (0, vitest_1.expect)(payload.temperature).toBe(0.7);
681
+ // Standard fields should still be present
682
+ (0, vitest_1.expect)(payload.chatId).toBe("chat-body");
683
+ (0, vitest_1.expect)(payload.trigger).toBe("submit-message");
684
+ });
685
+ });
686
+ (0, vitest_1.describe)("message types", () => {
687
+ (0, vitest_1.it)("should handle regenerate-message trigger", async () => {
688
+ const fetchSpy = vitest_1.vi.fn().mockImplementation(async (url) => {
689
+ const urlStr = typeof url === "string" ? url : url.toString();
690
+ if (urlStr.includes("/trigger")) {
691
+ return new Response(JSON.stringify({ id: "run_regen" }), {
692
+ status: 200,
693
+ headers: {
694
+ "content-type": "application/json",
695
+ "x-trigger-jwt": "token",
696
+ },
697
+ });
698
+ }
699
+ if (urlStr.includes("/realtime/v1/streams/")) {
700
+ return new Response(createSSEStream(""), {
701
+ status: 200,
702
+ headers: {
703
+ "content-type": "text/event-stream",
704
+ "X-Stream-Version": "v1",
705
+ },
706
+ });
707
+ }
708
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
709
+ });
710
+ global.fetch = fetchSpy;
711
+ const transport = new chat_js_1.TriggerChatTransport({
712
+ task: "my-task",
713
+ accessToken: "token",
714
+ baseURL: "https://api.test.trigger.dev",
715
+ });
716
+ const messages = [
717
+ createUserMessage("Hello!"),
718
+ createAssistantMessage("Hi there!"),
719
+ ];
720
+ await transport.sendMessages({
721
+ trigger: "regenerate-message",
722
+ chatId: "chat-regen",
723
+ messageId: "msg-to-regen",
724
+ messages,
725
+ abortSignal: undefined,
726
+ });
727
+ // Verify the payload includes the regenerate trigger type and messageId
728
+ const triggerCall = fetchSpy.mock.calls.find((call) => (typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger"));
729
+ const triggerBody = JSON.parse(triggerCall[1]?.body);
730
+ const payload = triggerBody.payload;
731
+ (0, vitest_1.expect)(payload.trigger).toBe("regenerate-message");
732
+ (0, vitest_1.expect)(payload.messageId).toBe("msg-to-regen");
733
+ });
734
+ });
735
+ (0, vitest_1.describe)("lastEventId tracking", () => {
736
+ (0, vitest_1.it)("should pass lastEventId to SSE subscription on subsequent turns", async () => {
737
+ const turnCompleteChunk = { type: "__trigger_turn_complete" };
738
+ let triggerCallCount = 0;
739
+ const streamFetchCalls = [];
740
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
741
+ const urlStr = typeof url === "string" ? url : url.toString();
742
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
743
+ triggerCallCount++;
744
+ return new Response(JSON.stringify({ id: "run_eid" }), {
745
+ status: 200,
746
+ headers: {
747
+ "content-type": "application/json",
748
+ "x-trigger-jwt": "pub_token_eid",
749
+ },
750
+ });
751
+ }
752
+ // Handle input stream sends (for second message)
753
+ if (urlStr.includes("/realtime/v1/streams/") && urlStr.includes("/input/")) {
754
+ return new Response(JSON.stringify({ ok: true }), {
755
+ status: 200,
756
+ headers: { "content-type": "application/json" },
757
+ });
758
+ }
759
+ if (urlStr.includes("/realtime/v1/streams/")) {
760
+ streamFetchCalls.push({
761
+ url: urlStr,
762
+ headers: init?.headers ?? {},
763
+ });
764
+ const chunks = [
765
+ ...sampleChunks,
766
+ { type: "finish", id: "part-1" },
767
+ turnCompleteChunk,
768
+ ];
769
+ return new Response(createSSEStream(sseEncode(chunks)), {
770
+ status: 200,
771
+ headers: {
772
+ "content-type": "text/event-stream",
773
+ "X-Stream-Version": "v1",
774
+ },
775
+ });
776
+ }
777
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
778
+ });
779
+ const transport = new chat_js_1.TriggerChatTransport({
780
+ task: "my-task",
781
+ accessToken: "token",
782
+ baseURL: "https://api.test.trigger.dev",
783
+ });
784
+ // First message — triggers a new run
785
+ const stream1 = await transport.sendMessages({
786
+ trigger: "submit-message",
787
+ chatId: "chat-eid",
788
+ messageId: undefined,
789
+ messages: [createUserMessage("Hello")],
790
+ abortSignal: undefined,
791
+ });
792
+ const reader1 = stream1.getReader();
793
+ while (true) {
794
+ const { done } = await reader1.read();
795
+ if (done)
796
+ break;
797
+ }
798
+ // Second message — sends via input stream
799
+ const stream2 = await transport.sendMessages({
800
+ trigger: "submit-message",
801
+ chatId: "chat-eid",
802
+ messageId: undefined,
803
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("What's up?")],
804
+ abortSignal: undefined,
805
+ });
806
+ const reader2 = stream2.getReader();
807
+ while (true) {
808
+ const { done } = await reader2.read();
809
+ if (done)
810
+ break;
811
+ }
812
+ // The second stream subscription should include a Last-Event-ID header
813
+ (0, vitest_1.expect)(streamFetchCalls.length).toBe(2);
814
+ const secondStreamHeaders = streamFetchCalls[1].headers;
815
+ // SSEStreamSubscription passes lastEventId as the Last-Event-ID header
816
+ (0, vitest_1.expect)(secondStreamHeaders["Last-Event-ID"]).toBeDefined();
817
+ });
818
+ });
819
+ (0, vitest_1.describe)("minimal wire payloads", () => {
820
+ (0, vitest_1.it)("should send only new messages via input stream on turn 2+", async () => {
821
+ const turnCompleteChunk = { type: "__trigger_turn_complete" };
822
+ const inputStreamPayloads = [];
823
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
824
+ const urlStr = typeof url === "string" ? url : url.toString();
825
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
826
+ return new Response(JSON.stringify({ id: "run_minimal" }), {
827
+ status: 200,
828
+ headers: {
829
+ "content-type": "application/json",
830
+ "x-trigger-jwt": "pub_token_minimal",
831
+ },
832
+ });
833
+ }
834
+ // Capture input stream payloads (ApiClient wraps in { data: ... })
835
+ if (urlStr.includes("/realtime/v1/streams/") && urlStr.includes("/input/")) {
836
+ const body = JSON.parse(init?.body);
837
+ inputStreamPayloads.push(body.data);
838
+ return new Response(JSON.stringify({ ok: true }), {
839
+ status: 200,
840
+ headers: { "content-type": "application/json" },
841
+ });
842
+ }
843
+ if (urlStr.includes("/realtime/v1/streams/")) {
844
+ const chunks = [
845
+ ...sampleChunks,
846
+ turnCompleteChunk,
847
+ ];
848
+ return new Response(createSSEStream(sseEncode(chunks)), {
849
+ status: 200,
850
+ headers: {
851
+ "content-type": "text/event-stream",
852
+ "X-Stream-Version": "v1",
853
+ },
854
+ });
855
+ }
856
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
857
+ });
858
+ const transport = new chat_js_1.TriggerChatTransport({
859
+ task: "my-task",
860
+ accessToken: "token",
861
+ baseURL: "https://api.test.trigger.dev",
862
+ });
863
+ const userMsg1 = createUserMessage("Hello");
864
+ const assistantMsg = createAssistantMessage("Hi there!");
865
+ const userMsg2 = createUserMessage("What's up?");
866
+ // Turn 1 — triggers a new run with full history
867
+ const stream1 = await transport.sendMessages({
868
+ trigger: "submit-message",
869
+ chatId: "chat-minimal",
870
+ messageId: undefined,
871
+ messages: [userMsg1],
872
+ abortSignal: undefined,
873
+ });
874
+ const r1 = stream1.getReader();
875
+ while (!(await r1.read()).done) { }
876
+ // Turn 2 — sends via input stream, should only include NEW messages
877
+ const stream2 = await transport.sendMessages({
878
+ trigger: "submit-message",
879
+ chatId: "chat-minimal",
880
+ messageId: undefined,
881
+ messages: [userMsg1, assistantMsg, userMsg2],
882
+ abortSignal: undefined,
883
+ });
884
+ const r2 = stream2.getReader();
885
+ while (!(await r2.read()).done) { }
886
+ // Verify: the input stream payload should only contain the new user message
887
+ (0, vitest_1.expect)(inputStreamPayloads).toHaveLength(1);
888
+ const sentPayload = inputStreamPayloads[0];
889
+ // Only the new user message should be sent (backend already has the assistant response)
890
+ (0, vitest_1.expect)(sentPayload.messages).toHaveLength(1);
891
+ (0, vitest_1.expect)(sentPayload.messages[0]).toEqual(userMsg2);
892
+ });
893
+ (0, vitest_1.it)("should send full history on first message (trigger)", async () => {
894
+ let triggerPayload;
895
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
896
+ const urlStr = typeof url === "string" ? url : url.toString();
897
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
898
+ triggerPayload = JSON.parse(init?.body);
899
+ return new Response(JSON.stringify({ id: "run_full" }), {
900
+ status: 200,
901
+ headers: {
902
+ "content-type": "application/json",
903
+ "x-trigger-jwt": "pub_token_full",
904
+ },
905
+ });
906
+ }
907
+ if (urlStr.includes("/realtime/v1/streams/")) {
908
+ return new Response(createSSEStream(sseEncode(sampleChunks)), {
909
+ status: 200,
910
+ headers: {
911
+ "content-type": "text/event-stream",
912
+ "X-Stream-Version": "v1",
913
+ },
914
+ });
915
+ }
916
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
917
+ });
918
+ const transport = new chat_js_1.TriggerChatTransport({
919
+ task: "my-task",
920
+ accessToken: "token",
921
+ baseURL: "https://api.test.trigger.dev",
922
+ });
923
+ const messages = [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("More")];
924
+ await transport.sendMessages({
925
+ trigger: "submit-message",
926
+ chatId: "chat-full",
927
+ messageId: undefined,
928
+ messages,
929
+ abortSignal: undefined,
930
+ });
931
+ // First message always sends full history via trigger
932
+ (0, vitest_1.expect)(triggerPayload.payload.messages).toHaveLength(3);
933
+ });
934
+ });
935
+ (0, vitest_1.describe)("AbortController cleanup", () => {
936
+ (0, vitest_1.it)("should terminate SSE connection after intercepting control chunk", async () => {
937
+ const controlChunk = { type: "__trigger_turn_complete" };
938
+ let streamAborted = false;
939
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
940
+ const urlStr = typeof url === "string" ? url : url.toString();
941
+ if (urlStr.includes("/trigger")) {
942
+ return new Response(JSON.stringify({ id: "run_abort_cleanup" }), {
943
+ status: 200,
944
+ headers: {
945
+ "content-type": "application/json",
946
+ "x-trigger-jwt": "pub_token",
947
+ },
948
+ });
949
+ }
950
+ if (urlStr.includes("/realtime/v1/streams/")) {
951
+ // Track abort signal
952
+ const signal = init?.signal;
953
+ if (signal) {
954
+ signal.addEventListener("abort", () => {
955
+ streamAborted = true;
956
+ });
957
+ }
958
+ const chunks = [
959
+ ...sampleChunks,
960
+ { type: "finish", id: "part-1" },
961
+ controlChunk,
962
+ ];
963
+ return new Response(createSSEStream(sseEncode(chunks)), {
964
+ status: 200,
965
+ headers: {
966
+ "content-type": "text/event-stream",
967
+ "X-Stream-Version": "v1",
968
+ },
969
+ });
970
+ }
971
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
972
+ });
973
+ const transport = new chat_js_1.TriggerChatTransport({
974
+ task: "my-task",
975
+ accessToken: "token",
976
+ baseURL: "https://api.test.trigger.dev",
977
+ });
978
+ const stream = await transport.sendMessages({
979
+ trigger: "submit-message",
980
+ chatId: "chat-abort-cleanup",
981
+ messageId: undefined,
982
+ messages: [createUserMessage("Hello")],
983
+ abortSignal: undefined,
984
+ });
985
+ // Consume all chunks
986
+ const reader = stream.getReader();
987
+ while (true) {
988
+ const { done } = await reader.read();
989
+ if (done)
990
+ break;
991
+ }
992
+ // The internal AbortController should have aborted the fetch
993
+ (0, vitest_1.expect)(streamAborted).toBe(true);
994
+ });
995
+ });
996
+ (0, vitest_1.describe)("async accessToken", () => {
997
+ (0, vitest_1.it)("should accept an async function for accessToken", async () => {
998
+ let tokenCallCount = 0;
999
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1000
+ const urlStr = typeof url === "string" ? url : url.toString();
1001
+ if (urlStr.includes("/trigger")) {
1002
+ return new Response(JSON.stringify({ id: `run_async_${tokenCallCount}` }), {
1003
+ status: 200,
1004
+ headers: {
1005
+ "content-type": "application/json",
1006
+ "x-trigger-jwt": "stream-token",
1007
+ },
1008
+ });
1009
+ }
1010
+ if (urlStr.includes("/realtime/v1/streams/")) {
1011
+ const chunks = [
1012
+ { type: "text-start", id: "p1" },
1013
+ { type: "text-end", id: "p1" },
1014
+ ];
1015
+ return new Response(createSSEStream(sseEncode(chunks)), {
1016
+ status: 200,
1017
+ headers: {
1018
+ "content-type": "text/event-stream",
1019
+ "X-Stream-Version": "v1",
1020
+ },
1021
+ });
1022
+ }
1023
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1024
+ });
1025
+ const transport = new chat_js_1.TriggerChatTransport({
1026
+ task: "my-task",
1027
+ accessToken: async () => {
1028
+ tokenCallCount++;
1029
+ // Simulate async work (e.g. server action)
1030
+ await new Promise((r) => setTimeout(r, 1));
1031
+ return `async-token-${tokenCallCount}`;
1032
+ },
1033
+ baseURL: "https://api.test.trigger.dev",
1034
+ });
1035
+ await transport.sendMessages({
1036
+ trigger: "submit-message",
1037
+ chatId: "chat-async",
1038
+ messageId: undefined,
1039
+ messages: [createUserMessage("Hello")],
1040
+ abortSignal: undefined,
1041
+ });
1042
+ (0, vitest_1.expect)(tokenCallCount).toBe(1);
1043
+ });
1044
+ (0, vitest_1.it)("should not resolve async token for input stream send flow", async () => {
1045
+ const turnCompleteChunk = { type: "__trigger_turn_complete" };
1046
+ let tokenCallCount = 0;
1047
+ let inputStreamSendCalled = false;
1048
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1049
+ const urlStr = typeof url === "string" ? url : url.toString();
1050
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
1051
+ return new Response(JSON.stringify({ id: "run_async_wp" }), {
1052
+ status: 200,
1053
+ headers: {
1054
+ "content-type": "application/json",
1055
+ "x-trigger-jwt": "stream-token",
1056
+ },
1057
+ });
1058
+ }
1059
+ // Handle input stream sends
1060
+ if (urlStr.includes("/realtime/v1/streams/") && urlStr.includes("/input/")) {
1061
+ inputStreamSendCalled = true;
1062
+ return new Response(JSON.stringify({ ok: true }), {
1063
+ status: 200,
1064
+ headers: { "content-type": "application/json" },
1065
+ });
1066
+ }
1067
+ if (urlStr.includes("/realtime/v1/streams/")) {
1068
+ const chunks = [
1069
+ ...sampleChunks,
1070
+ { type: "finish", id: "part-1" },
1071
+ turnCompleteChunk,
1072
+ ];
1073
+ return new Response(createSSEStream(sseEncode(chunks)), {
1074
+ status: 200,
1075
+ headers: {
1076
+ "content-type": "text/event-stream",
1077
+ "X-Stream-Version": "v1",
1078
+ },
1079
+ });
1080
+ }
1081
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1082
+ });
1083
+ const transport = new chat_js_1.TriggerChatTransport({
1084
+ task: "my-task",
1085
+ accessToken: async () => {
1086
+ tokenCallCount++;
1087
+ await new Promise((r) => setTimeout(r, 1));
1088
+ return `async-wp-token-${tokenCallCount}`;
1089
+ },
1090
+ baseURL: "https://api.test.trigger.dev",
1091
+ });
1092
+ // First message — triggers a new run (calls async token)
1093
+ const stream1 = await transport.sendMessages({
1094
+ trigger: "submit-message",
1095
+ chatId: "chat-async-wp",
1096
+ messageId: undefined,
1097
+ messages: [createUserMessage("Hello")],
1098
+ abortSignal: undefined,
1099
+ });
1100
+ const reader1 = stream1.getReader();
1101
+ while (true) {
1102
+ const { done } = await reader1.read();
1103
+ if (done)
1104
+ break;
1105
+ }
1106
+ const firstTokenCount = tokenCallCount;
1107
+ // Second message — should send via input stream (does NOT call async token)
1108
+ const stream2 = await transport.sendMessages({
1109
+ trigger: "submit-message",
1110
+ chatId: "chat-async-wp",
1111
+ messageId: undefined,
1112
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("More")],
1113
+ abortSignal: undefined,
1114
+ });
1115
+ const reader2 = stream2.getReader();
1116
+ while (true) {
1117
+ const { done } = await reader2.read();
1118
+ if (done)
1119
+ break;
1120
+ }
1121
+ // Token function should NOT have been called again for the input stream path
1122
+ (0, vitest_1.expect)(tokenCallCount).toBe(firstTokenCount);
1123
+ (0, vitest_1.expect)(inputStreamSendCalled).toBe(true);
1124
+ });
1125
+ });
1126
+ (0, vitest_1.describe)("single-run mode (input stream loop)", () => {
1127
+ (0, vitest_1.it)("should not forward turn-complete control chunk to consumer", async () => {
1128
+ const turnCompleteChunk = { type: "__trigger_turn_complete" };
1129
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1130
+ const urlStr = typeof url === "string" ? url : url.toString();
1131
+ if (urlStr.includes("/trigger")) {
1132
+ return new Response(JSON.stringify({ id: "run_single" }), {
1133
+ status: 200,
1134
+ headers: {
1135
+ "content-type": "application/json",
1136
+ "x-trigger-jwt": "pub_token",
1137
+ },
1138
+ });
1139
+ }
1140
+ if (urlStr.includes("/realtime/v1/streams/")) {
1141
+ const chunks = [
1142
+ ...sampleChunks,
1143
+ { type: "finish", id: "part-1" },
1144
+ turnCompleteChunk,
1145
+ ];
1146
+ return new Response(createSSEStream(sseEncode(chunks)), {
1147
+ status: 200,
1148
+ headers: {
1149
+ "content-type": "text/event-stream",
1150
+ "X-Stream-Version": "v1",
1151
+ },
1152
+ });
1153
+ }
1154
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1155
+ });
1156
+ const transport = new chat_js_1.TriggerChatTransport({
1157
+ task: "my-task",
1158
+ accessToken: "token",
1159
+ baseURL: "https://api.test.trigger.dev",
1160
+ });
1161
+ const stream = await transport.sendMessages({
1162
+ trigger: "submit-message",
1163
+ chatId: "chat-single",
1164
+ messageId: undefined,
1165
+ messages: [createUserMessage("Hello")],
1166
+ abortSignal: undefined,
1167
+ });
1168
+ // Read all chunks — the control chunk should NOT appear
1169
+ const reader = stream.getReader();
1170
+ const receivedChunks = [];
1171
+ while (true) {
1172
+ const { done, value } = await reader.read();
1173
+ if (done)
1174
+ break;
1175
+ receivedChunks.push(value);
1176
+ }
1177
+ // All AI SDK chunks should be forwarded
1178
+ (0, vitest_1.expect)(receivedChunks.length).toBe(sampleChunks.length + 1); // +1 for the finish chunk
1179
+ // Control chunk should not be in the output
1180
+ (0, vitest_1.expect)(receivedChunks.every((c) => c.type !== "__trigger_turn_complete")).toBe(true);
1181
+ });
1182
+ (0, vitest_1.it)("should send via input stream on second message instead of triggering a new run", async () => {
1183
+ const turnCompleteChunk = { type: "__trigger_turn_complete" };
1184
+ let triggerCallCount = 0;
1185
+ let inputStreamSendCalled = false;
1186
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
1187
+ const urlStr = typeof url === "string" ? url : url.toString();
1188
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
1189
+ triggerCallCount++;
1190
+ return new Response(JSON.stringify({ id: "run_resume" }), {
1191
+ status: 200,
1192
+ headers: {
1193
+ "content-type": "application/json",
1194
+ "x-trigger-jwt": "pub_token",
1195
+ },
1196
+ });
1197
+ }
1198
+ // Handle input stream sends
1199
+ if (urlStr.includes("/realtime/v1/streams/") && urlStr.includes("/input/")) {
1200
+ inputStreamSendCalled = true;
1201
+ return new Response(JSON.stringify({ ok: true }), {
1202
+ status: 200,
1203
+ headers: { "content-type": "application/json" },
1204
+ });
1205
+ }
1206
+ if (urlStr.includes("/realtime/v1/streams/")) {
1207
+ const chunks = [
1208
+ ...sampleChunks,
1209
+ { type: "finish", id: "part-1" },
1210
+ turnCompleteChunk,
1211
+ ];
1212
+ return new Response(createSSEStream(sseEncode(chunks)), {
1213
+ status: 200,
1214
+ headers: {
1215
+ "content-type": "text/event-stream",
1216
+ "X-Stream-Version": "v1",
1217
+ },
1218
+ });
1219
+ }
1220
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1221
+ });
1222
+ const transport = new chat_js_1.TriggerChatTransport({
1223
+ task: "my-task",
1224
+ accessToken: "token",
1225
+ baseURL: "https://api.test.trigger.dev",
1226
+ });
1227
+ // First message — triggers a new run
1228
+ const stream1 = await transport.sendMessages({
1229
+ trigger: "submit-message",
1230
+ chatId: "chat-resume",
1231
+ messageId: undefined,
1232
+ messages: [createUserMessage("Hello")],
1233
+ abortSignal: undefined,
1234
+ });
1235
+ // Consume stream
1236
+ const reader1 = stream1.getReader();
1237
+ while (true) {
1238
+ const { done } = await reader1.read();
1239
+ if (done)
1240
+ break;
1241
+ }
1242
+ (0, vitest_1.expect)(triggerCallCount).toBe(1);
1243
+ // Second message — should send via input stream instead of triggering
1244
+ const stream2 = await transport.sendMessages({
1245
+ trigger: "submit-message",
1246
+ chatId: "chat-resume",
1247
+ messageId: undefined,
1248
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("How are you?")],
1249
+ abortSignal: undefined,
1250
+ });
1251
+ // Consume second stream
1252
+ const reader2 = stream2.getReader();
1253
+ while (true) {
1254
+ const { done } = await reader2.read();
1255
+ if (done)
1256
+ break;
1257
+ }
1258
+ // Should NOT have triggered a second run
1259
+ (0, vitest_1.expect)(triggerCallCount).toBe(1);
1260
+ // Should have sent via input stream
1261
+ (0, vitest_1.expect)(inputStreamSendCalled).toBe(true);
1262
+ });
1263
+ (0, vitest_1.it)("should fall back to triggering a new run if stream closes without control chunk", async () => {
1264
+ let triggerCallCount = 0;
1265
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1266
+ const urlStr = typeof url === "string" ? url : url.toString();
1267
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
1268
+ triggerCallCount++;
1269
+ return new Response(JSON.stringify({ id: `run_fallback_${triggerCallCount}` }), {
1270
+ status: 200,
1271
+ headers: {
1272
+ "content-type": "application/json",
1273
+ "x-trigger-jwt": "pub_token",
1274
+ },
1275
+ });
1276
+ }
1277
+ if (urlStr.includes("/realtime/v1/streams/")) {
1278
+ // No control chunk — stream just ends after the finish
1279
+ const chunks = [
1280
+ { type: "text-start", id: "p1" },
1281
+ { type: "text-delta", id: "p1", delta: "Hello" },
1282
+ { type: "text-end", id: "p1" },
1283
+ ];
1284
+ return new Response(createSSEStream(sseEncode(chunks)), {
1285
+ status: 200,
1286
+ headers: {
1287
+ "content-type": "text/event-stream",
1288
+ "X-Stream-Version": "v1",
1289
+ },
1290
+ });
1291
+ }
1292
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1293
+ });
1294
+ const transport = new chat_js_1.TriggerChatTransport({
1295
+ task: "my-task",
1296
+ accessToken: "token",
1297
+ baseURL: "https://api.test.trigger.dev",
1298
+ });
1299
+ // First message
1300
+ const stream1 = await transport.sendMessages({
1301
+ trigger: "submit-message",
1302
+ chatId: "chat-fallback",
1303
+ messageId: undefined,
1304
+ messages: [createUserMessage("Hello")],
1305
+ abortSignal: undefined,
1306
+ });
1307
+ const reader1 = stream1.getReader();
1308
+ while (true) {
1309
+ const { done } = await reader1.read();
1310
+ if (done)
1311
+ break;
1312
+ }
1313
+ (0, vitest_1.expect)(triggerCallCount).toBe(1);
1314
+ // Second message — no waitpoint token stored, should trigger a new run
1315
+ await transport.sendMessages({
1316
+ trigger: "submit-message",
1317
+ chatId: "chat-fallback",
1318
+ messageId: undefined,
1319
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("Again")],
1320
+ abortSignal: undefined,
1321
+ });
1322
+ // Should have triggered a second run
1323
+ (0, vitest_1.expect)(triggerCallCount).toBe(2);
1324
+ });
1325
+ (0, vitest_1.it)("should fall back to new run when sendInputStream fails", async () => {
1326
+ const turnCompleteChunk = { type: "__trigger_turn_complete" };
1327
+ let triggerCallCount = 0;
1328
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1329
+ const urlStr = typeof url === "string" ? url : url.toString();
1330
+ if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
1331
+ triggerCallCount++;
1332
+ return new Response(JSON.stringify({ id: `run_fail_${triggerCallCount}` }), {
1333
+ status: 200,
1334
+ headers: {
1335
+ "content-type": "application/json",
1336
+ "x-trigger-jwt": "pub_token",
1337
+ },
1338
+ });
1339
+ }
1340
+ // Input stream send fails
1341
+ if (urlStr.includes("/realtime/v1/streams/") && urlStr.includes("/input/")) {
1342
+ return new Response(JSON.stringify({ error: "Run not found" }), {
1343
+ status: 404,
1344
+ headers: { "content-type": "application/json" },
1345
+ });
1346
+ }
1347
+ if (urlStr.includes("/realtime/v1/streams/")) {
1348
+ const chunks = [
1349
+ ...sampleChunks,
1350
+ { type: "finish", id: "part-1" },
1351
+ turnCompleteChunk,
1352
+ ];
1353
+ return new Response(createSSEStream(sseEncode(chunks)), {
1354
+ status: 200,
1355
+ headers: {
1356
+ "content-type": "text/event-stream",
1357
+ "X-Stream-Version": "v1",
1358
+ },
1359
+ });
1360
+ }
1361
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1362
+ });
1363
+ const transport = new chat_js_1.TriggerChatTransport({
1364
+ task: "my-task",
1365
+ accessToken: "token",
1366
+ baseURL: "https://api.test.trigger.dev",
1367
+ });
1368
+ // First message
1369
+ const stream1 = await transport.sendMessages({
1370
+ trigger: "submit-message",
1371
+ chatId: "chat-fail",
1372
+ messageId: undefined,
1373
+ messages: [createUserMessage("Hello")],
1374
+ abortSignal: undefined,
1375
+ });
1376
+ const reader1 = stream1.getReader();
1377
+ while (true) {
1378
+ const { done } = await reader1.read();
1379
+ if (done)
1380
+ break;
1381
+ }
1382
+ (0, vitest_1.expect)(triggerCallCount).toBe(1);
1383
+ // Second message — sendInputStream will fail, should fall back to new run
1384
+ const stream2 = await transport.sendMessages({
1385
+ trigger: "submit-message",
1386
+ chatId: "chat-fail",
1387
+ messageId: undefined,
1388
+ messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("Again")],
1389
+ abortSignal: undefined,
1390
+ });
1391
+ const reader2 = stream2.getReader();
1392
+ while (true) {
1393
+ const { done } = await reader2.read();
1394
+ if (done)
1395
+ break;
1396
+ }
1397
+ // Should have triggered a second run as fallback
1398
+ (0, vitest_1.expect)(triggerCallCount).toBe(2);
1399
+ });
1400
+ });
1401
+ (0, vitest_1.describe)("onSessionChange", () => {
1402
+ (0, vitest_1.it)("should fire when a new session is created", async () => {
1403
+ const onSessionChange = vitest_1.vi.fn();
1404
+ const triggerRunId = "run_session_new";
1405
+ const publicToken = "pub_session_new";
1406
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1407
+ const urlStr = typeof url === "string" ? url : url.toString();
1408
+ if (urlStr.includes("/trigger")) {
1409
+ return new Response(JSON.stringify({ id: triggerRunId }), {
1410
+ status: 200,
1411
+ headers: {
1412
+ "content-type": "application/json",
1413
+ "x-trigger-jwt": publicToken,
1414
+ },
1415
+ });
1416
+ }
1417
+ if (urlStr.includes("/realtime/v1/streams/")) {
1418
+ const chunks = [
1419
+ ...sampleChunks,
1420
+ { type: "__trigger_turn_complete" },
1421
+ ];
1422
+ return new Response(createSSEStream(sseEncode(chunks)), {
1423
+ status: 200,
1424
+ headers: {
1425
+ "content-type": "text/event-stream",
1426
+ "X-Stream-Version": "v1",
1427
+ },
1428
+ });
1429
+ }
1430
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1431
+ });
1432
+ const transport = new chat_js_1.TriggerChatTransport({
1433
+ task: "my-task",
1434
+ accessToken: "token",
1435
+ baseURL: "https://api.test.trigger.dev",
1436
+ onSessionChange,
1437
+ });
1438
+ const stream = await transport.sendMessages({
1439
+ trigger: "submit-message",
1440
+ chatId: "chat-1",
1441
+ messageId: undefined,
1442
+ messages: [createUserMessage("Hello")],
1443
+ abortSignal: undefined,
1444
+ });
1445
+ // Session created notification should have fired
1446
+ (0, vitest_1.expect)(onSessionChange).toHaveBeenCalledWith("chat-1", {
1447
+ runId: triggerRunId,
1448
+ publicAccessToken: publicToken,
1449
+ lastEventId: undefined,
1450
+ });
1451
+ // Consume stream
1452
+ const reader = stream.getReader();
1453
+ while (!(await reader.read()).done) { }
1454
+ // Should also fire with updated lastEventId on turn complete
1455
+ const lastCall = onSessionChange.mock.calls[onSessionChange.mock.calls.length - 1];
1456
+ (0, vitest_1.expect)(lastCall[0]).toBe("chat-1");
1457
+ (0, vitest_1.expect)(lastCall[1]).not.toBeNull();
1458
+ (0, vitest_1.expect)(lastCall[1].lastEventId).toBeDefined();
1459
+ });
1460
+ (0, vitest_1.it)("should fire with null when session is deleted (stream ends naturally)", async () => {
1461
+ const onSessionChange = vitest_1.vi.fn();
1462
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1463
+ const urlStr = typeof url === "string" ? url : url.toString();
1464
+ if (urlStr.includes("/trigger")) {
1465
+ return new Response(JSON.stringify({ id: "run_end" }), {
1466
+ status: 200,
1467
+ headers: {
1468
+ "content-type": "application/json",
1469
+ "x-trigger-jwt": "pub_end",
1470
+ },
1471
+ });
1472
+ }
1473
+ if (urlStr.includes("/realtime/v1/streams/")) {
1474
+ // No turn-complete chunk — stream ends naturally (run completed)
1475
+ return new Response(createSSEStream(sseEncode(sampleChunks)), {
1476
+ status: 200,
1477
+ headers: {
1478
+ "content-type": "text/event-stream",
1479
+ "X-Stream-Version": "v1",
1480
+ },
1481
+ });
1482
+ }
1483
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1484
+ });
1485
+ const transport = new chat_js_1.TriggerChatTransport({
1486
+ task: "my-task",
1487
+ accessToken: "token",
1488
+ baseURL: "https://api.test.trigger.dev",
1489
+ onSessionChange,
1490
+ });
1491
+ const stream = await transport.sendMessages({
1492
+ trigger: "submit-message",
1493
+ chatId: "chat-end",
1494
+ messageId: undefined,
1495
+ messages: [createUserMessage("Hello")],
1496
+ abortSignal: undefined,
1497
+ });
1498
+ // Consume the stream fully
1499
+ const reader = stream.getReader();
1500
+ while (!(await reader.read()).done) { }
1501
+ // Session should have been created then deleted
1502
+ (0, vitest_1.expect)(onSessionChange).toHaveBeenCalledWith("chat-end", vitest_1.expect.objectContaining({
1503
+ runId: "run_end",
1504
+ }));
1505
+ (0, vitest_1.expect)(onSessionChange).toHaveBeenCalledWith("chat-end", null);
1506
+ });
1507
+ (0, vitest_1.it)("should be updatable via setOnSessionChange", async () => {
1508
+ const onSessionChange1 = vitest_1.vi.fn();
1509
+ const onSessionChange2 = vitest_1.vi.fn();
1510
+ global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
1511
+ const urlStr = typeof url === "string" ? url : url.toString();
1512
+ if (urlStr.includes("/trigger")) {
1513
+ return new Response(JSON.stringify({ id: "run_update" }), {
1514
+ status: 200,
1515
+ headers: {
1516
+ "content-type": "application/json",
1517
+ "x-trigger-jwt": "pub_update",
1518
+ },
1519
+ });
1520
+ }
1521
+ if (urlStr.includes("/realtime/v1/streams/")) {
1522
+ const chunks = [
1523
+ ...sampleChunks,
1524
+ { type: "__trigger_turn_complete" },
1525
+ ];
1526
+ return new Response(createSSEStream(sseEncode(chunks)), {
1527
+ status: 200,
1528
+ headers: {
1529
+ "content-type": "text/event-stream",
1530
+ "X-Stream-Version": "v1",
1531
+ },
1532
+ });
1533
+ }
1534
+ throw new Error(`Unexpected fetch URL: ${urlStr}`);
1535
+ });
1536
+ const transport = new chat_js_1.TriggerChatTransport({
1537
+ task: "my-task",
1538
+ accessToken: "token",
1539
+ baseURL: "https://api.test.trigger.dev",
1540
+ onSessionChange: onSessionChange1,
1541
+ });
1542
+ // Update the callback before sending
1543
+ transport.setOnSessionChange(onSessionChange2);
1544
+ await transport.sendMessages({
1545
+ trigger: "submit-message",
1546
+ chatId: "chat-update",
1547
+ messageId: undefined,
1548
+ messages: [createUserMessage("Hello")],
1549
+ abortSignal: undefined,
1550
+ });
1551
+ // Only onSessionChange2 should have been called
1552
+ (0, vitest_1.expect)(onSessionChange1).not.toHaveBeenCalled();
1553
+ (0, vitest_1.expect)(onSessionChange2).toHaveBeenCalled();
1554
+ });
1555
+ });
1556
+ });
1557
+ //# sourceMappingURL=chat.test.js.map