@trigger.dev/sdk 4.5.14 → 4.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/v3/ai-shared.d.ts +7 -0
- package/dist/commonjs/v3/ai-shared.js +15 -0
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.d.ts +11 -1
- package/dist/commonjs/v3/ai.js +11 -3
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.js +1 -1
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +2 -1
- package/dist/commonjs/v3/chat-server.js +3 -3
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +42 -13
- package/dist/commonjs/v3/sessions.js +85 -16
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai-shared.d.ts +7 -0
- package/dist/esm/v3/ai-shared.js +14 -0
- package/dist/esm/v3/ai-shared.js.map +1 -1
- package/dist/esm/v3/ai.d.ts +11 -1
- package/dist/esm/v3/ai.js +12 -4
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.js +2 -2
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +2 -1
- package/dist/esm/v3/chat-server.js +3 -3
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +42 -13
- package/dist/esm/v3/sessions.js +85 -16
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/reference.mdx +1 -1
- package/docs/ai-chat/sessions.mdx +1 -1
- package/docs/ai-chat/side-channels.mdx +172 -0
- package/docs/mcp-tools.mdx +37 -0
- package/docs/realtime/react-hooks/session-stream.mdx +17 -0
- package/package.json +5 -2
- package/dist/commonjs/v3/auth.test.d.ts +0 -1
- package/dist/commonjs/v3/auth.test.js +0 -149
- package/dist/commonjs/v3/auth.test.js.map +0 -1
- package/dist/commonjs/v3/batchDebounce.test.d.ts +0 -1
- package/dist/commonjs/v3/batchDebounce.test.js +0 -189
- package/dist/commonjs/v3/batchDebounce.test.js.map +0 -1
- package/dist/commonjs/v3/chat-server.test.d.ts +0 -1
- package/dist/commonjs/v3/chat-server.test.js +0 -713
- package/dist/commonjs/v3/chat-server.test.js.map +0 -1
- package/dist/commonjs/v3/chat-tab-coordinator.test.d.ts +0 -1
- package/dist/commonjs/v3/chat-tab-coordinator.test.js +0 -140
- package/dist/commonjs/v3/chat-tab-coordinator.test.js.map +0 -1
- package/dist/commonjs/v3/chat.test.d.ts +0 -1
- package/dist/commonjs/v3/chat.test.js +0 -1838
- package/dist/commonjs/v3/chat.test.js.map +0 -1
- package/dist/commonjs/v3/createStartSessionAction.test.d.ts +0 -1
- package/dist/commonjs/v3/createStartSessionAction.test.js +0 -176
- package/dist/commonjs/v3/createStartSessionAction.test.js.map +0 -1
- package/dist/commonjs/v3/runs-bulk.test.d.ts +0 -1
- package/dist/commonjs/v3/runs-bulk.test.js +0 -141
- package/dist/commonjs/v3/runs-bulk.test.js.map +0 -1
- package/dist/commonjs/v3/schedules/index.test.d.ts +0 -1
- package/dist/commonjs/v3/schedules/index.test.js +0 -49
- package/dist/commonjs/v3/schedules/index.test.js.map +0 -1
- package/dist/commonjs/v3/schedules/index.types.test.d.ts +0 -1
- package/dist/commonjs/v3/schedules/index.types.test.js +0 -93
- package/dist/commonjs/v3/schedules/index.types.test.js.map +0 -1
- package/dist/commonjs/v3/sessions.test.d.ts +0 -1
- package/dist/commonjs/v3/sessions.test.js +0 -154
- package/dist/commonjs/v3/sessions.test.js.map +0 -1
- package/dist/commonjs/v3/shared.test.d.ts +0 -1
- package/dist/commonjs/v3/shared.test.js +0 -236
- package/dist/commonjs/v3/shared.test.js.map +0 -1
- package/dist/commonjs/v3/streams.test.d.ts +0 -1
- package/dist/commonjs/v3/streams.test.js +0 -51
- package/dist/commonjs/v3/streams.test.js.map +0 -1
- package/dist/commonjs/v3/triggerClient.test.d.ts +0 -1
- package/dist/commonjs/v3/triggerClient.test.js +0 -226
- package/dist/commonjs/v3/triggerClient.test.js.map +0 -1
- package/dist/commonjs/v3/triggerClient.types.test.d.ts +0 -1
- package/dist/commonjs/v3/triggerClient.types.test.js +0 -123
- package/dist/commonjs/v3/triggerClient.types.test.js.map +0 -1
- package/dist/esm/v3/auth.test.d.ts +0 -1
- package/dist/esm/v3/auth.test.js +0 -147
- package/dist/esm/v3/auth.test.js.map +0 -1
- package/dist/esm/v3/batchDebounce.test.d.ts +0 -1
- package/dist/esm/v3/batchDebounce.test.js +0 -187
- package/dist/esm/v3/batchDebounce.test.js.map +0 -1
- package/dist/esm/v3/chat-server.test.d.ts +0 -1
- package/dist/esm/v3/chat-server.test.js +0 -711
- package/dist/esm/v3/chat-server.test.js.map +0 -1
- package/dist/esm/v3/chat-tab-coordinator.test.d.ts +0 -1
- package/dist/esm/v3/chat-tab-coordinator.test.js +0 -138
- package/dist/esm/v3/chat-tab-coordinator.test.js.map +0 -1
- package/dist/esm/v3/chat.test.d.ts +0 -1
- package/dist/esm/v3/chat.test.js +0 -1836
- package/dist/esm/v3/chat.test.js.map +0 -1
- package/dist/esm/v3/createStartSessionAction.test.d.ts +0 -1
- package/dist/esm/v3/createStartSessionAction.test.js +0 -174
- package/dist/esm/v3/createStartSessionAction.test.js.map +0 -1
- package/dist/esm/v3/runs-bulk.test.d.ts +0 -1
- package/dist/esm/v3/runs-bulk.test.js +0 -139
- package/dist/esm/v3/runs-bulk.test.js.map +0 -1
- package/dist/esm/v3/schedules/index.test.d.ts +0 -1
- package/dist/esm/v3/schedules/index.test.js +0 -47
- package/dist/esm/v3/schedules/index.test.js.map +0 -1
- package/dist/esm/v3/schedules/index.types.test.d.ts +0 -1
- package/dist/esm/v3/schedules/index.types.test.js +0 -91
- package/dist/esm/v3/schedules/index.types.test.js.map +0 -1
- package/dist/esm/v3/sessions.test.d.ts +0 -1
- package/dist/esm/v3/sessions.test.js +0 -152
- package/dist/esm/v3/sessions.test.js.map +0 -1
- package/dist/esm/v3/shared.test.d.ts +0 -1
- package/dist/esm/v3/shared.test.js +0 -234
- package/dist/esm/v3/shared.test.js.map +0 -1
- package/dist/esm/v3/streams.test.d.ts +0 -1
- package/dist/esm/v3/streams.test.js +0 -49
- package/dist/esm/v3/streams.test.js.map +0 -1
- package/dist/esm/v3/triggerClient.test.d.ts +0 -1
- package/dist/esm/v3/triggerClient.test.js +0 -224
- package/dist/esm/v3/triggerClient.test.js.map +0 -1
- package/dist/esm/v3/triggerClient.types.test.d.ts +0 -1
- package/dist/esm/v3/triggerClient.types.test.js +0 -88
- package/dist/esm/v3/triggerClient.types.test.js.map +0 -1
|
@@ -1,1838 +0,0 @@
|
|
|
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
|
-
// ───────────────────────────────────────────────────────────────────────────
|
|
6
|
-
// Test helpers
|
|
7
|
-
// ───────────────────────────────────────────────────────────────────────────
|
|
8
|
-
/**
|
|
9
|
-
* Encode chunks as SSE text. The runtime SSE parser
|
|
10
|
-
* ({@link SSEStreamSubscription}) auto-parses the `data:` field via
|
|
11
|
-
* `safeParseJSON` and yields it as `value.chunk`, so each `data:` line
|
|
12
|
-
* just needs to contain the JSON-encoded chunk directly.
|
|
13
|
-
*
|
|
14
|
-
* In production the session backend sends the raw S2 record body as the
|
|
15
|
-
* `data:` field — that body is itself a JSON string (the transport
|
|
16
|
-
* round-trips through `JSON.stringify`/`JSON.parse`). The transport's
|
|
17
|
-
* SSE reader handles both shapes (`typeof value.chunk === "string"` →
|
|
18
|
-
* parse-once, `=== "object"` → use as-is). We pick the object form
|
|
19
|
-
* here for test simplicity.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* Encode test chunks as a session-stream v2 SSE batch event. Each chunk
|
|
23
|
-
* becomes one S2 record; chunks of shape `{type: "trigger:turn-complete"}`
|
|
24
|
-
* or `{type: "trigger:upgrade-required"}` are translated into header-form
|
|
25
|
-
* control records (empty body, `trigger-control` header) to match the
|
|
26
|
-
* production wire shape.
|
|
27
|
-
*/
|
|
28
|
-
function sseEncode(chunks) {
|
|
29
|
-
let nextSeq = 1;
|
|
30
|
-
const records = chunks.map((chunk, i) => {
|
|
31
|
-
const partId = `p-${i}`;
|
|
32
|
-
const type = chunk.type;
|
|
33
|
-
if (type === "trigger:turn-complete") {
|
|
34
|
-
const headers = [["trigger-control", "turn-complete"]];
|
|
35
|
-
const token = chunk.publicAccessToken;
|
|
36
|
-
if (token)
|
|
37
|
-
headers.push(["public-access-token", token]);
|
|
38
|
-
return {
|
|
39
|
-
body: "",
|
|
40
|
-
seq_num: nextSeq++,
|
|
41
|
-
timestamp: 1700000000000 + i,
|
|
42
|
-
headers,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (type === "trigger:upgrade-required") {
|
|
46
|
-
return {
|
|
47
|
-
body: "",
|
|
48
|
-
seq_num: nextSeq++,
|
|
49
|
-
timestamp: 1700000000000 + i,
|
|
50
|
-
headers: [["trigger-control", "upgrade-required"]],
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
body: JSON.stringify({ data: chunk, id: partId }),
|
|
55
|
-
seq_num: nextSeq++,
|
|
56
|
-
timestamp: 1700000000000 + i,
|
|
57
|
-
headers: [],
|
|
58
|
-
};
|
|
59
|
-
});
|
|
60
|
-
return `event: batch\ndata: ${JSON.stringify({ records })}\n\n`;
|
|
61
|
-
}
|
|
62
|
-
function createSSEStream(sseText) {
|
|
63
|
-
const encoder = new TextEncoder();
|
|
64
|
-
return new ReadableStream({
|
|
65
|
-
start(controller) {
|
|
66
|
-
controller.enqueue(encoder.encode(sseText));
|
|
67
|
-
controller.close();
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
let messageIdCounter = 0;
|
|
72
|
-
function createUserMessage(text) {
|
|
73
|
-
return {
|
|
74
|
-
id: `msg-user-${++messageIdCounter}`,
|
|
75
|
-
role: "user",
|
|
76
|
-
parts: [{ type: "text", text }],
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
const sampleChunks = [
|
|
80
|
-
{ type: "text-start", id: "part-1" },
|
|
81
|
-
{ type: "text-delta", id: "part-1", delta: "Hello" },
|
|
82
|
-
{ type: "text-delta", id: "part-1", delta: " world" },
|
|
83
|
-
{ type: "text-delta", id: "part-1", delta: "!" },
|
|
84
|
-
{ type: "text-end", id: "part-1" },
|
|
85
|
-
];
|
|
86
|
-
const sampleChunksWithTurnComplete = [
|
|
87
|
-
...sampleChunks,
|
|
88
|
-
{ type: "trigger:turn-complete" },
|
|
89
|
-
];
|
|
90
|
-
// URL predicates
|
|
91
|
-
function isSessionCreateUrl(urlStr) {
|
|
92
|
-
return urlStr.endsWith("/api/v1/sessions") || urlStr.endsWith("/api/v1/sessions/");
|
|
93
|
-
}
|
|
94
|
-
function isSessionOutSubscribeUrl(urlStr) {
|
|
95
|
-
return /\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr);
|
|
96
|
-
}
|
|
97
|
-
function isSessionStreamAppendUrl(urlStr) {
|
|
98
|
-
return /\/realtime\/v1\/sessions\/[^/]+\/(in|out)\/append$/.test(urlStr);
|
|
99
|
-
}
|
|
100
|
-
function chatIdFromUrl(urlStr) {
|
|
101
|
-
const m = urlStr.match(/\/realtime\/v1\/sessions\/([^/]+)\//);
|
|
102
|
-
return m?.[1];
|
|
103
|
-
}
|
|
104
|
-
const _DEFAULT_RUN_ID = "run_default";
|
|
105
|
-
const _DEFAULT_SESSION_ID = "session_default";
|
|
106
|
-
const _DEFAULT_SESSION_PAT = "pat_session_default";
|
|
107
|
-
function defaultAppendResponse() {
|
|
108
|
-
return new Response(JSON.stringify({ ok: true }), {
|
|
109
|
-
status: 200,
|
|
110
|
-
headers: { "content-type": "application/json" },
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
function defaultSseResponse(chunks = sampleChunksWithTurnComplete) {
|
|
114
|
-
return new Response(createSSEStream(sseEncode(chunks)), {
|
|
115
|
-
status: 200,
|
|
116
|
-
headers: {
|
|
117
|
-
"content-type": "text/event-stream",
|
|
118
|
-
// Session streams are always v2 in production — batch format
|
|
119
|
-
// with one S2 record per SSE event. The legacy v1 path is for
|
|
120
|
-
// run-scoped Redis streams.
|
|
121
|
-
"X-Stream-Version": "v2",
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* An SSE response whose body stays open until the request signal aborts.
|
|
127
|
-
* Models a live subscription sitting on a quiet server.
|
|
128
|
-
*/
|
|
129
|
-
function openSseResponse(signal) {
|
|
130
|
-
const body = new ReadableStream({
|
|
131
|
-
start(controller) {
|
|
132
|
-
const onAbort = () => {
|
|
133
|
-
const err = new Error("aborted");
|
|
134
|
-
err.name = "AbortError";
|
|
135
|
-
try {
|
|
136
|
-
controller.error(err);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
/* already errored */
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
if (signal?.aborted)
|
|
143
|
-
onAbort();
|
|
144
|
-
else
|
|
145
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
return new Response(body, {
|
|
149
|
-
status: 200,
|
|
150
|
-
headers: {
|
|
151
|
-
"content-type": "text/event-stream",
|
|
152
|
-
"X-Stream-Version": "v2",
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
function authError(status = 401) {
|
|
157
|
-
return new Response(JSON.stringify({ error: "Unauthorized", name: "TriggerApiError", status }), {
|
|
158
|
-
status,
|
|
159
|
-
headers: { "content-type": "application/json" },
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Drains a UIMessageChunk stream into an array. Used to assert what
|
|
164
|
-
* the transport surfaced after filtering control chunks.
|
|
165
|
-
*/
|
|
166
|
-
async function drainChunks(stream) {
|
|
167
|
-
const reader = stream.getReader();
|
|
168
|
-
const out = [];
|
|
169
|
-
try {
|
|
170
|
-
while (true) {
|
|
171
|
-
const { done, value } = await reader.read();
|
|
172
|
-
if (done)
|
|
173
|
-
break;
|
|
174
|
-
out.push(value);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
finally {
|
|
178
|
-
reader.releaseLock();
|
|
179
|
-
}
|
|
180
|
-
return out;
|
|
181
|
-
}
|
|
182
|
-
// ───────────────────────────────────────────────────────────────────────────
|
|
183
|
-
// Tests
|
|
184
|
-
// ───────────────────────────────────────────────────────────────────────────
|
|
185
|
-
(0, vitest_1.describe)("TriggerChatTransport", () => {
|
|
186
|
-
let originalFetch;
|
|
187
|
-
(0, vitest_1.beforeEach)(() => {
|
|
188
|
-
originalFetch = global.fetch;
|
|
189
|
-
});
|
|
190
|
-
(0, vitest_1.afterEach)(() => {
|
|
191
|
-
global.fetch = originalFetch;
|
|
192
|
-
vitest_1.vi.restoreAllMocks();
|
|
193
|
-
});
|
|
194
|
-
(0, vitest_1.describe)("constructor", () => {
|
|
195
|
-
(0, vitest_1.it)("creates with required options", () => {
|
|
196
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
197
|
-
task: "my-chat-task",
|
|
198
|
-
accessToken: () => "pat",
|
|
199
|
-
});
|
|
200
|
-
(0, vitest_1.expect)(transport).toBeInstanceOf(chat_js_1.TriggerChatTransport);
|
|
201
|
-
});
|
|
202
|
-
(0, vitest_1.it)("createChatTransport returns a TriggerChatTransport", () => {
|
|
203
|
-
const transport = (0, chat_js_1.createChatTransport)({
|
|
204
|
-
task: "my-chat-task",
|
|
205
|
-
accessToken: () => "pat",
|
|
206
|
-
});
|
|
207
|
-
(0, vitest_1.expect)(transport).toBeInstanceOf(chat_js_1.TriggerChatTransport);
|
|
208
|
-
});
|
|
209
|
-
(0, vitest_1.it)("hydrates sessions from options.sessions", () => {
|
|
210
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
211
|
-
task: "my-chat-task",
|
|
212
|
-
accessToken: () => "pat",
|
|
213
|
-
sessions: {
|
|
214
|
-
"chat-1": {
|
|
215
|
-
publicAccessToken: "hydrated-pat",
|
|
216
|
-
lastEventId: "42",
|
|
217
|
-
activeInputSeq: 41,
|
|
218
|
-
isStreaming: false,
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
const session = transport.getSession("chat-1");
|
|
223
|
-
(0, vitest_1.expect)(session).toEqual({
|
|
224
|
-
publicAccessToken: "hydrated-pat",
|
|
225
|
-
lastEventId: "42",
|
|
226
|
-
activeInputSeq: 41,
|
|
227
|
-
isStreaming: false,
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
(0, vitest_1.it)("returns undefined for unknown chatIds", () => {
|
|
231
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
232
|
-
task: "my-chat-task",
|
|
233
|
-
accessToken: () => "pat",
|
|
234
|
-
});
|
|
235
|
-
(0, vitest_1.expect)(transport.getSession("unknown")).toBeUndefined();
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
(0, vitest_1.describe)("setSession / setOnSessionChange", () => {
|
|
239
|
-
(0, vitest_1.it)("setSession installs persisted state and notifies", () => {
|
|
240
|
-
const onSessionChange = vitest_1.vi.fn();
|
|
241
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
242
|
-
task: "my-chat-task",
|
|
243
|
-
accessToken: () => "pat",
|
|
244
|
-
onSessionChange,
|
|
245
|
-
});
|
|
246
|
-
transport.setSession("chat-x", {
|
|
247
|
-
publicAccessToken: "tok",
|
|
248
|
-
lastEventId: "10",
|
|
249
|
-
activeInputSeq: 9,
|
|
250
|
-
});
|
|
251
|
-
(0, vitest_1.expect)(transport.getSession("chat-x")).toMatchObject({
|
|
252
|
-
publicAccessToken: "tok",
|
|
253
|
-
lastEventId: "10",
|
|
254
|
-
activeInputSeq: 9,
|
|
255
|
-
});
|
|
256
|
-
(0, vitest_1.expect)(onSessionChange).toHaveBeenCalledWith("chat-x", vitest_1.expect.objectContaining({
|
|
257
|
-
publicAccessToken: "tok",
|
|
258
|
-
lastEventId: "10",
|
|
259
|
-
activeInputSeq: 9,
|
|
260
|
-
}));
|
|
261
|
-
});
|
|
262
|
-
(0, vitest_1.it)("setOnSessionChange swaps the callback at runtime", () => {
|
|
263
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
264
|
-
task: "my-chat-task",
|
|
265
|
-
accessToken: () => "pat",
|
|
266
|
-
});
|
|
267
|
-
const cb1 = vitest_1.vi.fn();
|
|
268
|
-
const cb2 = vitest_1.vi.fn();
|
|
269
|
-
transport.setOnSessionChange(cb1);
|
|
270
|
-
transport.setSession("c", { publicAccessToken: "t1" });
|
|
271
|
-
(0, vitest_1.expect)(cb1).toHaveBeenCalledTimes(1);
|
|
272
|
-
transport.setOnSessionChange(cb2);
|
|
273
|
-
transport.setSession("c", { publicAccessToken: "t2" });
|
|
274
|
-
(0, vitest_1.expect)(cb1).toHaveBeenCalledTimes(1);
|
|
275
|
-
(0, vitest_1.expect)(cb2).toHaveBeenCalledTimes(1);
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
(0, vitest_1.describe)("start", () => {
|
|
279
|
-
(0, vitest_1.it)("calls the customer's startSession callback and caches the returned PAT", async () => {
|
|
280
|
-
const startSession = vitest_1.vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-1" });
|
|
281
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
282
|
-
task: "my-chat-task",
|
|
283
|
-
accessToken: () => "should-not-be-called",
|
|
284
|
-
startSession,
|
|
285
|
-
});
|
|
286
|
-
const result = await transport.start("chat-1");
|
|
287
|
-
(0, vitest_1.expect)(startSession).toHaveBeenCalledWith({
|
|
288
|
-
taskId: "my-chat-task",
|
|
289
|
-
chatId: "chat-1",
|
|
290
|
-
clientData: {},
|
|
291
|
-
});
|
|
292
|
-
(0, vitest_1.expect)(result.publicAccessToken).toBe("session-pat-1");
|
|
293
|
-
(0, vitest_1.expect)(transport.getSession("chat-1")?.publicAccessToken).toBe("session-pat-1");
|
|
294
|
-
});
|
|
295
|
-
(0, vitest_1.it)("is idempotent — second call returns the cached state without re-invoking startSession", async () => {
|
|
296
|
-
const startSession = vitest_1.vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-2" });
|
|
297
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
298
|
-
task: "my-chat-task",
|
|
299
|
-
accessToken: () => "pat",
|
|
300
|
-
startSession,
|
|
301
|
-
});
|
|
302
|
-
await transport.start("chat-2");
|
|
303
|
-
await transport.start("chat-2");
|
|
304
|
-
(0, vitest_1.expect)(startSession).toHaveBeenCalledTimes(1);
|
|
305
|
-
});
|
|
306
|
-
(0, vitest_1.it)("dedupes concurrent calls via an in-flight promise", async () => {
|
|
307
|
-
let resolveStart;
|
|
308
|
-
const startPromise = new Promise((resolve) => {
|
|
309
|
-
resolveStart = resolve;
|
|
310
|
-
});
|
|
311
|
-
const startSession = vitest_1.vi.fn().mockReturnValue(startPromise);
|
|
312
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
313
|
-
task: "my-chat-task",
|
|
314
|
-
accessToken: () => "pat",
|
|
315
|
-
startSession,
|
|
316
|
-
});
|
|
317
|
-
const a = transport.start("chat-3");
|
|
318
|
-
const b = transport.start("chat-3");
|
|
319
|
-
resolveStart({ publicAccessToken: "session-pat-3" });
|
|
320
|
-
await Promise.all([a, b]);
|
|
321
|
-
(0, vitest_1.expect)(startSession).toHaveBeenCalledTimes(1);
|
|
322
|
-
});
|
|
323
|
-
(0, vitest_1.it)("preload() is an alias for start()", async () => {
|
|
324
|
-
const startSession = vitest_1.vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-pre" });
|
|
325
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
326
|
-
task: "my-chat-task",
|
|
327
|
-
accessToken: () => "pat",
|
|
328
|
-
startSession,
|
|
329
|
-
});
|
|
330
|
-
await transport.preload("chat-pre");
|
|
331
|
-
(0, vitest_1.expect)(startSession).toHaveBeenCalledTimes(1);
|
|
332
|
-
(0, vitest_1.expect)(transport.getSession("chat-pre")?.publicAccessToken).toBe("session-pat-pre");
|
|
333
|
-
});
|
|
334
|
-
(0, vitest_1.it)("throws a clear error when start() is called without startSession configured", async () => {
|
|
335
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
336
|
-
task: "my-chat-task",
|
|
337
|
-
accessToken: () => "pat",
|
|
338
|
-
});
|
|
339
|
-
await (0, vitest_1.expect)(transport.start("chat-no-start")).rejects.toThrow(/startSession/);
|
|
340
|
-
});
|
|
341
|
-
(0, vitest_1.it)("threads the transport's `clientData` through to startSession", async () => {
|
|
342
|
-
const startSession = vitest_1.vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-cd" });
|
|
343
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
344
|
-
task: "my-chat-task",
|
|
345
|
-
accessToken: () => "pat",
|
|
346
|
-
startSession,
|
|
347
|
-
clientData: { userId: "u-1", model: "claude-sonnet-4-6" },
|
|
348
|
-
});
|
|
349
|
-
await transport.start("chat-cd");
|
|
350
|
-
(0, vitest_1.expect)(startSession).toHaveBeenCalledWith({
|
|
351
|
-
taskId: "my-chat-task",
|
|
352
|
-
chatId: "chat-cd",
|
|
353
|
-
clientData: { userId: "u-1", model: "claude-sonnet-4-6" },
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
(0, vitest_1.it)("setClientData updates the value passed to subsequent startSession calls", async () => {
|
|
357
|
-
const startSession = vitest_1.vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-set" });
|
|
358
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
359
|
-
task: "my-chat-task",
|
|
360
|
-
accessToken: () => "pat",
|
|
361
|
-
startSession,
|
|
362
|
-
clientData: { userId: "old" },
|
|
363
|
-
});
|
|
364
|
-
transport.setClientData({ userId: "new" });
|
|
365
|
-
await transport.start("chat-set");
|
|
366
|
-
(0, vitest_1.expect)(startSession).toHaveBeenCalledWith({
|
|
367
|
-
taskId: "my-chat-task",
|
|
368
|
-
chatId: "chat-set",
|
|
369
|
-
clientData: { userId: "new" },
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
(0, vitest_1.describe)("ensureSessionState (lazy start on first sendMessage)", () => {
|
|
374
|
-
(0, vitest_1.it)("calls startSession lazily on first sendMessage when no PAT is hydrated", async () => {
|
|
375
|
-
const startSession = vitest_1.vi.fn().mockResolvedValue({ publicAccessToken: "lazy-session-pat" });
|
|
376
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
377
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
378
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
379
|
-
return defaultAppendResponse();
|
|
380
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
381
|
-
return defaultSseResponse();
|
|
382
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
383
|
-
});
|
|
384
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
385
|
-
task: "my-chat-task",
|
|
386
|
-
accessToken: () => "should-not-be-called",
|
|
387
|
-
startSession,
|
|
388
|
-
baseURL: "https://api.test.trigger.dev",
|
|
389
|
-
});
|
|
390
|
-
const stream = await transport.sendMessages({
|
|
391
|
-
trigger: "submit-message",
|
|
392
|
-
chatId: "chat-lazy",
|
|
393
|
-
messageId: undefined,
|
|
394
|
-
messages: [createUserMessage("hi")],
|
|
395
|
-
abortSignal: undefined,
|
|
396
|
-
});
|
|
397
|
-
await drainChunks(stream);
|
|
398
|
-
(0, vitest_1.expect)(startSession).toHaveBeenCalledTimes(1);
|
|
399
|
-
(0, vitest_1.expect)(startSession).toHaveBeenCalledWith({
|
|
400
|
-
taskId: "my-chat-task",
|
|
401
|
-
chatId: "chat-lazy",
|
|
402
|
-
clientData: {},
|
|
403
|
-
});
|
|
404
|
-
(0, vitest_1.expect)(transport.getSession("chat-lazy")?.publicAccessToken).toBe("lazy-session-pat");
|
|
405
|
-
});
|
|
406
|
-
(0, vitest_1.it)("falls back to accessToken when no startSession is configured (out-of-band session create)", async () => {
|
|
407
|
-
const accessToken = vitest_1.vi.fn().mockResolvedValue("server-mediated-pat");
|
|
408
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
409
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
410
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
411
|
-
return defaultAppendResponse();
|
|
412
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
413
|
-
return defaultSseResponse();
|
|
414
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
415
|
-
});
|
|
416
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
417
|
-
task: "my-chat-task",
|
|
418
|
-
accessToken,
|
|
419
|
-
baseURL: "https://api.test.trigger.dev",
|
|
420
|
-
});
|
|
421
|
-
const stream = await transport.sendMessages({
|
|
422
|
-
trigger: "submit-message",
|
|
423
|
-
chatId: "chat-server",
|
|
424
|
-
messageId: undefined,
|
|
425
|
-
messages: [createUserMessage("hi")],
|
|
426
|
-
abortSignal: undefined,
|
|
427
|
-
});
|
|
428
|
-
await drainChunks(stream);
|
|
429
|
-
(0, vitest_1.expect)(accessToken).toHaveBeenCalledTimes(1);
|
|
430
|
-
(0, vitest_1.expect)(accessToken).toHaveBeenCalledWith({ chatId: "chat-server" });
|
|
431
|
-
});
|
|
432
|
-
(0, vitest_1.it)("does not call accessToken when a PAT is hydrated", async () => {
|
|
433
|
-
const accessToken = vitest_1.vi.fn().mockResolvedValue("should-not-be-called");
|
|
434
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
435
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
436
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
437
|
-
return defaultAppendResponse();
|
|
438
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
439
|
-
return defaultSseResponse();
|
|
440
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
441
|
-
});
|
|
442
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
443
|
-
task: "my-chat-task",
|
|
444
|
-
accessToken,
|
|
445
|
-
sessions: {
|
|
446
|
-
"chat-h": { publicAccessToken: "hydrated-pat" },
|
|
447
|
-
},
|
|
448
|
-
});
|
|
449
|
-
const stream = await transport.sendMessages({
|
|
450
|
-
trigger: "submit-message",
|
|
451
|
-
chatId: "chat-h",
|
|
452
|
-
messageId: undefined,
|
|
453
|
-
messages: [createUserMessage("hi")],
|
|
454
|
-
abortSignal: undefined,
|
|
455
|
-
});
|
|
456
|
-
await drainChunks(stream);
|
|
457
|
-
(0, vitest_1.expect)(accessToken).not.toHaveBeenCalled();
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
(0, vitest_1.describe)("sendMessages", () => {
|
|
461
|
-
(0, vitest_1.it)("posts the user message to .in/append and streams chunks from .out", async () => {
|
|
462
|
-
const requests = [];
|
|
463
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
464
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
465
|
-
requests.push({ url: urlStr, init });
|
|
466
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
467
|
-
return defaultAppendResponse();
|
|
468
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
469
|
-
return defaultSseResponse();
|
|
470
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
471
|
-
});
|
|
472
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
473
|
-
task: "my-chat-task",
|
|
474
|
-
accessToken: () => "pat",
|
|
475
|
-
baseURL: "https://api.test.trigger.dev",
|
|
476
|
-
sessions: { "chat-1": { publicAccessToken: "p" } },
|
|
477
|
-
});
|
|
478
|
-
const stream = await transport.sendMessages({
|
|
479
|
-
trigger: "submit-message",
|
|
480
|
-
chatId: "chat-1",
|
|
481
|
-
messageId: "m1",
|
|
482
|
-
messages: [createUserMessage("Hello")],
|
|
483
|
-
abortSignal: undefined,
|
|
484
|
-
});
|
|
485
|
-
const chunks = await drainChunks(stream);
|
|
486
|
-
// Five UI chunks pass through; trigger:turn-complete is filtered.
|
|
487
|
-
(0, vitest_1.expect)(chunks).toHaveLength(sampleChunks.length);
|
|
488
|
-
(0, vitest_1.expect)(chunks[0]).toEqual(sampleChunks[0]);
|
|
489
|
-
const append = requests.find((r) => isSessionStreamAppendUrl(r.url) && r.url.endsWith("/in/append"));
|
|
490
|
-
(0, vitest_1.expect)(append).toBeDefined();
|
|
491
|
-
(0, vitest_1.expect)(chatIdFromUrl(append.url)).toBe("chat-1");
|
|
492
|
-
// Body is the serialized ChatInputChunk.
|
|
493
|
-
const body = JSON.parse(append.init.body);
|
|
494
|
-
(0, vitest_1.expect)(body.kind).toBe("message");
|
|
495
|
-
(0, vitest_1.expect)(body.payload.chatId).toBe("chat-1");
|
|
496
|
-
(0, vitest_1.expect)(body.payload.trigger).toBe("submit-message");
|
|
497
|
-
});
|
|
498
|
-
(0, vitest_1.it)("addresses .out SSE by chatId (not by sessionId)", async () => {
|
|
499
|
-
const requests = [];
|
|
500
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
501
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
502
|
-
requests.push(urlStr);
|
|
503
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
504
|
-
return defaultAppendResponse();
|
|
505
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
506
|
-
return defaultSseResponse();
|
|
507
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
508
|
-
});
|
|
509
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
510
|
-
task: "my-chat-task",
|
|
511
|
-
accessToken: () => "pat",
|
|
512
|
-
baseURL: "https://api.test.trigger.dev",
|
|
513
|
-
sessions: { "chat-by-chatid": { publicAccessToken: "p" } },
|
|
514
|
-
});
|
|
515
|
-
const stream = await transport.sendMessages({
|
|
516
|
-
trigger: "submit-message",
|
|
517
|
-
chatId: "chat-by-chatid",
|
|
518
|
-
messageId: undefined,
|
|
519
|
-
messages: [createUserMessage("Hi")],
|
|
520
|
-
abortSignal: undefined,
|
|
521
|
-
});
|
|
522
|
-
await drainChunks(stream);
|
|
523
|
-
const subscribe = requests.find(isSessionOutSubscribeUrl);
|
|
524
|
-
(0, vitest_1.expect)(subscribe).toBeDefined();
|
|
525
|
-
(0, vitest_1.expect)(subscribe).toContain("/realtime/v1/sessions/chat-by-chatid/out");
|
|
526
|
-
});
|
|
527
|
-
(0, vitest_1.it)("functional baseURL dispatches per endpoint (in vs out)", async () => {
|
|
528
|
-
const requests = [];
|
|
529
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
530
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
531
|
-
requests.push({ url: urlStr, ctxEndpoint: undefined });
|
|
532
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
533
|
-
return defaultAppendResponse();
|
|
534
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
535
|
-
return defaultSseResponse();
|
|
536
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
537
|
-
});
|
|
538
|
-
const baseURLFn = vitest_1.vi.fn(({ endpoint }) => endpoint === "out" ? "https://stream.example.com" : "https://api.example.com");
|
|
539
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
540
|
-
task: "my-chat-task",
|
|
541
|
-
accessToken: () => "pat",
|
|
542
|
-
baseURL: baseURLFn,
|
|
543
|
-
sessions: { "chat-fn": { publicAccessToken: "p" } },
|
|
544
|
-
});
|
|
545
|
-
const stream = await transport.sendMessages({
|
|
546
|
-
trigger: "submit-message",
|
|
547
|
-
chatId: "chat-fn",
|
|
548
|
-
messageId: undefined,
|
|
549
|
-
messages: [createUserMessage("Hi")],
|
|
550
|
-
abortSignal: undefined,
|
|
551
|
-
});
|
|
552
|
-
await drainChunks(stream);
|
|
553
|
-
const appendCalls = baseURLFn.mock.calls.filter((c) => c[0].endpoint === "in");
|
|
554
|
-
const outCalls = baseURLFn.mock.calls.filter((c) => c[0].endpoint === "out");
|
|
555
|
-
(0, vitest_1.expect)(appendCalls.length).toBeGreaterThanOrEqual(1);
|
|
556
|
-
(0, vitest_1.expect)(outCalls.length).toBeGreaterThanOrEqual(1);
|
|
557
|
-
(0, vitest_1.expect)(appendCalls[0][0].chatId).toBe("chat-fn");
|
|
558
|
-
(0, vitest_1.expect)(outCalls[0][0].chatId).toBe("chat-fn");
|
|
559
|
-
const append = requests.find((r) => isSessionStreamAppendUrl(r.url));
|
|
560
|
-
const subscribe = requests.find((r) => isSessionOutSubscribeUrl(r.url));
|
|
561
|
-
(0, vitest_1.expect)(append.url.startsWith("https://api.example.com/")).toBe(true);
|
|
562
|
-
(0, vitest_1.expect)(subscribe.url.startsWith("https://stream.example.com/")).toBe(true);
|
|
563
|
-
});
|
|
564
|
-
(0, vitest_1.it)("fetch override is invoked for both .in/append and .out SSE with endpoint ctx", async () => {
|
|
565
|
-
const fetchCalls = [];
|
|
566
|
-
const customFetch = vitest_1.vi.fn(async (url, init, ctx) => {
|
|
567
|
-
fetchCalls.push({ url, endpoint: ctx.endpoint, chatId: ctx.chatId });
|
|
568
|
-
if (isSessionStreamAppendUrl(url))
|
|
569
|
-
return defaultAppendResponse();
|
|
570
|
-
if (isSessionOutSubscribeUrl(url))
|
|
571
|
-
return defaultSseResponse();
|
|
572
|
-
throw new Error(`Unexpected URL: ${url}`);
|
|
573
|
-
});
|
|
574
|
-
global.fetch = vitest_1.vi.fn().mockRejectedValue(new Error("global fetch should not be called"));
|
|
575
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
576
|
-
task: "my-chat-task",
|
|
577
|
-
accessToken: () => "pat",
|
|
578
|
-
baseURL: "https://api.test.trigger.dev",
|
|
579
|
-
fetch: customFetch,
|
|
580
|
-
sessions: { "chat-fetch": { publicAccessToken: "p" } },
|
|
581
|
-
});
|
|
582
|
-
const stream = await transport.sendMessages({
|
|
583
|
-
trigger: "submit-message",
|
|
584
|
-
chatId: "chat-fetch",
|
|
585
|
-
messageId: undefined,
|
|
586
|
-
messages: [createUserMessage("Hi")],
|
|
587
|
-
abortSignal: undefined,
|
|
588
|
-
});
|
|
589
|
-
await drainChunks(stream);
|
|
590
|
-
const inCalls = fetchCalls.filter((c) => c.endpoint === "in");
|
|
591
|
-
const outCalls = fetchCalls.filter((c) => c.endpoint === "out");
|
|
592
|
-
(0, vitest_1.expect)(inCalls.length).toBeGreaterThanOrEqual(1);
|
|
593
|
-
(0, vitest_1.expect)(outCalls.length).toBeGreaterThanOrEqual(1);
|
|
594
|
-
(0, vitest_1.expect)(inCalls[0].chatId).toBe("chat-fetch");
|
|
595
|
-
(0, vitest_1.expect)(outCalls[0].chatId).toBe("chat-fetch");
|
|
596
|
-
});
|
|
597
|
-
(0, vitest_1.it)("routes .out SSE through streamBaseURL while appends stay on baseURL", async () => {
|
|
598
|
-
const requests = [];
|
|
599
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
600
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
601
|
-
requests.push(urlStr);
|
|
602
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
603
|
-
return defaultAppendResponse();
|
|
604
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
605
|
-
return defaultSseResponse();
|
|
606
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
607
|
-
});
|
|
608
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
609
|
-
task: "my-chat-task",
|
|
610
|
-
accessToken: () => "pat",
|
|
611
|
-
baseURL: "https://api.test.trigger.dev",
|
|
612
|
-
streamBaseURL: "https://chat-proxy.example.com",
|
|
613
|
-
sessions: { "chat-split": { publicAccessToken: "p" } },
|
|
614
|
-
});
|
|
615
|
-
const stream = await transport.sendMessages({
|
|
616
|
-
trigger: "submit-message",
|
|
617
|
-
chatId: "chat-split",
|
|
618
|
-
messageId: undefined,
|
|
619
|
-
messages: [createUserMessage("Hi")],
|
|
620
|
-
abortSignal: undefined,
|
|
621
|
-
});
|
|
622
|
-
await drainChunks(stream);
|
|
623
|
-
const append = requests.find(isSessionStreamAppendUrl);
|
|
624
|
-
const subscribe = requests.find(isSessionOutSubscribeUrl);
|
|
625
|
-
(0, vitest_1.expect)(append.startsWith("https://api.test.trigger.dev/")).toBe(true);
|
|
626
|
-
(0, vitest_1.expect)(subscribe.startsWith("https://chat-proxy.example.com/")).toBe(true);
|
|
627
|
-
(0, vitest_1.expect)(subscribe).toContain("/realtime/v1/sessions/chat-split/out");
|
|
628
|
-
});
|
|
629
|
-
(0, vitest_1.it)("for submit-message, only the latest message is delivered to .in", async () => {
|
|
630
|
-
// Slim wire: each `.in/append` carries at most ONE new message in
|
|
631
|
-
// `payload.message` (singular). Even if the caller hands sendMessages
|
|
632
|
-
// an array of three, only the last element flows to the wire — the
|
|
633
|
-
// agent rebuilds prior history at run boot from snapshot + replay.
|
|
634
|
-
let appendBody;
|
|
635
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
636
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
637
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
638
|
-
appendBody = JSON.parse(init.body);
|
|
639
|
-
return defaultAppendResponse();
|
|
640
|
-
}
|
|
641
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
642
|
-
return defaultSseResponse();
|
|
643
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
644
|
-
});
|
|
645
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
646
|
-
task: "my-chat-task",
|
|
647
|
-
accessToken: () => "pat",
|
|
648
|
-
sessions: { "chat-slice": { publicAccessToken: "p" } },
|
|
649
|
-
});
|
|
650
|
-
const stream = await transport.sendMessages({
|
|
651
|
-
trigger: "submit-message",
|
|
652
|
-
chatId: "chat-slice",
|
|
653
|
-
messageId: undefined,
|
|
654
|
-
messages: [
|
|
655
|
-
createUserMessage("first"),
|
|
656
|
-
createUserMessage("second"),
|
|
657
|
-
createUserMessage("third"),
|
|
658
|
-
],
|
|
659
|
-
abortSignal: undefined,
|
|
660
|
-
});
|
|
661
|
-
await drainChunks(stream);
|
|
662
|
-
(0, vitest_1.expect)(appendBody.payload.message).toBeDefined();
|
|
663
|
-
(0, vitest_1.expect)(appendBody.payload.message.parts[0].text).toBe("third");
|
|
664
|
-
(0, vitest_1.expect)(appendBody.payload.messages).toBeUndefined();
|
|
665
|
-
});
|
|
666
|
-
(0, vitest_1.it)("for regenerate-message, no message is delivered to .in (server slices its own tail)", async () => {
|
|
667
|
-
// Slim wire: the regenerate trigger ships NO message — the agent
|
|
668
|
-
// trims the trailing assistant from its accumulator and re-runs from
|
|
669
|
-
// the prior user turn. The wire payload only carries the trigger
|
|
670
|
-
// discriminator + chatId + metadata.
|
|
671
|
-
let appendBody;
|
|
672
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
673
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
674
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
675
|
-
appendBody = JSON.parse(init.body);
|
|
676
|
-
return defaultAppendResponse();
|
|
677
|
-
}
|
|
678
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
679
|
-
return defaultSseResponse();
|
|
680
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
681
|
-
});
|
|
682
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
683
|
-
task: "my-chat-task",
|
|
684
|
-
accessToken: () => "pat",
|
|
685
|
-
sessions: { "chat-regen": { publicAccessToken: "p" } },
|
|
686
|
-
});
|
|
687
|
-
const stream = await transport.sendMessages({
|
|
688
|
-
trigger: "regenerate-message",
|
|
689
|
-
chatId: "chat-regen",
|
|
690
|
-
messageId: undefined,
|
|
691
|
-
messages: [createUserMessage("a"), createUserMessage("b")],
|
|
692
|
-
abortSignal: undefined,
|
|
693
|
-
});
|
|
694
|
-
await drainChunks(stream);
|
|
695
|
-
(0, vitest_1.expect)(appendBody.payload.trigger).toBe("regenerate-message");
|
|
696
|
-
(0, vitest_1.expect)(appendBody.payload.message).toBeUndefined();
|
|
697
|
-
(0, vitest_1.expect)(appendBody.payload.messages).toBeUndefined();
|
|
698
|
-
});
|
|
699
|
-
(0, vitest_1.it)("merges transport-level clientData into per-call metadata (per-call wins)", async () => {
|
|
700
|
-
let appendBody;
|
|
701
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
702
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
703
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
704
|
-
appendBody = JSON.parse(init.body);
|
|
705
|
-
return defaultAppendResponse();
|
|
706
|
-
}
|
|
707
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
708
|
-
return defaultSseResponse();
|
|
709
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
710
|
-
});
|
|
711
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
712
|
-
task: "my-chat-task",
|
|
713
|
-
accessToken: () => "pat",
|
|
714
|
-
clientData: { userId: "u1", scope: "default" },
|
|
715
|
-
sessions: { "chat-md": { publicAccessToken: "p" } },
|
|
716
|
-
});
|
|
717
|
-
const stream = await transport.sendMessages({
|
|
718
|
-
trigger: "submit-message",
|
|
719
|
-
chatId: "chat-md",
|
|
720
|
-
messageId: undefined,
|
|
721
|
-
messages: [createUserMessage("hi")],
|
|
722
|
-
abortSignal: undefined,
|
|
723
|
-
metadata: { scope: "request" },
|
|
724
|
-
});
|
|
725
|
-
await drainChunks(stream);
|
|
726
|
-
(0, vitest_1.expect)(appendBody.payload.metadata).toEqual({ userId: "u1", scope: "request" });
|
|
727
|
-
});
|
|
728
|
-
(0, vitest_1.it)("filters trigger:upgrade-required and continues reading", async () => {
|
|
729
|
-
const chunks = [
|
|
730
|
-
...sampleChunks.slice(0, 2),
|
|
731
|
-
{ type: "trigger:upgrade-required" },
|
|
732
|
-
...sampleChunks.slice(2),
|
|
733
|
-
{ type: "trigger:turn-complete" },
|
|
734
|
-
];
|
|
735
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
736
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
737
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
738
|
-
return defaultAppendResponse();
|
|
739
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
740
|
-
return defaultSseResponse(chunks);
|
|
741
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
742
|
-
});
|
|
743
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
744
|
-
task: "my-chat-task",
|
|
745
|
-
accessToken: () => "pat",
|
|
746
|
-
sessions: { "chat-up": { publicAccessToken: "p" } },
|
|
747
|
-
});
|
|
748
|
-
const stream = await transport.sendMessages({
|
|
749
|
-
trigger: "submit-message",
|
|
750
|
-
chatId: "chat-up",
|
|
751
|
-
messageId: undefined,
|
|
752
|
-
messages: [createUserMessage("hi")],
|
|
753
|
-
abortSignal: undefined,
|
|
754
|
-
});
|
|
755
|
-
const surfaced = await drainChunks(stream);
|
|
756
|
-
// Both control chunks are filtered.
|
|
757
|
-
(0, vitest_1.expect)(surfaced).toHaveLength(sampleChunks.length);
|
|
758
|
-
(0, vitest_1.expect)(surfaced.find((c) => c.type === "trigger:upgrade-required")).toBeUndefined();
|
|
759
|
-
(0, vitest_1.expect)(surfaced.find((c) => c.type === "trigger:turn-complete")).toBeUndefined();
|
|
760
|
-
});
|
|
761
|
-
(0, vitest_1.it)("clears isStreaming on turn-complete and notifies", async () => {
|
|
762
|
-
const onSessionChange = vitest_1.vi.fn();
|
|
763
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
764
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
765
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
766
|
-
return defaultAppendResponse();
|
|
767
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
768
|
-
return defaultSseResponse();
|
|
769
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
770
|
-
});
|
|
771
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
772
|
-
task: "my-chat-task",
|
|
773
|
-
accessToken: () => "pat",
|
|
774
|
-
onSessionChange,
|
|
775
|
-
sessions: { "chat-tc": { publicAccessToken: "p" } },
|
|
776
|
-
});
|
|
777
|
-
const stream = await transport.sendMessages({
|
|
778
|
-
trigger: "submit-message",
|
|
779
|
-
chatId: "chat-tc",
|
|
780
|
-
messageId: undefined,
|
|
781
|
-
messages: [createUserMessage("hi")],
|
|
782
|
-
abortSignal: undefined,
|
|
783
|
-
});
|
|
784
|
-
await drainChunks(stream);
|
|
785
|
-
const lastIsStreamingFalse = onSessionChange.mock.calls
|
|
786
|
-
.map((call) => call[1])
|
|
787
|
-
.reverse()
|
|
788
|
-
.find((s) => s !== null && s.isStreaming === false);
|
|
789
|
-
(0, vitest_1.expect)(lastIsStreamingFalse).toBeDefined();
|
|
790
|
-
});
|
|
791
|
-
});
|
|
792
|
-
(0, vitest_1.describe)("auth retry on 401", () => {
|
|
793
|
-
(0, vitest_1.it)("refreshes the PAT via accessToken and retries the .in/append once", async () => {
|
|
794
|
-
const accessToken = vitest_1.vi.fn().mockResolvedValue("fresh-pat");
|
|
795
|
-
let appendCount = 0;
|
|
796
|
-
let appendAuth = null;
|
|
797
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
798
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
799
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
800
|
-
appendCount++;
|
|
801
|
-
if (appendCount === 1)
|
|
802
|
-
return authError(401);
|
|
803
|
-
appendAuth = new Headers(init?.headers).get("Authorization");
|
|
804
|
-
return defaultAppendResponse();
|
|
805
|
-
}
|
|
806
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
807
|
-
return defaultSseResponse();
|
|
808
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
809
|
-
});
|
|
810
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
811
|
-
task: "my-chat-task",
|
|
812
|
-
accessToken,
|
|
813
|
-
sessions: { "chat-401": { publicAccessToken: "stale-pat" } },
|
|
814
|
-
});
|
|
815
|
-
const stream = await transport.sendMessages({
|
|
816
|
-
trigger: "submit-message",
|
|
817
|
-
chatId: "chat-401",
|
|
818
|
-
messageId: undefined,
|
|
819
|
-
messages: [createUserMessage("hi")],
|
|
820
|
-
abortSignal: undefined,
|
|
821
|
-
});
|
|
822
|
-
await drainChunks(stream);
|
|
823
|
-
(0, vitest_1.expect)(accessToken).toHaveBeenCalledWith({ chatId: "chat-401" });
|
|
824
|
-
(0, vitest_1.expect)(appendCount).toBe(2);
|
|
825
|
-
(0, vitest_1.expect)(appendAuth).toBe("Bearer fresh-pat");
|
|
826
|
-
(0, vitest_1.expect)(transport.getSession("chat-401")?.publicAccessToken).toBe("fresh-pat");
|
|
827
|
-
});
|
|
828
|
-
});
|
|
829
|
-
(0, vitest_1.describe)("stopGeneration", () => {
|
|
830
|
-
(0, vitest_1.it)("posts {kind: stop} to .in/append and returns true", async () => {
|
|
831
|
-
let stopBody;
|
|
832
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
833
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
834
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
835
|
-
stopBody = JSON.parse(init.body);
|
|
836
|
-
return defaultAppendResponse();
|
|
837
|
-
}
|
|
838
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
839
|
-
});
|
|
840
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
841
|
-
task: "my-chat-task",
|
|
842
|
-
accessToken: () => "pat",
|
|
843
|
-
sessions: { "chat-stop": { publicAccessToken: "p" } },
|
|
844
|
-
});
|
|
845
|
-
const ok = await transport.stopGeneration("chat-stop");
|
|
846
|
-
(0, vitest_1.expect)(ok).toBe(true);
|
|
847
|
-
(0, vitest_1.expect)(stopBody).toEqual({ kind: "stop" });
|
|
848
|
-
});
|
|
849
|
-
(0, vitest_1.it)("returns false when there is no session for the chatId", async () => {
|
|
850
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
851
|
-
task: "my-chat-task",
|
|
852
|
-
accessToken: () => "pat",
|
|
853
|
-
});
|
|
854
|
-
const ok = await transport.stopGeneration("never-started");
|
|
855
|
-
(0, vitest_1.expect)(ok).toBe(false);
|
|
856
|
-
});
|
|
857
|
-
});
|
|
858
|
-
(0, vitest_1.describe)("sendAction", () => {
|
|
859
|
-
(0, vitest_1.it)("posts an action chunk to .in/append and subscribes to .out", async () => {
|
|
860
|
-
let actionBody;
|
|
861
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
862
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
863
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
864
|
-
actionBody = JSON.parse(init.body);
|
|
865
|
-
return defaultAppendResponse();
|
|
866
|
-
}
|
|
867
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
868
|
-
return defaultSseResponse();
|
|
869
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
870
|
-
});
|
|
871
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
872
|
-
task: "my-chat-task",
|
|
873
|
-
accessToken: () => "pat",
|
|
874
|
-
sessions: { "chat-act": { publicAccessToken: "p" } },
|
|
875
|
-
});
|
|
876
|
-
const stream = await transport.sendAction("chat-act", { type: "undo" });
|
|
877
|
-
await drainChunks(stream);
|
|
878
|
-
(0, vitest_1.expect)(actionBody.kind).toBe("message");
|
|
879
|
-
(0, vitest_1.expect)(actionBody.payload.trigger).toBe("action");
|
|
880
|
-
(0, vitest_1.expect)(actionBody.payload.action).toEqual({ type: "undo" });
|
|
881
|
-
});
|
|
882
|
-
(0, vitest_1.it)("marks the session streaming and notifies before subscribing", async () => {
|
|
883
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
884
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
885
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
886
|
-
return new Response(JSON.stringify({ ok: true, seq: 7 }), { status: 200 });
|
|
887
|
-
}
|
|
888
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
889
|
-
return defaultSseResponse();
|
|
890
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
891
|
-
});
|
|
892
|
-
const onSessionChange = vitest_1.vi.fn();
|
|
893
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
894
|
-
task: "my-chat-task",
|
|
895
|
-
accessToken: () => "pat",
|
|
896
|
-
onSessionChange,
|
|
897
|
-
sessions: { "chat-act-stream": { publicAccessToken: "p" } },
|
|
898
|
-
});
|
|
899
|
-
const stream = await transport.sendAction("chat-act-stream", { type: "undo" });
|
|
900
|
-
// isStreaming:true must be observed during the action — otherwise a reload
|
|
901
|
-
// mid-action sees a persisted isStreaming:false and never resumes.
|
|
902
|
-
(0, vitest_1.expect)(onSessionChange.mock.calls.some(([, session]) => session && session.isStreaming === true && session.activeInputSeq === 7)).toBe(true);
|
|
903
|
-
await drainChunks(stream);
|
|
904
|
-
});
|
|
905
|
-
});
|
|
906
|
-
(0, vitest_1.describe)("append idempotency header", () => {
|
|
907
|
-
(0, vitest_1.it)("the per-append X-Part-Id wins over a transport-wide headers override", async () => {
|
|
908
|
-
let appendPartId;
|
|
909
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
910
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
911
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
912
|
-
appendPartId = init.headers["X-Part-Id"];
|
|
913
|
-
return defaultAppendResponse();
|
|
914
|
-
}
|
|
915
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
916
|
-
return defaultSseResponse();
|
|
917
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
918
|
-
});
|
|
919
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
920
|
-
task: "my-chat-task",
|
|
921
|
-
accessToken: () => "pat",
|
|
922
|
-
headers: { "X-Part-Id": "STATIC-OVERRIDE" },
|
|
923
|
-
sessions: { "chat-hdr": { publicAccessToken: "p" } },
|
|
924
|
-
});
|
|
925
|
-
const stream = await transport.sendMessages({
|
|
926
|
-
trigger: "submit-message",
|
|
927
|
-
chatId: "chat-hdr",
|
|
928
|
-
messageId: undefined,
|
|
929
|
-
messages: [createUserMessage("hi")],
|
|
930
|
-
abortSignal: undefined,
|
|
931
|
-
});
|
|
932
|
-
await drainChunks(stream);
|
|
933
|
-
(0, vitest_1.expect)(appendPartId).toBeDefined();
|
|
934
|
-
(0, vitest_1.expect)(appendPartId).not.toBe("STATIC-OVERRIDE");
|
|
935
|
-
});
|
|
936
|
-
});
|
|
937
|
-
(0, vitest_1.describe)("reconnectToStream", () => {
|
|
938
|
-
(0, vitest_1.it)("returns null when no session exists", async () => {
|
|
939
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
940
|
-
task: "my-chat-task",
|
|
941
|
-
accessToken: () => "pat",
|
|
942
|
-
});
|
|
943
|
-
const result = await transport.reconnectToStream({ chatId: "missing" });
|
|
944
|
-
(0, vitest_1.expect)(result).toBeNull();
|
|
945
|
-
});
|
|
946
|
-
(0, vitest_1.it)("returns null when the session is hydrated with isStreaming=false", async () => {
|
|
947
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
948
|
-
task: "my-chat-task",
|
|
949
|
-
accessToken: () => "pat",
|
|
950
|
-
sessions: {
|
|
951
|
-
"chat-rc": { publicAccessToken: "p", isStreaming: false },
|
|
952
|
-
},
|
|
953
|
-
});
|
|
954
|
-
const result = await transport.reconnectToStream({ chatId: "chat-rc" });
|
|
955
|
-
(0, vitest_1.expect)(result).toBeNull();
|
|
956
|
-
});
|
|
957
|
-
(0, vitest_1.it)("resumes in watch mode when the session is hydrated with isStreaming=false", async () => {
|
|
958
|
-
let subscribeCount = 0;
|
|
959
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
960
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
961
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
962
|
-
subscribeCount++;
|
|
963
|
-
const response = defaultSseResponse([{ type: "text-delta", id: "p1", delta: "turn2" }]);
|
|
964
|
-
const headers = new Headers(response.headers);
|
|
965
|
-
headers.set("X-Session-Settled", "true");
|
|
966
|
-
return new Response(response.body, { status: 200, headers });
|
|
967
|
-
}
|
|
968
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
969
|
-
});
|
|
970
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
971
|
-
task: "my-chat-task",
|
|
972
|
-
accessToken: () => "pat",
|
|
973
|
-
watch: true,
|
|
974
|
-
sessions: {
|
|
975
|
-
"chat-rc-watch": { publicAccessToken: "p", isStreaming: false },
|
|
976
|
-
},
|
|
977
|
-
});
|
|
978
|
-
const stream = await transport.reconnectToStream({ chatId: "chat-rc-watch" });
|
|
979
|
-
(0, vitest_1.expect)(stream).not.toBeNull();
|
|
980
|
-
const chunks = await drainChunks(stream);
|
|
981
|
-
(0, vitest_1.expect)(subscribeCount).toBe(1);
|
|
982
|
-
(0, vitest_1.expect)(chunks).toEqual([{ type: "text-delta", id: "p1", delta: "turn2" }]);
|
|
983
|
-
});
|
|
984
|
-
(0, vitest_1.it)("opens an SSE subscription with the X-Peek-Settled header set", async () => {
|
|
985
|
-
let subscribeHeaders;
|
|
986
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
987
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
988
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
989
|
-
subscribeHeaders = new Headers(init?.headers);
|
|
990
|
-
return defaultSseResponse();
|
|
991
|
-
}
|
|
992
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
993
|
-
});
|
|
994
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
995
|
-
task: "my-chat-task",
|
|
996
|
-
accessToken: () => "pat",
|
|
997
|
-
sessions: {
|
|
998
|
-
"chat-rc-on": { publicAccessToken: "p", isStreaming: true },
|
|
999
|
-
},
|
|
1000
|
-
});
|
|
1001
|
-
const stream = await transport.reconnectToStream({ chatId: "chat-rc-on" });
|
|
1002
|
-
(0, vitest_1.expect)(stream).not.toBeNull();
|
|
1003
|
-
await drainChunks(stream);
|
|
1004
|
-
(0, vitest_1.expect)(subscribeHeaders?.get("X-Peek-Settled")).toBe("1");
|
|
1005
|
-
});
|
|
1006
|
-
});
|
|
1007
|
-
(0, vitest_1.describe)("stream body ends mid-turn", () => {
|
|
1008
|
-
(0, vitest_1.it)("resubscribes from the last event id when the close was not settled", async () => {
|
|
1009
|
-
const subscribeHeaders = [];
|
|
1010
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
1011
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1012
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1013
|
-
return defaultAppendResponse();
|
|
1014
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1015
|
-
subscribeHeaders.push(new Headers(init?.headers));
|
|
1016
|
-
// First connection ends mid-turn: one chunk, no turn-complete,
|
|
1017
|
-
// no `X-Session-Settled`.
|
|
1018
|
-
return subscribeHeaders.length === 1
|
|
1019
|
-
? defaultSseResponse([{ type: "text-start", id: "part-1" }])
|
|
1020
|
-
: defaultSseResponse([
|
|
1021
|
-
{ type: "text-delta", id: "part-1", delta: "resumed" },
|
|
1022
|
-
{ type: "trigger:turn-complete" },
|
|
1023
|
-
]);
|
|
1024
|
-
}
|
|
1025
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1026
|
-
});
|
|
1027
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1028
|
-
task: "my-chat-task",
|
|
1029
|
-
accessToken: () => "pat",
|
|
1030
|
-
sessions: { "chat-eof": { publicAccessToken: "p" } },
|
|
1031
|
-
});
|
|
1032
|
-
const stream = await transport.sendMessages({
|
|
1033
|
-
trigger: "submit-message",
|
|
1034
|
-
chatId: "chat-eof",
|
|
1035
|
-
messageId: undefined,
|
|
1036
|
-
messages: [createUserMessage("hi")],
|
|
1037
|
-
abortSignal: undefined,
|
|
1038
|
-
});
|
|
1039
|
-
const chunks = await drainChunks(stream);
|
|
1040
|
-
(0, vitest_1.expect)(subscribeHeaders).toHaveLength(2);
|
|
1041
|
-
(0, vitest_1.expect)(subscribeHeaders[1]?.get("Last-Event-ID")).toBe("1");
|
|
1042
|
-
(0, vitest_1.expect)(chunks).toEqual([
|
|
1043
|
-
{ type: "text-start", id: "part-1" },
|
|
1044
|
-
{ type: "text-delta", id: "part-1", delta: "resumed" },
|
|
1045
|
-
]);
|
|
1046
|
-
(0, vitest_1.expect)(transport.getSession("chat-eof")?.isStreaming).toBe(false);
|
|
1047
|
-
});
|
|
1048
|
-
(0, vitest_1.it)("stops and clears isStreaming when the close was settled", async () => {
|
|
1049
|
-
let subscribeCount = 0;
|
|
1050
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1051
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1052
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1053
|
-
return defaultAppendResponse();
|
|
1054
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1055
|
-
subscribeCount++;
|
|
1056
|
-
const response = defaultSseResponse([{ type: "text-start", id: "part-1" }]);
|
|
1057
|
-
const headers = new Headers(response.headers);
|
|
1058
|
-
headers.set("X-Session-Settled", "true");
|
|
1059
|
-
return new Response(response.body, { status: 200, headers });
|
|
1060
|
-
}
|
|
1061
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1062
|
-
});
|
|
1063
|
-
const onSessionChange = vitest_1.vi.fn();
|
|
1064
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1065
|
-
task: "my-chat-task",
|
|
1066
|
-
accessToken: () => "pat",
|
|
1067
|
-
onSessionChange,
|
|
1068
|
-
sessions: { "chat-settled": { publicAccessToken: "p" } },
|
|
1069
|
-
});
|
|
1070
|
-
const stream = await transport.sendMessages({
|
|
1071
|
-
trigger: "submit-message",
|
|
1072
|
-
chatId: "chat-settled",
|
|
1073
|
-
messageId: undefined,
|
|
1074
|
-
messages: [createUserMessage("hi")],
|
|
1075
|
-
abortSignal: undefined,
|
|
1076
|
-
});
|
|
1077
|
-
await drainChunks(stream);
|
|
1078
|
-
(0, vitest_1.expect)(subscribeCount).toBe(1);
|
|
1079
|
-
(0, vitest_1.expect)(transport.getSession("chat-settled")?.isStreaming).toBe(false);
|
|
1080
|
-
(0, vitest_1.expect)(onSessionChange.mock.calls.some(([, session]) => session && session.isStreaming === false)).toBe(true);
|
|
1081
|
-
});
|
|
1082
|
-
(0, vitest_1.it)("keeps streaming when every window delivers a single record", async () => {
|
|
1083
|
-
// One record per window arrives via `primed` on the resumed connection —
|
|
1084
|
-
// it must still re-earn the budget, otherwise a slow turn is truncated.
|
|
1085
|
-
const WINDOWS = 8;
|
|
1086
|
-
let subscribeCount = 0;
|
|
1087
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1088
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1089
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1090
|
-
return defaultAppendResponse();
|
|
1091
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1092
|
-
subscribeCount++;
|
|
1093
|
-
return subscribeCount > WINDOWS
|
|
1094
|
-
? defaultSseResponse([{ type: "trigger:turn-complete" }])
|
|
1095
|
-
: defaultSseResponse([
|
|
1096
|
-
{ type: "text-delta", id: "part-1", delta: `d${subscribeCount}` },
|
|
1097
|
-
]);
|
|
1098
|
-
}
|
|
1099
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1100
|
-
});
|
|
1101
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1102
|
-
task: "my-chat-task",
|
|
1103
|
-
accessToken: () => "pat",
|
|
1104
|
-
sessions: { "chat-slow": { publicAccessToken: "p" } },
|
|
1105
|
-
});
|
|
1106
|
-
const stream = await transport.sendMessages({
|
|
1107
|
-
trigger: "submit-message",
|
|
1108
|
-
chatId: "chat-slow",
|
|
1109
|
-
messageId: undefined,
|
|
1110
|
-
messages: [createUserMessage("hi")],
|
|
1111
|
-
abortSignal: undefined,
|
|
1112
|
-
});
|
|
1113
|
-
const chunks = await drainChunks(stream);
|
|
1114
|
-
(0, vitest_1.expect)(subscribeCount).toBe(WINDOWS + 1);
|
|
1115
|
-
(0, vitest_1.expect)(chunks).toHaveLength(WINDOWS);
|
|
1116
|
-
(0, vitest_1.expect)(transport.getSession("chat-slow")?.isStreaming).toBe(false);
|
|
1117
|
-
});
|
|
1118
|
-
(0, vitest_1.it)("surfaces an error after the resubscribe budget is exhausted", async () => {
|
|
1119
|
-
// Fake timers so the 100ms..1.6s backoffs don't cost real seconds.
|
|
1120
|
-
vitest_1.vi.useFakeTimers();
|
|
1121
|
-
try {
|
|
1122
|
-
let subscribeCount = 0;
|
|
1123
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1124
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1125
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1126
|
-
return defaultAppendResponse();
|
|
1127
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1128
|
-
subscribeCount++;
|
|
1129
|
-
// Never any records, never settled — the pathological case.
|
|
1130
|
-
return defaultSseResponse([]);
|
|
1131
|
-
}
|
|
1132
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1133
|
-
});
|
|
1134
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1135
|
-
task: "my-chat-task",
|
|
1136
|
-
accessToken: () => "pat",
|
|
1137
|
-
sessions: { "chat-empty": { publicAccessToken: "p" } },
|
|
1138
|
-
});
|
|
1139
|
-
const stream = await transport.sendMessages({
|
|
1140
|
-
trigger: "submit-message",
|
|
1141
|
-
chatId: "chat-empty",
|
|
1142
|
-
messageId: undefined,
|
|
1143
|
-
messages: [createUserMessage("hi")],
|
|
1144
|
-
abortSignal: undefined,
|
|
1145
|
-
});
|
|
1146
|
-
// A cut-off turn surfaces an error rather than reading as complete.
|
|
1147
|
-
// Attach the rejection assertion before advancing timers so the
|
|
1148
|
-
// rejection is never unhandled.
|
|
1149
|
-
const drained = drainChunks(stream);
|
|
1150
|
-
const rejects = (0, vitest_1.expect)(drained).rejects.toThrow(/reconnect budget exhausted/i);
|
|
1151
|
-
await vitest_1.vi.advanceTimersByTimeAsync(10_000);
|
|
1152
|
-
await rejects;
|
|
1153
|
-
// One initial connect plus the five-attempt resubscribe budget.
|
|
1154
|
-
(0, vitest_1.expect)(subscribeCount).toBe(6);
|
|
1155
|
-
// State is cleared before the throw, so a reload won't reopen a
|
|
1156
|
-
// doomed subscription.
|
|
1157
|
-
(0, vitest_1.expect)(transport.getSession("chat-empty")?.isStreaming).toBe(false);
|
|
1158
|
-
}
|
|
1159
|
-
finally {
|
|
1160
|
-
vitest_1.vi.useRealTimers();
|
|
1161
|
-
}
|
|
1162
|
-
});
|
|
1163
|
-
});
|
|
1164
|
-
(0, vitest_1.describe)("watch mode across long-poll window boundaries", () => {
|
|
1165
|
-
function settled(response) {
|
|
1166
|
-
const headers = new Headers(response.headers);
|
|
1167
|
-
headers.set("X-Session-Settled", "true");
|
|
1168
|
-
return new Response(response.body, { status: 200, headers });
|
|
1169
|
-
}
|
|
1170
|
-
(0, vitest_1.it)("resubscribes after a completed turn and receives a later wake", async () => {
|
|
1171
|
-
let subscribeCount = 0;
|
|
1172
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1173
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1174
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1175
|
-
subscribeCount++;
|
|
1176
|
-
// Window 1: a turn completes, then the body EOFs with no
|
|
1177
|
-
// settled header — the quiet long-poll boundary.
|
|
1178
|
-
return subscribeCount === 1
|
|
1179
|
-
? defaultSseResponse([
|
|
1180
|
-
{ type: "text-delta", id: "p1", delta: "turn1" },
|
|
1181
|
-
{ type: "trigger:turn-complete" },
|
|
1182
|
-
])
|
|
1183
|
-
: settled(defaultSseResponse([{ type: "text-delta", id: "p2", delta: "wake" }]));
|
|
1184
|
-
}
|
|
1185
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1186
|
-
});
|
|
1187
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1188
|
-
task: "my-chat-task",
|
|
1189
|
-
accessToken: () => "pat",
|
|
1190
|
-
watch: true,
|
|
1191
|
-
sessions: { "chat-watch-eof": { publicAccessToken: "p", isStreaming: true } },
|
|
1192
|
-
});
|
|
1193
|
-
const stream = await transport.reconnectToStream({ chatId: "chat-watch-eof" });
|
|
1194
|
-
const chunks = await drainChunks(stream);
|
|
1195
|
-
(0, vitest_1.expect)(subscribeCount).toBe(2);
|
|
1196
|
-
(0, vitest_1.expect)(chunks).toEqual([
|
|
1197
|
-
{ type: "text-delta", id: "p1", delta: "turn1" },
|
|
1198
|
-
{ type: "text-delta", id: "p2", delta: "wake" },
|
|
1199
|
-
]);
|
|
1200
|
-
});
|
|
1201
|
-
(0, vitest_1.it)("does not peek-settle an idle resubscribe, so the next turn is delivered", async () => {
|
|
1202
|
-
// Watch mode must NOT send X-Peek-Settled between turns: a settled peek
|
|
1203
|
-
// while no turn is in flight closes the standing subscription and the
|
|
1204
|
-
// viewer never sees turn 2. This mock plays the server's peek shortcut —
|
|
1205
|
-
// a peek request with nothing in flight settles — to prove the transport
|
|
1206
|
-
// long-polls instead.
|
|
1207
|
-
const subscribeHeaders = [];
|
|
1208
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
1209
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1210
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1211
|
-
subscribeHeaders.push(new Headers(init?.headers));
|
|
1212
|
-
const n = subscribeHeaders.length;
|
|
1213
|
-
if (n === 1) {
|
|
1214
|
-
// Turn 1 completes, then the body EOFs (no settled header).
|
|
1215
|
-
return defaultSseResponse([
|
|
1216
|
-
{ type: "text-delta", id: "p1", delta: "turn1" },
|
|
1217
|
-
{ type: "trigger:turn-complete" },
|
|
1218
|
-
]);
|
|
1219
|
-
}
|
|
1220
|
-
if (n === 2) {
|
|
1221
|
-
// Idle resubscribe. If it peeked, the server settles and the
|
|
1222
|
-
// subscription would close before turn 2; a long-poll delivers it.
|
|
1223
|
-
if (init && new Headers(init.headers).get("X-Peek-Settled")) {
|
|
1224
|
-
return settled(defaultSseResponse([]));
|
|
1225
|
-
}
|
|
1226
|
-
return defaultSseResponse([
|
|
1227
|
-
{ type: "text-delta", id: "p2", delta: "turn2" },
|
|
1228
|
-
{ type: "trigger:turn-complete" },
|
|
1229
|
-
]);
|
|
1230
|
-
}
|
|
1231
|
-
// Turn 2 done — end the watch cleanly.
|
|
1232
|
-
return settled(defaultSseResponse([]));
|
|
1233
|
-
}
|
|
1234
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1235
|
-
});
|
|
1236
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1237
|
-
task: "my-chat-task",
|
|
1238
|
-
accessToken: () => "pat",
|
|
1239
|
-
watch: true,
|
|
1240
|
-
sessions: { "chat-watch-turn2": { publicAccessToken: "p", isStreaming: true } },
|
|
1241
|
-
});
|
|
1242
|
-
const stream = await transport.reconnectToStream({ chatId: "chat-watch-turn2" });
|
|
1243
|
-
const chunks = await drainChunks(stream);
|
|
1244
|
-
(0, vitest_1.expect)(subscribeHeaders[1]?.get("X-Peek-Settled")).toBeNull();
|
|
1245
|
-
(0, vitest_1.expect)(chunks).toEqual([
|
|
1246
|
-
{ type: "text-delta", id: "p1", delta: "turn1" },
|
|
1247
|
-
{ type: "text-delta", id: "p2", delta: "turn2" },
|
|
1248
|
-
]);
|
|
1249
|
-
});
|
|
1250
|
-
(0, vitest_1.it)("cancelling the reader stops the resubscribe loop", async () => {
|
|
1251
|
-
// A consumer that stops reading without aborting must not leak the
|
|
1252
|
-
// resubscribe loop — the stream's cancel() aborts it.
|
|
1253
|
-
vitest_1.vi.useFakeTimers();
|
|
1254
|
-
try {
|
|
1255
|
-
let subscribeCount = 0;
|
|
1256
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1257
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1258
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1259
|
-
subscribeCount++;
|
|
1260
|
-
// Quiet: EOF, no records, never settled — watch keeps resubscribing.
|
|
1261
|
-
return defaultSseResponse([]);
|
|
1262
|
-
}
|
|
1263
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1264
|
-
});
|
|
1265
|
-
const events = [];
|
|
1266
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1267
|
-
task: "my-chat-task",
|
|
1268
|
-
accessToken: () => "pat",
|
|
1269
|
-
watch: true,
|
|
1270
|
-
onEvent: (e) => events.push(e),
|
|
1271
|
-
sessions: { "chat-watch-cancel": { publicAccessToken: "p", isStreaming: true } },
|
|
1272
|
-
});
|
|
1273
|
-
const stream = await transport.reconnectToStream({ chatId: "chat-watch-cancel" });
|
|
1274
|
-
const reader = stream.getReader();
|
|
1275
|
-
await vitest_1.vi.advanceTimersByTimeAsync(10_000);
|
|
1276
|
-
(0, vitest_1.expect)(subscribeCount).toBeGreaterThan(1);
|
|
1277
|
-
const countAtCancel = subscribeCount;
|
|
1278
|
-
await reader.cancel();
|
|
1279
|
-
await vitest_1.vi.advanceTimersByTimeAsync(10_000);
|
|
1280
|
-
(0, vitest_1.expect)(subscribeCount).toBe(countAtCancel);
|
|
1281
|
-
// A clean cancel must not surface a spurious stream-error (an
|
|
1282
|
-
// unguarded controller.close() after cancel would throw "Invalid
|
|
1283
|
-
// state" and leak it onto the telemetry channel).
|
|
1284
|
-
(0, vitest_1.expect)(events.some((e) => e.type === "stream-error")).toBe(false);
|
|
1285
|
-
}
|
|
1286
|
-
finally {
|
|
1287
|
-
vitest_1.vi.useRealTimers();
|
|
1288
|
-
}
|
|
1289
|
-
});
|
|
1290
|
-
(0, vitest_1.it)("stops when the server says the session settled", async () => {
|
|
1291
|
-
let subscribeCount = 0;
|
|
1292
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1293
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1294
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1295
|
-
subscribeCount++;
|
|
1296
|
-
return settled(defaultSseResponse([
|
|
1297
|
-
{ type: "text-delta", id: "p1", delta: "last" },
|
|
1298
|
-
{ type: "trigger:turn-complete" },
|
|
1299
|
-
]));
|
|
1300
|
-
}
|
|
1301
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1302
|
-
});
|
|
1303
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1304
|
-
task: "my-chat-task",
|
|
1305
|
-
accessToken: () => "pat",
|
|
1306
|
-
watch: true,
|
|
1307
|
-
sessions: { "chat-watch-settled": { publicAccessToken: "p", isStreaming: true } },
|
|
1308
|
-
});
|
|
1309
|
-
const stream = await transport.reconnectToStream({ chatId: "chat-watch-settled" });
|
|
1310
|
-
const chunks = await drainChunks(stream);
|
|
1311
|
-
(0, vitest_1.expect)(subscribeCount).toBe(1);
|
|
1312
|
-
(0, vitest_1.expect)(chunks).toHaveLength(1);
|
|
1313
|
-
(0, vitest_1.expect)(transport.getSession("chat-watch-settled")?.isStreaming).toBe(false);
|
|
1314
|
-
});
|
|
1315
|
-
(0, vitest_1.it)("stops promptly when aborted during backoff", async () => {
|
|
1316
|
-
vitest_1.vi.useFakeTimers();
|
|
1317
|
-
try {
|
|
1318
|
-
let subscribeCount = 0;
|
|
1319
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1320
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1321
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1322
|
-
return defaultAppendResponse();
|
|
1323
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1324
|
-
subscribeCount++;
|
|
1325
|
-
// Every window is quiet: EOF with no records, never settled.
|
|
1326
|
-
return defaultSseResponse([]);
|
|
1327
|
-
}
|
|
1328
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1329
|
-
});
|
|
1330
|
-
const abortController = new AbortController();
|
|
1331
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1332
|
-
task: "my-chat-task",
|
|
1333
|
-
accessToken: () => "pat",
|
|
1334
|
-
watch: true,
|
|
1335
|
-
sessions: { "chat-watch-abort": { publicAccessToken: "p", isStreaming: true } },
|
|
1336
|
-
});
|
|
1337
|
-
const stream = await transport.reconnectToStream({
|
|
1338
|
-
chatId: "chat-watch-abort",
|
|
1339
|
-
abortSignal: abortController.signal,
|
|
1340
|
-
});
|
|
1341
|
-
const drained = drainChunks(stream);
|
|
1342
|
-
await vitest_1.vi.advanceTimersByTimeAsync(10_000);
|
|
1343
|
-
// The budget doesn't apply in watch mode, so it is still reconnecting.
|
|
1344
|
-
(0, vitest_1.expect)(subscribeCount).toBeGreaterThan(6);
|
|
1345
|
-
const countAtAbort = subscribeCount;
|
|
1346
|
-
abortController.abort();
|
|
1347
|
-
await drained;
|
|
1348
|
-
await vitest_1.vi.advanceTimersByTimeAsync(10_000);
|
|
1349
|
-
(0, vitest_1.expect)(subscribeCount).toBe(countAtAbort);
|
|
1350
|
-
}
|
|
1351
|
-
finally {
|
|
1352
|
-
vitest_1.vi.useRealTimers();
|
|
1353
|
-
}
|
|
1354
|
-
});
|
|
1355
|
-
});
|
|
1356
|
-
(0, vitest_1.describe)("reconnectToStream stop-on-abort ownership (TRI-13070)", () => {
|
|
1357
|
-
// A quiet stream: EOF, no records, never settled — the subscription
|
|
1358
|
-
// stays alive (watch mode) so an abort mid-flight exercises the stop path.
|
|
1359
|
-
function quietWatchTransport() {
|
|
1360
|
-
let appendCount = 0;
|
|
1361
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1362
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1363
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
1364
|
-
appendCount++;
|
|
1365
|
-
return defaultAppendResponse();
|
|
1366
|
-
}
|
|
1367
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
1368
|
-
return defaultSseResponse([]);
|
|
1369
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1370
|
-
});
|
|
1371
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1372
|
-
task: "my-chat-task",
|
|
1373
|
-
accessToken: () => "pat",
|
|
1374
|
-
watch: true,
|
|
1375
|
-
sessions: { "chat-own": { publicAccessToken: "p", isStreaming: true } },
|
|
1376
|
-
});
|
|
1377
|
-
return { transport, appends: () => appendCount };
|
|
1378
|
-
}
|
|
1379
|
-
(0, vitest_1.it)("passive subscriber aborting writes no stop chunk to .in", async () => {
|
|
1380
|
-
vitest_1.vi.useFakeTimers();
|
|
1381
|
-
try {
|
|
1382
|
-
const { transport, appends } = quietWatchTransport();
|
|
1383
|
-
const abort = new AbortController();
|
|
1384
|
-
const stream = await transport.reconnectToStream({
|
|
1385
|
-
chatId: "chat-own",
|
|
1386
|
-
abortSignal: abort.signal,
|
|
1387
|
-
});
|
|
1388
|
-
const drained = drainChunks(stream);
|
|
1389
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1390
|
-
abort.abort();
|
|
1391
|
-
await drained;
|
|
1392
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1393
|
-
(0, vitest_1.expect)(appends()).toBe(0);
|
|
1394
|
-
}
|
|
1395
|
-
finally {
|
|
1396
|
-
vitest_1.vi.useRealTimers();
|
|
1397
|
-
}
|
|
1398
|
-
});
|
|
1399
|
-
(0, vitest_1.it)("owning subscriber with stopOnAbort:true sends a stop chunk on abort", async () => {
|
|
1400
|
-
vitest_1.vi.useFakeTimers();
|
|
1401
|
-
try {
|
|
1402
|
-
const { transport, appends } = quietWatchTransport();
|
|
1403
|
-
const abort = new AbortController();
|
|
1404
|
-
const stream = await transport.reconnectToStream({
|
|
1405
|
-
chatId: "chat-own",
|
|
1406
|
-
abortSignal: abort.signal,
|
|
1407
|
-
stopOnAbort: true,
|
|
1408
|
-
});
|
|
1409
|
-
const drained = drainChunks(stream);
|
|
1410
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1411
|
-
abort.abort();
|
|
1412
|
-
await drained;
|
|
1413
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1414
|
-
(0, vitest_1.expect)(appends()).toBe(1);
|
|
1415
|
-
}
|
|
1416
|
-
finally {
|
|
1417
|
-
vitest_1.vi.useRealTimers();
|
|
1418
|
-
}
|
|
1419
|
-
});
|
|
1420
|
-
(0, vitest_1.it)("abortSignal presence alone (stopOnAbort unset) sends no stop", async () => {
|
|
1421
|
-
vitest_1.vi.useFakeTimers();
|
|
1422
|
-
try {
|
|
1423
|
-
const { transport, appends } = quietWatchTransport();
|
|
1424
|
-
const abort = new AbortController();
|
|
1425
|
-
const stream = await transport.reconnectToStream({
|
|
1426
|
-
chatId: "chat-own",
|
|
1427
|
-
abortSignal: abort.signal,
|
|
1428
|
-
});
|
|
1429
|
-
const drained = drainChunks(stream);
|
|
1430
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1431
|
-
abort.abort();
|
|
1432
|
-
await drained;
|
|
1433
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1434
|
-
(0, vitest_1.expect)(appends()).toBe(0);
|
|
1435
|
-
}
|
|
1436
|
-
finally {
|
|
1437
|
-
vitest_1.vi.useRealTimers();
|
|
1438
|
-
}
|
|
1439
|
-
});
|
|
1440
|
-
});
|
|
1441
|
-
(0, vitest_1.describe)("superseded stream teardown", () => {
|
|
1442
|
-
(0, vitest_1.it)("keeps the successor's controller registered when the aborted stream tears down", async () => {
|
|
1443
|
-
vitest_1.vi.useFakeTimers();
|
|
1444
|
-
try {
|
|
1445
|
-
let appendCount = 0;
|
|
1446
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1447
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1448
|
-
if (isSessionStreamAppendUrl(urlStr)) {
|
|
1449
|
-
appendCount++;
|
|
1450
|
-
return defaultAppendResponse();
|
|
1451
|
-
}
|
|
1452
|
-
// Quiet stream: EOF, no records, never settled — watch keeps it open.
|
|
1453
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
1454
|
-
return defaultSseResponse([]);
|
|
1455
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1456
|
-
});
|
|
1457
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1458
|
-
task: "my-chat-task",
|
|
1459
|
-
accessToken: () => "pat",
|
|
1460
|
-
watch: true,
|
|
1461
|
-
sessions: { "chat-race": { publicAccessToken: "p", isStreaming: true } },
|
|
1462
|
-
});
|
|
1463
|
-
const send = () => transport.sendMessages({
|
|
1464
|
-
trigger: "submit-message",
|
|
1465
|
-
chatId: "chat-race",
|
|
1466
|
-
messageId: undefined,
|
|
1467
|
-
messages: [createUserMessage("hi")],
|
|
1468
|
-
abortSignal: undefined,
|
|
1469
|
-
});
|
|
1470
|
-
const first = drainChunks(await send());
|
|
1471
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1472
|
-
// Supersede: the new stream registers its controller synchronously,
|
|
1473
|
-
// the aborted one tears down a microtask later.
|
|
1474
|
-
const second = await send();
|
|
1475
|
-
let secondClosed = false;
|
|
1476
|
-
const secondDrain = drainChunks(second).then(() => {
|
|
1477
|
-
secondClosed = true;
|
|
1478
|
-
});
|
|
1479
|
-
await first;
|
|
1480
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1481
|
-
// stopGeneration posts the stop chunk either way — only the
|
|
1482
|
-
// closing assertion proves it found the successor to abort.
|
|
1483
|
-
appendCount = 0;
|
|
1484
|
-
(0, vitest_1.expect)(await transport.stopGeneration("chat-race")).toBe(true);
|
|
1485
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1486
|
-
(0, vitest_1.expect)(appendCount).toBe(1);
|
|
1487
|
-
(0, vitest_1.expect)(secondClosed).toBe(true);
|
|
1488
|
-
transport.dispose();
|
|
1489
|
-
await secondDrain;
|
|
1490
|
-
}
|
|
1491
|
-
finally {
|
|
1492
|
-
vitest_1.vi.useRealTimers();
|
|
1493
|
-
}
|
|
1494
|
-
});
|
|
1495
|
-
(0, vitest_1.it)("keeps the tab claim the successor took (multi-tab)", async () => {
|
|
1496
|
-
vitest_1.vi.useFakeTimers();
|
|
1497
|
-
try {
|
|
1498
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
1499
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1500
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1501
|
-
return defaultAppendResponse();
|
|
1502
|
-
// Open SSE that only ends when the subscription is aborted, so
|
|
1503
|
-
// the superseded stream tears down while the successor is live.
|
|
1504
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
1505
|
-
return openSseResponse(init?.signal);
|
|
1506
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1507
|
-
});
|
|
1508
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1509
|
-
task: "my-chat-task",
|
|
1510
|
-
accessToken: () => "pat",
|
|
1511
|
-
multiTab: true,
|
|
1512
|
-
sessions: { "chat-race-tab": { publicAccessToken: "p", isStreaming: true } },
|
|
1513
|
-
});
|
|
1514
|
-
const send = () => transport.sendMessages({
|
|
1515
|
-
trigger: "submit-message",
|
|
1516
|
-
chatId: "chat-race-tab",
|
|
1517
|
-
messageId: undefined,
|
|
1518
|
-
messages: [createUserMessage("hi")],
|
|
1519
|
-
abortSignal: undefined,
|
|
1520
|
-
});
|
|
1521
|
-
const first = drainChunks(await send());
|
|
1522
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1523
|
-
const secondDrain = drainChunks(await send());
|
|
1524
|
-
await first;
|
|
1525
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1526
|
-
// The superseded stream must not release the claim its successor
|
|
1527
|
-
// holds — otherwise this tab flips to read-only mid-turn.
|
|
1528
|
-
(0, vitest_1.expect)(transport.hasClaim("chat-race-tab")).toBe(true);
|
|
1529
|
-
transport.dispose();
|
|
1530
|
-
await secondDrain;
|
|
1531
|
-
}
|
|
1532
|
-
finally {
|
|
1533
|
-
vitest_1.vi.useRealTimers();
|
|
1534
|
-
}
|
|
1535
|
-
});
|
|
1536
|
-
(0, vitest_1.it)("releases the tab claim when the user stops generation (multi-tab)", async () => {
|
|
1537
|
-
vitest_1.vi.useFakeTimers();
|
|
1538
|
-
try {
|
|
1539
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
1540
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1541
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1542
|
-
return defaultAppendResponse();
|
|
1543
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
1544
|
-
return openSseResponse(init?.signal);
|
|
1545
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1546
|
-
});
|
|
1547
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1548
|
-
task: "my-chat-task",
|
|
1549
|
-
accessToken: () => "pat",
|
|
1550
|
-
multiTab: true,
|
|
1551
|
-
sessions: { "chat-stop-tab": { publicAccessToken: "p", isStreaming: true } },
|
|
1552
|
-
});
|
|
1553
|
-
const drain = drainChunks(await transport.sendMessages({
|
|
1554
|
-
trigger: "submit-message",
|
|
1555
|
-
chatId: "chat-stop-tab",
|
|
1556
|
-
messageId: undefined,
|
|
1557
|
-
messages: [createUserMessage("hi")],
|
|
1558
|
-
abortSignal: undefined,
|
|
1559
|
-
}));
|
|
1560
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1561
|
-
(0, vitest_1.expect)(transport.hasClaim("chat-stop-tab")).toBe(true);
|
|
1562
|
-
(0, vitest_1.expect)(await transport.stopGeneration("chat-stop-tab")).toBe(true);
|
|
1563
|
-
await vitest_1.vi.advanceTimersByTimeAsync(1_000);
|
|
1564
|
-
// The turn ends here with no successor stream, so the claim must be
|
|
1565
|
-
// freed or other tabs stay read-only until this one closes.
|
|
1566
|
-
(0, vitest_1.expect)(transport.hasClaim("chat-stop-tab")).toBe(false);
|
|
1567
|
-
transport.dispose();
|
|
1568
|
-
await drain;
|
|
1569
|
-
}
|
|
1570
|
-
finally {
|
|
1571
|
-
vitest_1.vi.useRealTimers();
|
|
1572
|
-
}
|
|
1573
|
-
});
|
|
1574
|
-
});
|
|
1575
|
-
(0, vitest_1.describe)("multi-tab coordination", () => {
|
|
1576
|
-
(0, vitest_1.it)("isReadOnly defaults to false when multiTab is disabled", () => {
|
|
1577
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1578
|
-
task: "my-chat-task",
|
|
1579
|
-
accessToken: () => "pat",
|
|
1580
|
-
});
|
|
1581
|
-
(0, vitest_1.expect)(transport.isReadOnly("any-chat")).toBe(false);
|
|
1582
|
-
(0, vitest_1.expect)(transport.hasClaim("any-chat")).toBe(false);
|
|
1583
|
-
});
|
|
1584
|
-
});
|
|
1585
|
-
(0, vitest_1.describe)("endpoint (chat.handover routing)", () => {
|
|
1586
|
-
/**
|
|
1587
|
-
* Encode UIMessageChunks the same way the chat-server.ts handler
|
|
1588
|
-
* does: `data: <JSON>\n\n` per chunk. The transport's
|
|
1589
|
-
* `parseUIMessageSseTransform` parses this back into chunk objects.
|
|
1590
|
-
*/
|
|
1591
|
-
function handoverSseBody(chunks) {
|
|
1592
|
-
const encoder = new TextEncoder();
|
|
1593
|
-
return new ReadableStream({
|
|
1594
|
-
start(controller) {
|
|
1595
|
-
for (const chunk of chunks) {
|
|
1596
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
|
1597
|
-
}
|
|
1598
|
-
controller.close();
|
|
1599
|
-
},
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
function handoverResponse(args) {
|
|
1603
|
-
return new Response(handoverSseBody(args.chunks), {
|
|
1604
|
-
status: 200,
|
|
1605
|
-
headers: {
|
|
1606
|
-
"content-type": "text/event-stream",
|
|
1607
|
-
"X-Trigger-Chat-Id": args.chatId,
|
|
1608
|
-
"X-Trigger-Chat-Access-Token": args.accessToken,
|
|
1609
|
-
},
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
(0, vitest_1.it)("first-turn POSTs the wire payload to endpoint when no session exists", async () => {
|
|
1613
|
-
const requests = [];
|
|
1614
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
1615
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1616
|
-
requests.push({ url: urlStr, init });
|
|
1617
|
-
if (urlStr === "https://my-app.example/api/chat") {
|
|
1618
|
-
return handoverResponse({
|
|
1619
|
-
chatId: "chat-handover-1",
|
|
1620
|
-
accessToken: "handover-pat-1",
|
|
1621
|
-
chunks: sampleChunks,
|
|
1622
|
-
});
|
|
1623
|
-
}
|
|
1624
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1625
|
-
});
|
|
1626
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1627
|
-
task: "my-chat-task",
|
|
1628
|
-
accessToken: () => "pat",
|
|
1629
|
-
headStart: "https://my-app.example/api/chat",
|
|
1630
|
-
});
|
|
1631
|
-
const stream = await transport.sendMessages({
|
|
1632
|
-
trigger: "submit-message",
|
|
1633
|
-
chatId: "chat-handover-1",
|
|
1634
|
-
messageId: "m1",
|
|
1635
|
-
messages: [createUserMessage("hello")],
|
|
1636
|
-
abortSignal: undefined,
|
|
1637
|
-
});
|
|
1638
|
-
const chunks = await drainChunks(stream);
|
|
1639
|
-
// Chunks were forwarded from the handler's SSE body unchanged.
|
|
1640
|
-
(0, vitest_1.expect)(chunks).toEqual(sampleChunks);
|
|
1641
|
-
// Only the endpoint was called — no /api/v1/sessions, no .in/append,
|
|
1642
|
-
// no .out subscribe. The handler owns first-turn end-to-end.
|
|
1643
|
-
const endpointPosts = requests.filter((r) => r.url === "https://my-app.example/api/chat");
|
|
1644
|
-
(0, vitest_1.expect)(endpointPosts).toHaveLength(1);
|
|
1645
|
-
(0, vitest_1.expect)(requests.some((r) => isSessionCreateUrl(r.url))).toBe(false);
|
|
1646
|
-
(0, vitest_1.expect)(requests.some((r) => isSessionStreamAppendUrl(r.url))).toBe(false);
|
|
1647
|
-
(0, vitest_1.expect)(requests.some((r) => isSessionOutSubscribeUrl(r.url))).toBe(false);
|
|
1648
|
-
// Body shape: head-start wire payload. Full UIMessage history is
|
|
1649
|
-
// shipped via `headStartMessages` (this is the one path that still
|
|
1650
|
-
// ships full history — the route handler runs against the customer's
|
|
1651
|
-
// own HTTP endpoint, not /in/append, so the 512 KiB cap doesn't
|
|
1652
|
-
// apply). The `message` field is omitted on this path.
|
|
1653
|
-
const body = JSON.parse(endpointPosts[0].init.body);
|
|
1654
|
-
(0, vitest_1.expect)(body.chatId).toBe("chat-handover-1");
|
|
1655
|
-
(0, vitest_1.expect)(body.trigger).toBe("submit-message");
|
|
1656
|
-
(0, vitest_1.expect)(body.messageId).toBe("m1");
|
|
1657
|
-
(0, vitest_1.expect)(body.headStartMessages).toHaveLength(1);
|
|
1658
|
-
(0, vitest_1.expect)(body.message).toBeUndefined();
|
|
1659
|
-
(0, vitest_1.expect)(body.messages).toBeUndefined();
|
|
1660
|
-
});
|
|
1661
|
-
(0, vitest_1.it)("hydrates session state from response headers so subsequent turns bypass the endpoint", async () => {
|
|
1662
|
-
const requests = [];
|
|
1663
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
1664
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1665
|
-
requests.push({ url: urlStr, init });
|
|
1666
|
-
if (urlStr === "https://my-app.example/api/chat") {
|
|
1667
|
-
return handoverResponse({
|
|
1668
|
-
chatId: "chat-handover-2",
|
|
1669
|
-
accessToken: "handover-pat-2",
|
|
1670
|
-
chunks: sampleChunks,
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1674
|
-
return defaultAppendResponse();
|
|
1675
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
1676
|
-
return defaultSseResponse();
|
|
1677
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1678
|
-
});
|
|
1679
|
-
const onSessionChange = vitest_1.vi.fn();
|
|
1680
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1681
|
-
task: "my-chat-task",
|
|
1682
|
-
accessToken: () => "fallback-pat",
|
|
1683
|
-
headStart: "https://my-app.example/api/chat",
|
|
1684
|
-
onSessionChange,
|
|
1685
|
-
});
|
|
1686
|
-
// Turn 1 — POSTs to endpoint, hydrates session.
|
|
1687
|
-
await drainChunks(await transport.sendMessages({
|
|
1688
|
-
trigger: "submit-message",
|
|
1689
|
-
chatId: "chat-handover-2",
|
|
1690
|
-
messageId: "m1",
|
|
1691
|
-
messages: [createUserMessage("first")],
|
|
1692
|
-
abortSignal: undefined,
|
|
1693
|
-
}));
|
|
1694
|
-
const hydrated = transport.getSession("chat-handover-2");
|
|
1695
|
-
(0, vitest_1.expect)(hydrated).toBeDefined();
|
|
1696
|
-
(0, vitest_1.expect)(hydrated.publicAccessToken).toBe("handover-pat-2");
|
|
1697
|
-
(0, vitest_1.expect)(onSessionChange).toHaveBeenCalledWith("chat-handover-2", vitest_1.expect.objectContaining({ publicAccessToken: "handover-pat-2" }));
|
|
1698
|
-
// Turn 2 — bypass endpoint, write directly to .in.
|
|
1699
|
-
requests.length = 0;
|
|
1700
|
-
const turn2Stream = await transport.sendMessages({
|
|
1701
|
-
trigger: "submit-message",
|
|
1702
|
-
chatId: "chat-handover-2",
|
|
1703
|
-
messageId: "m2",
|
|
1704
|
-
messages: [createUserMessage("second")],
|
|
1705
|
-
abortSignal: undefined,
|
|
1706
|
-
});
|
|
1707
|
-
(0, vitest_1.expect)(requests.some((r) => r.url === "https://my-app.example/api/chat")).toBe(false);
|
|
1708
|
-
const append = requests.find((r) => isSessionStreamAppendUrl(r.url) && r.url.endsWith("/in/append"));
|
|
1709
|
-
(0, vitest_1.expect)(append).toBeDefined();
|
|
1710
|
-
(0, vitest_1.expect)(chatIdFromUrl(append.url)).toBe("chat-handover-2");
|
|
1711
|
-
// Drain after asserting append — `.out` is subscribed lazily when the
|
|
1712
|
-
// returned stream is read.
|
|
1713
|
-
await drainChunks(turn2Stream);
|
|
1714
|
-
const subscribe = requests.find((r) => isSessionOutSubscribeUrl(r.url));
|
|
1715
|
-
(0, vitest_1.expect)(subscribe).toBeDefined();
|
|
1716
|
-
});
|
|
1717
|
-
(0, vitest_1.it)("bypasses endpoint when a session is already hydrated (page reload after first turn)", async () => {
|
|
1718
|
-
const requests = [];
|
|
1719
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
1720
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1721
|
-
requests.push({ url: urlStr, init });
|
|
1722
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1723
|
-
return defaultAppendResponse();
|
|
1724
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
1725
|
-
return defaultSseResponse();
|
|
1726
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1727
|
-
});
|
|
1728
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1729
|
-
task: "my-chat-task",
|
|
1730
|
-
accessToken: () => "pat",
|
|
1731
|
-
headStart: "https://my-app.example/api/chat",
|
|
1732
|
-
sessions: {
|
|
1733
|
-
"chat-resumed": { publicAccessToken: "persisted-pat" },
|
|
1734
|
-
},
|
|
1735
|
-
});
|
|
1736
|
-
await drainChunks(await transport.sendMessages({
|
|
1737
|
-
trigger: "submit-message",
|
|
1738
|
-
chatId: "chat-resumed",
|
|
1739
|
-
messageId: undefined,
|
|
1740
|
-
messages: [createUserMessage("hi again")],
|
|
1741
|
-
abortSignal: undefined,
|
|
1742
|
-
}));
|
|
1743
|
-
(0, vitest_1.expect)(requests.some((r) => r.url === "https://my-app.example/api/chat")).toBe(false);
|
|
1744
|
-
(0, vitest_1.expect)(requests.some((r) => isSessionStreamAppendUrl(r.url))).toBe(true);
|
|
1745
|
-
});
|
|
1746
|
-
(0, vitest_1.it)("propagates a non-2xx response from the endpoint as an error", async () => {
|
|
1747
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1748
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1749
|
-
if (urlStr === "https://my-app.example/api/chat") {
|
|
1750
|
-
return new Response(null, { status: 500, statusText: "Internal Server Error" });
|
|
1751
|
-
}
|
|
1752
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1753
|
-
});
|
|
1754
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1755
|
-
task: "my-chat-task",
|
|
1756
|
-
accessToken: () => "pat",
|
|
1757
|
-
headStart: "https://my-app.example/api/chat",
|
|
1758
|
-
});
|
|
1759
|
-
await (0, vitest_1.expect)(transport.sendMessages({
|
|
1760
|
-
trigger: "submit-message",
|
|
1761
|
-
chatId: "chat-handover-err",
|
|
1762
|
-
messageId: undefined,
|
|
1763
|
-
messages: [createUserMessage("oops")],
|
|
1764
|
-
abortSignal: undefined,
|
|
1765
|
-
})).rejects.toThrow(/500/);
|
|
1766
|
-
});
|
|
1767
|
-
(0, vitest_1.it)("leaves the legacy direct-trigger path unchanged when endpoint is unset", async () => {
|
|
1768
|
-
const requests = [];
|
|
1769
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1770
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1771
|
-
requests.push(urlStr);
|
|
1772
|
-
if (isSessionStreamAppendUrl(urlStr))
|
|
1773
|
-
return defaultAppendResponse();
|
|
1774
|
-
if (isSessionOutSubscribeUrl(urlStr))
|
|
1775
|
-
return defaultSseResponse();
|
|
1776
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1777
|
-
});
|
|
1778
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1779
|
-
task: "my-chat-task",
|
|
1780
|
-
accessToken: () => "pat",
|
|
1781
|
-
// endpoint NOT set
|
|
1782
|
-
sessions: { "chat-legacy": { publicAccessToken: "p" } },
|
|
1783
|
-
});
|
|
1784
|
-
await drainChunks(await transport.sendMessages({
|
|
1785
|
-
trigger: "submit-message",
|
|
1786
|
-
chatId: "chat-legacy",
|
|
1787
|
-
messageId: undefined,
|
|
1788
|
-
messages: [createUserMessage("legacy")],
|
|
1789
|
-
abortSignal: undefined,
|
|
1790
|
-
}));
|
|
1791
|
-
// No POST to /api/chat anywhere.
|
|
1792
|
-
(0, vitest_1.expect)(requests.some((u) => u.endsWith("/api/chat"))).toBe(false);
|
|
1793
|
-
(0, vitest_1.expect)(requests.some(isSessionStreamAppendUrl)).toBe(true);
|
|
1794
|
-
(0, vitest_1.expect)(requests.some(isSessionOutSubscribeUrl)).toBe(true);
|
|
1795
|
-
});
|
|
1796
|
-
});
|
|
1797
|
-
(0, vitest_1.describe)("watch mode", () => {
|
|
1798
|
-
(0, vitest_1.it)("keeps the SSE open across trigger:turn-complete (multi-turn watch)", async () => {
|
|
1799
|
-
const turn1 = [
|
|
1800
|
-
{ type: "text-delta", id: "p1", delta: "Hi" },
|
|
1801
|
-
{ type: "trigger:turn-complete" },
|
|
1802
|
-
{ type: "text-delta", id: "p2", delta: "Again" },
|
|
1803
|
-
{ type: "trigger:turn-complete" },
|
|
1804
|
-
];
|
|
1805
|
-
let subscribeCount = 0;
|
|
1806
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url) => {
|
|
1807
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1808
|
-
if (isSessionOutSubscribeUrl(urlStr)) {
|
|
1809
|
-
subscribeCount++;
|
|
1810
|
-
if (subscribeCount === 1)
|
|
1811
|
-
return defaultSseResponse(turn1);
|
|
1812
|
-
// Watch mode reconnects past the body EOF; settle so the drain ends.
|
|
1813
|
-
const response = defaultSseResponse([]);
|
|
1814
|
-
const headers = new Headers(response.headers);
|
|
1815
|
-
headers.set("X-Session-Settled", "true");
|
|
1816
|
-
return new Response(response.body, { status: 200, headers });
|
|
1817
|
-
}
|
|
1818
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
1819
|
-
});
|
|
1820
|
-
const transport = new chat_js_1.TriggerChatTransport({
|
|
1821
|
-
task: "my-chat-task",
|
|
1822
|
-
accessToken: () => "pat",
|
|
1823
|
-
watch: true,
|
|
1824
|
-
sessions: {
|
|
1825
|
-
"chat-watch": { publicAccessToken: "p", isStreaming: true },
|
|
1826
|
-
},
|
|
1827
|
-
});
|
|
1828
|
-
const stream = await transport.reconnectToStream({ chatId: "chat-watch" });
|
|
1829
|
-
const surfaced = await drainChunks(stream);
|
|
1830
|
-
// Both trigger:turn-complete control chunks filtered; both
|
|
1831
|
-
// text-deltas surfaced because watch mode kept the loop alive
|
|
1832
|
-
// through the first turn-complete.
|
|
1833
|
-
const textChunks = surfaced.filter((c) => c.type === "text-delta");
|
|
1834
|
-
(0, vitest_1.expect)(textChunks).toHaveLength(2);
|
|
1835
|
-
});
|
|
1836
|
-
});
|
|
1837
|
-
});
|
|
1838
|
-
//# sourceMappingURL=chat.test.js.map
|