@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,713 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const vitest_1 = require("vitest");
|
|
4
|
-
const ai_1 = require("ai");
|
|
5
|
-
const test_1 = require("ai/test");
|
|
6
|
-
// Stub `SessionStreamInstance` so the handler's S2 tee is a no-op
|
|
7
|
-
// instead of trying to reach a real S2 endpoint. The real one calls
|
|
8
|
-
// `apiClient.initializeSessionStream` then pipes via S2 — both are
|
|
9
|
-
// out of scope for handler-shape tests.
|
|
10
|
-
vitest_1.vi.mock("@trigger.dev/core/v3", async (importActual) => {
|
|
11
|
-
const actual = (await importActual());
|
|
12
|
-
class StubSessionStreamInstance {
|
|
13
|
-
constructor(opts) {
|
|
14
|
-
// Drain the source so the upstream tee doesn't backpressure-stall
|
|
15
|
-
// the SSE half. We don't keep the chunks — durability/resume is
|
|
16
|
-
// out of scope here.
|
|
17
|
-
void (async () => {
|
|
18
|
-
const reader = opts.source.getReader();
|
|
19
|
-
try {
|
|
20
|
-
while (true) {
|
|
21
|
-
const { done } = await reader.read();
|
|
22
|
-
if (done)
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
finally {
|
|
27
|
-
reader.releaseLock();
|
|
28
|
-
}
|
|
29
|
-
})();
|
|
30
|
-
}
|
|
31
|
-
async wait() {
|
|
32
|
-
return { written: 0 };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return { ...actual, SessionStreamInstance: StubSessionStreamInstance };
|
|
36
|
-
});
|
|
37
|
-
// Import AFTER the mock so chat-server picks up the stubbed class.
|
|
38
|
-
const chat_server_js_1 = require("./chat-server.js");
|
|
39
|
-
const v3_1 = require("@trigger.dev/core/v3");
|
|
40
|
-
// ── Helpers ────────────────────────────────────────────────────────────
|
|
41
|
-
function textStream(text) {
|
|
42
|
-
return (0, ai_1.simulateReadableStream)({
|
|
43
|
-
chunks: [
|
|
44
|
-
{ type: "text-start", id: "t1" },
|
|
45
|
-
{ type: "text-delta", id: "t1", delta: text },
|
|
46
|
-
{ type: "text-end", id: "t1" },
|
|
47
|
-
{
|
|
48
|
-
type: "finish",
|
|
49
|
-
finishReason: { unified: "stop", raw: "stop" },
|
|
50
|
-
usage: {
|
|
51
|
-
inputTokens: { total: 5, noCache: 5, cacheRead: undefined, cacheWrite: undefined },
|
|
52
|
-
outputTokens: { total: 5, text: 5, reasoning: undefined },
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
function toolCallStream() {
|
|
59
|
-
return (0, ai_1.simulateReadableStream)({
|
|
60
|
-
chunks: [
|
|
61
|
-
{
|
|
62
|
-
type: "tool-call",
|
|
63
|
-
toolCallId: "tc-1",
|
|
64
|
-
toolName: "weather",
|
|
65
|
-
input: JSON.stringify({ city: "tokyo" }),
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
type: "finish",
|
|
69
|
-
finishReason: { unified: "tool-calls", raw: "tool-calls" },
|
|
70
|
-
usage: {
|
|
71
|
-
inputTokens: { total: 5, noCache: 5, cacheRead: undefined, cacheWrite: undefined },
|
|
72
|
-
outputTokens: { total: 5, text: 0, reasoning: undefined },
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
function makeRequest(body) {
|
|
79
|
-
return new Request("https://my-app.example/api/chat", {
|
|
80
|
-
method: "POST",
|
|
81
|
-
headers: { "content-type": "application/json" },
|
|
82
|
-
body: JSON.stringify(body),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
const SESSION_PAT = "tr_session_pat_for_handover";
|
|
86
|
-
function createSessionResponse(externalId) {
|
|
87
|
-
return new Response(JSON.stringify({
|
|
88
|
-
id: "session_test",
|
|
89
|
-
externalId,
|
|
90
|
-
type: "chat.agent",
|
|
91
|
-
taskIdentifier: "test-agent",
|
|
92
|
-
triggerConfig: {
|
|
93
|
-
basePayload: { chatId: externalId, trigger: "handover-prepare" },
|
|
94
|
-
idleTimeoutInSeconds: 60,
|
|
95
|
-
},
|
|
96
|
-
currentRunId: "run_test",
|
|
97
|
-
runId: "run_test",
|
|
98
|
-
publicAccessToken: SESSION_PAT,
|
|
99
|
-
tags: [],
|
|
100
|
-
metadata: null,
|
|
101
|
-
closedAt: null,
|
|
102
|
-
closedReason: null,
|
|
103
|
-
expiresAt: null,
|
|
104
|
-
createdAt: new Date(0).toISOString(),
|
|
105
|
-
updatedAt: new Date(0).toISOString(),
|
|
106
|
-
isCached: false,
|
|
107
|
-
}), {
|
|
108
|
-
status: 200,
|
|
109
|
-
headers: { "content-type": "application/json" },
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
function appendOkResponse() {
|
|
113
|
-
return new Response(JSON.stringify({ ok: true }), {
|
|
114
|
-
status: 200,
|
|
115
|
-
headers: { "content-type": "application/json" },
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
async function readSSEBodyToChunks(res) {
|
|
119
|
-
const text = await res.text();
|
|
120
|
-
return text
|
|
121
|
-
.split("\n\n")
|
|
122
|
-
.filter((b) => b.startsWith("data: "))
|
|
123
|
-
.map((b) => JSON.parse(b.slice(6)));
|
|
124
|
-
}
|
|
125
|
-
async function withApiContext(fn) {
|
|
126
|
-
return v3_1.apiClientManager.runWithConfig({
|
|
127
|
-
baseURL: "https://api.test.trigger.dev",
|
|
128
|
-
secretKey: "tr_test_secret",
|
|
129
|
-
}, fn);
|
|
130
|
-
}
|
|
131
|
-
// ── Tests ──────────────────────────────────────────────────────────────
|
|
132
|
-
(0, vitest_1.describe)("chat.headStart (route handler)", () => {
|
|
133
|
-
let originalFetch;
|
|
134
|
-
(0, vitest_1.beforeEach)(() => {
|
|
135
|
-
originalFetch = global.fetch;
|
|
136
|
-
});
|
|
137
|
-
(0, vitest_1.afterEach)(() => {
|
|
138
|
-
global.fetch = originalFetch;
|
|
139
|
-
vitest_1.vi.restoreAllMocks();
|
|
140
|
-
});
|
|
141
|
-
(0, vitest_1.it)("creates the session with handover-prepare in basePayload and returns the session PAT in headers", async () => {
|
|
142
|
-
const requests = [];
|
|
143
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
144
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
145
|
-
requests.push({ url: urlStr, init });
|
|
146
|
-
if (urlStr.endsWith("/api/v1/sessions") || urlStr.endsWith("/api/v1/sessions/")) {
|
|
147
|
-
return createSessionResponse("chat-1");
|
|
148
|
-
}
|
|
149
|
-
if (urlStr.includes("/realtime/v1/sessions/") && urlStr.endsWith("/in/append")) {
|
|
150
|
-
return appendOkResponse();
|
|
151
|
-
}
|
|
152
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
153
|
-
});
|
|
154
|
-
const handler = chat_server_js_1.chat.headStart({
|
|
155
|
-
agentId: "test-agent",
|
|
156
|
-
run: async ({ chat: chatHelper }) => {
|
|
157
|
-
return (0, ai_1.streamText)({
|
|
158
|
-
...chatHelper.toStreamTextOptions(),
|
|
159
|
-
model: new test_1.MockLanguageModelV3({
|
|
160
|
-
doStream: async () => ({ stream: textStream("hi back") }),
|
|
161
|
-
}),
|
|
162
|
-
});
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
const res = await withApiContext(() => handler(makeRequest({
|
|
166
|
-
chatId: "chat-1",
|
|
167
|
-
trigger: "submit-message",
|
|
168
|
-
headStartMessages: [{ id: "m1", role: "user", parts: [{ type: "text", text: "hi" }] }],
|
|
169
|
-
})));
|
|
170
|
-
(0, vitest_1.expect)(res.status).toBe(200);
|
|
171
|
-
(0, vitest_1.expect)(res.headers.get("X-Trigger-Chat-Id")).toBe("chat-1");
|
|
172
|
-
(0, vitest_1.expect)(res.headers.get("X-Trigger-Chat-Access-Token")).toBe(SESSION_PAT);
|
|
173
|
-
(0, vitest_1.expect)(res.headers.get("Content-Type")).toMatch(/text\/event-stream/);
|
|
174
|
-
const sessionCreate = requests.find((r) => r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/"));
|
|
175
|
-
(0, vitest_1.expect)(sessionCreate).toBeDefined();
|
|
176
|
-
const body = JSON.parse(sessionCreate.init.body);
|
|
177
|
-
(0, vitest_1.expect)(body.type).toBe("chat.agent");
|
|
178
|
-
(0, vitest_1.expect)(body.externalId).toBe("chat-1");
|
|
179
|
-
(0, vitest_1.expect)(body.taskIdentifier).toBe("test-agent");
|
|
180
|
-
// The trigger payload is rewritten to handover-prepare even though the
|
|
181
|
-
// browser sent submit-message — the agent boots into the handover wait branch.
|
|
182
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.trigger).toBe("handover-prepare");
|
|
183
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.chatId).toBe("chat-1");
|
|
184
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.idleTimeoutInSeconds).toBe(60);
|
|
185
|
-
});
|
|
186
|
-
(0, vitest_1.it)("merges triggerConfig tags and queue into createSession", async () => {
|
|
187
|
-
const requests = [];
|
|
188
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
189
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
190
|
-
requests.push({ url: urlStr, init });
|
|
191
|
-
if (urlStr.endsWith("/api/v1/sessions") || urlStr.endsWith("/api/v1/sessions/")) {
|
|
192
|
-
return createSessionResponse("chat-1");
|
|
193
|
-
}
|
|
194
|
-
if (urlStr.includes("/realtime/v1/sessions/") && urlStr.endsWith("/in/append")) {
|
|
195
|
-
return appendOkResponse();
|
|
196
|
-
}
|
|
197
|
-
if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) {
|
|
198
|
-
return new Response(new ReadableStream({
|
|
199
|
-
start(c) {
|
|
200
|
-
c.close();
|
|
201
|
-
},
|
|
202
|
-
}), {
|
|
203
|
-
status: 200,
|
|
204
|
-
headers: { "content-type": "text/event-stream" },
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
208
|
-
});
|
|
209
|
-
const handler = chat_server_js_1.chat.headStart({
|
|
210
|
-
agentId: "test-agent",
|
|
211
|
-
triggerConfig: {
|
|
212
|
-
tags: ["org:acme", "agentic-run:xyz"],
|
|
213
|
-
queue: "my-queue",
|
|
214
|
-
},
|
|
215
|
-
run: async ({ chat: chatHelper }) => {
|
|
216
|
-
return (0, ai_1.streamText)({
|
|
217
|
-
...chatHelper.toStreamTextOptions(),
|
|
218
|
-
model: new test_1.MockLanguageModelV3({
|
|
219
|
-
doStream: async () => ({ stream: textStream("hi back") }),
|
|
220
|
-
}),
|
|
221
|
-
});
|
|
222
|
-
},
|
|
223
|
-
});
|
|
224
|
-
await withApiContext(() => handler(makeRequest({
|
|
225
|
-
chatId: "chat-1",
|
|
226
|
-
trigger: "submit-message",
|
|
227
|
-
headStartMessages: [{ id: "m1", role: "user", parts: [{ type: "text", text: "hi" }] }],
|
|
228
|
-
})));
|
|
229
|
-
const sessionCreate = requests.find((r) => r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/"));
|
|
230
|
-
(0, vitest_1.expect)(sessionCreate).toBeDefined();
|
|
231
|
-
const body = JSON.parse(sessionCreate.init.body);
|
|
232
|
-
(0, vitest_1.expect)(body.triggerConfig.tags).toEqual(["chat:chat-1", "org:acme", "agentic-run:xyz"]);
|
|
233
|
-
(0, vitest_1.expect)(body.triggerConfig.queue).toBe("my-queue");
|
|
234
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.trigger).toBe("handover-prepare");
|
|
235
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.chatId).toBe("chat-1");
|
|
236
|
-
});
|
|
237
|
-
(0, vitest_1.it)("dispatches handover with isFinal=true on pure-text finishReason", async () => {
|
|
238
|
-
const requests = [];
|
|
239
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
240
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
241
|
-
requests.push({ url: urlStr, init });
|
|
242
|
-
if (urlStr.endsWith("/api/v1/sessions") || urlStr.endsWith("/api/v1/sessions/")) {
|
|
243
|
-
return createSessionResponse("chat-final");
|
|
244
|
-
}
|
|
245
|
-
if (urlStr.includes("/realtime/v1/sessions/") && urlStr.endsWith("/in/append")) {
|
|
246
|
-
return appendOkResponse();
|
|
247
|
-
}
|
|
248
|
-
// Stitched response subscribes to `.out` after handover.
|
|
249
|
-
if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) {
|
|
250
|
-
return new Response(new ReadableStream({
|
|
251
|
-
start(c) {
|
|
252
|
-
c.close();
|
|
253
|
-
},
|
|
254
|
-
}), {
|
|
255
|
-
status: 200,
|
|
256
|
-
headers: { "content-type": "text/event-stream" },
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
260
|
-
});
|
|
261
|
-
const handler = chat_server_js_1.chat.headStart({
|
|
262
|
-
agentId: "test-agent",
|
|
263
|
-
run: async ({ chat: chatHelper }) => {
|
|
264
|
-
return (0, ai_1.streamText)({
|
|
265
|
-
...chatHelper.toStreamTextOptions(),
|
|
266
|
-
model: new test_1.MockLanguageModelV3({
|
|
267
|
-
doStream: async () => ({ stream: textStream("just a text reply") }),
|
|
268
|
-
}),
|
|
269
|
-
});
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
const res = await withApiContext(() => handler(makeRequest({
|
|
273
|
-
chatId: "chat-final",
|
|
274
|
-
trigger: "submit-message",
|
|
275
|
-
// Slim wire: head-start ships full history via `headStartMessages`
|
|
276
|
-
// (not `messages` / `message`). The route handler reads that field
|
|
277
|
-
// off the request body before invoking the customer's run().
|
|
278
|
-
headStartMessages: [{ id: "m1", role: "user", parts: [{ type: "text", text: "hi" }] }],
|
|
279
|
-
})));
|
|
280
|
-
// Drain the SSE body so handoverWhenDone observes finishReason.
|
|
281
|
-
const chunks = await readSSEBodyToChunks(res);
|
|
282
|
-
(0, vitest_1.expect)(chunks.some((c) => c.type === "text-delta")).toBe(true);
|
|
283
|
-
// Give the deferred handoverWhenDone a tick to dispatch.
|
|
284
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
285
|
-
const handoverPost = requests.find((r) => r.url.includes("/realtime/v1/sessions/chat-final/in/append") && r.init?.body !== undefined);
|
|
286
|
-
(0, vitest_1.expect)(handoverPost).toBeDefined();
|
|
287
|
-
const body = JSON.parse(handoverPost.init.body);
|
|
288
|
-
// Pure-text finishes go through `kind: "handover"` with `isFinal: true`
|
|
289
|
-
// so the agent runs hooks (persistence, etc.) without making an LLM call.
|
|
290
|
-
(0, vitest_1.expect)(body.kind).toBe("handover");
|
|
291
|
-
(0, vitest_1.expect)(body.isFinal).toBe(true);
|
|
292
|
-
// The partial carries the customer's response messages — a single
|
|
293
|
-
// assistant message with the streamed text.
|
|
294
|
-
(0, vitest_1.expect)(Array.isArray(body.partialAssistantMessage)).toBe(true);
|
|
295
|
-
const assistant = body.partialAssistantMessage.find((m) => m.role === "assistant");
|
|
296
|
-
(0, vitest_1.expect)(assistant).toBeDefined();
|
|
297
|
-
});
|
|
298
|
-
(0, vitest_1.it)("dispatches handover with response.messages on tool-call finishReason", async () => {
|
|
299
|
-
const requests = [];
|
|
300
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
301
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
302
|
-
requests.push({ url: urlStr, init });
|
|
303
|
-
if (urlStr.endsWith("/api/v1/sessions") || urlStr.endsWith("/api/v1/sessions/")) {
|
|
304
|
-
return createSessionResponse("chat-tool");
|
|
305
|
-
}
|
|
306
|
-
if (urlStr.includes("/realtime/v1/sessions/") && urlStr.endsWith("/in/append")) {
|
|
307
|
-
return appendOkResponse();
|
|
308
|
-
}
|
|
309
|
-
// Stitched response now subscribes to `.out` after handover to
|
|
310
|
-
// pick up agent-side chunks. Return an empty SSE body that
|
|
311
|
-
// closes immediately — this test validates dispatch only, not
|
|
312
|
-
// the agent-side resume.
|
|
313
|
-
if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) {
|
|
314
|
-
return new Response(new ReadableStream({
|
|
315
|
-
start(c) {
|
|
316
|
-
c.close();
|
|
317
|
-
},
|
|
318
|
-
}), {
|
|
319
|
-
status: 200,
|
|
320
|
-
headers: { "content-type": "text/event-stream" },
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
324
|
-
});
|
|
325
|
-
// Schema-only tool — no execute. The mock model emits a tool-call;
|
|
326
|
-
// AI SDK doesn't run it (no execute) and finishes with "tool-calls".
|
|
327
|
-
const { tool } = await import("ai");
|
|
328
|
-
const { z } = await import("zod");
|
|
329
|
-
const weatherTool = tool({
|
|
330
|
-
description: "weather",
|
|
331
|
-
inputSchema: z.object({ city: z.string() }),
|
|
332
|
-
});
|
|
333
|
-
const handler = chat_server_js_1.chat.headStart({
|
|
334
|
-
agentId: "test-agent",
|
|
335
|
-
run: async ({ chat: chatHelper }) => {
|
|
336
|
-
return (0, ai_1.streamText)({
|
|
337
|
-
...chatHelper.toStreamTextOptions({ tools: { weather: weatherTool } }),
|
|
338
|
-
model: new test_1.MockLanguageModelV3({
|
|
339
|
-
doStream: async () => ({ stream: toolCallStream() }),
|
|
340
|
-
}),
|
|
341
|
-
});
|
|
342
|
-
},
|
|
343
|
-
});
|
|
344
|
-
const res = await withApiContext(() => handler(makeRequest({
|
|
345
|
-
chatId: "chat-tool",
|
|
346
|
-
trigger: "submit-message",
|
|
347
|
-
headStartMessages: [
|
|
348
|
-
{ id: "m1", role: "user", parts: [{ type: "text", text: "weather in tokyo?" }] },
|
|
349
|
-
],
|
|
350
|
-
})));
|
|
351
|
-
await readSSEBodyToChunks(res);
|
|
352
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
353
|
-
const handoverPost = requests.find((r) => r.url.includes("/realtime/v1/sessions/chat-tool/in/append") && r.init?.body !== undefined);
|
|
354
|
-
(0, vitest_1.expect)(handoverPost).toBeDefined();
|
|
355
|
-
const body = JSON.parse(handoverPost.init.body);
|
|
356
|
-
(0, vitest_1.expect)(body.kind).toBe("handover");
|
|
357
|
-
(0, vitest_1.expect)(body.isFinal).toBe(false); // pending tool-calls — agent runs streamText
|
|
358
|
-
(0, vitest_1.expect)(Array.isArray(body.partialAssistantMessage)).toBe(true);
|
|
359
|
-
// The partial is reshaped into AI SDK's tool-approval round so the
|
|
360
|
-
// agent's `streamText` can resume by executing the pending tool-call
|
|
361
|
-
// before step 2. Assistant gets a `tool-approval-request` part
|
|
362
|
-
// alongside the original `tool-call`; a trailing `tool` message
|
|
363
|
-
// carries the `tool-approval-response { approved: true }`.
|
|
364
|
-
const assistant = body.partialAssistantMessage.find((m) => m.role === "assistant");
|
|
365
|
-
(0, vitest_1.expect)(assistant).toBeDefined();
|
|
366
|
-
const toolCallPart = assistant.content.find((p) => p.type === "tool-call");
|
|
367
|
-
(0, vitest_1.expect)(toolCallPart).toBeDefined();
|
|
368
|
-
const approvalRequestPart = assistant.content.find((p) => p.type === "tool-approval-request");
|
|
369
|
-
(0, vitest_1.expect)(approvalRequestPart).toBeDefined();
|
|
370
|
-
(0, vitest_1.expect)(approvalRequestPart.toolCallId).toBe(toolCallPart.toolCallId);
|
|
371
|
-
const trailingTool = body.partialAssistantMessage[body.partialAssistantMessage.length - 1];
|
|
372
|
-
(0, vitest_1.expect)(trailingTool.role).toBe("tool");
|
|
373
|
-
const approvalResponsePart = trailingTool.content.find((p) => p.type === "tool-approval-response");
|
|
374
|
-
(0, vitest_1.expect)(approvalResponsePart).toBeDefined();
|
|
375
|
-
(0, vitest_1.expect)(approvalResponsePart.approvalId).toBe(approvalRequestPart.approvalId);
|
|
376
|
-
(0, vitest_1.expect)(approvalResponsePart.approved).toBe(true);
|
|
377
|
-
});
|
|
378
|
-
(0, vitest_1.it)("rejects requests missing chatId", async () => {
|
|
379
|
-
global.fetch = vitest_1.vi.fn().mockResolvedValue(new Response("nope", { status: 500 }));
|
|
380
|
-
const handler = chat_server_js_1.chat.headStart({
|
|
381
|
-
agentId: "test-agent",
|
|
382
|
-
run: async ({ chat: chatHelper }) => {
|
|
383
|
-
return (0, ai_1.streamText)({
|
|
384
|
-
...chatHelper.toStreamTextOptions(),
|
|
385
|
-
model: new test_1.MockLanguageModelV3({
|
|
386
|
-
doStream: async () => ({ stream: textStream("x") }),
|
|
387
|
-
}),
|
|
388
|
-
});
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
await (0, vitest_1.expect)(withApiContext(() => handler(makeRequest({
|
|
392
|
-
// no chatId
|
|
393
|
-
trigger: "submit-message",
|
|
394
|
-
messages: [],
|
|
395
|
-
})))).rejects.toThrow(/chatId/);
|
|
396
|
-
});
|
|
397
|
-
});
|
|
398
|
-
(0, vitest_1.describe)("chat.startHeadStart (detached)", () => {
|
|
399
|
-
let originalFetch;
|
|
400
|
-
(0, vitest_1.beforeEach)(() => {
|
|
401
|
-
originalFetch = global.fetch;
|
|
402
|
-
});
|
|
403
|
-
(0, vitest_1.afterEach)(() => {
|
|
404
|
-
global.fetch = originalFetch;
|
|
405
|
-
vitest_1.vi.restoreAllMocks();
|
|
406
|
-
});
|
|
407
|
-
function wireFetch(requests) {
|
|
408
|
-
global.fetch = vitest_1.vi.fn().mockImplementation(async (url, init) => {
|
|
409
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
410
|
-
requests.push({ url: urlStr, init });
|
|
411
|
-
if (urlStr.endsWith("/api/v1/sessions") || urlStr.endsWith("/api/v1/sessions/")) {
|
|
412
|
-
return createSessionResponse("chat-1");
|
|
413
|
-
}
|
|
414
|
-
if (urlStr.includes("/realtime/v1/sessions/") && urlStr.endsWith("/in/append")) {
|
|
415
|
-
return appendOkResponse();
|
|
416
|
-
}
|
|
417
|
-
throw new Error(`Unexpected URL: ${urlStr}`);
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
const userMessages = [
|
|
421
|
-
{ id: "m1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
422
|
-
];
|
|
423
|
-
(0, vitest_1.it)("returns { chatId, completion } (no Response) and creates the session with handover-prepare + headStartMessages", async () => {
|
|
424
|
-
const requests = [];
|
|
425
|
-
wireFetch(requests);
|
|
426
|
-
const result = await withApiContext(() => chat_server_js_1.chat.startHeadStart({
|
|
427
|
-
agentId: "test-agent",
|
|
428
|
-
chatId: "chat-1",
|
|
429
|
-
messages: userMessages,
|
|
430
|
-
run: async ({ chat: chatHelper }) => (0, ai_1.streamText)({
|
|
431
|
-
...chatHelper.toStreamTextOptions(),
|
|
432
|
-
model: new test_1.MockLanguageModelV3({
|
|
433
|
-
doStream: async () => ({ stream: textStream("hi back") }),
|
|
434
|
-
}),
|
|
435
|
-
}),
|
|
436
|
-
}));
|
|
437
|
-
// Shape: a plain object, not a Response.
|
|
438
|
-
(0, vitest_1.expect)(result.chatId).toBe("chat-1");
|
|
439
|
-
(0, vitest_1.expect)(typeof result.completion.then).toBe("function");
|
|
440
|
-
(0, vitest_1.expect)(result).not.toBeInstanceOf(Response);
|
|
441
|
-
await result.completion;
|
|
442
|
-
const sessionCreate = requests.find((r) => r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/"));
|
|
443
|
-
(0, vitest_1.expect)(sessionCreate).toBeDefined();
|
|
444
|
-
const body = JSON.parse(sessionCreate.init.body);
|
|
445
|
-
(0, vitest_1.expect)(body.type).toBe("chat.agent");
|
|
446
|
-
(0, vitest_1.expect)(body.externalId).toBe("chat-1");
|
|
447
|
-
(0, vitest_1.expect)(body.taskIdentifier).toBe("test-agent");
|
|
448
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.trigger).toBe("handover-prepare");
|
|
449
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.chatId).toBe("chat-1");
|
|
450
|
-
// Full first-turn history rides on headStartMessages (not /in/append).
|
|
451
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.headStartMessages).toHaveLength(1);
|
|
452
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.headStartMessages[0].id).toBe("m1");
|
|
453
|
-
});
|
|
454
|
-
(0, vitest_1.it)("dispatches a final handover (isFinal: true) on a pure-text step 1", async () => {
|
|
455
|
-
const requests = [];
|
|
456
|
-
wireFetch(requests);
|
|
457
|
-
const { completion } = await withApiContext(() => chat_server_js_1.chat.startHeadStart({
|
|
458
|
-
agentId: "test-agent",
|
|
459
|
-
chatId: "chat-1",
|
|
460
|
-
messages: userMessages,
|
|
461
|
-
run: async ({ chat: chatHelper }) => (0, ai_1.streamText)({
|
|
462
|
-
...chatHelper.toStreamTextOptions(),
|
|
463
|
-
model: new test_1.MockLanguageModelV3({
|
|
464
|
-
doStream: async () => ({ stream: textStream("the answer") }),
|
|
465
|
-
}),
|
|
466
|
-
}),
|
|
467
|
-
}));
|
|
468
|
-
await completion;
|
|
469
|
-
const append = requests.find((r) => r.url.endsWith("/in/append"));
|
|
470
|
-
(0, vitest_1.expect)(append).toBeDefined();
|
|
471
|
-
const appendBody = append.init.body;
|
|
472
|
-
(0, vitest_1.expect)(appendBody).toContain('"kind":"handover"');
|
|
473
|
-
(0, vitest_1.expect)(appendBody).toContain('"isFinal":true');
|
|
474
|
-
// A stable assistant messageId is carried across the handover boundary.
|
|
475
|
-
(0, vitest_1.expect)(appendBody).toContain('"messageId":');
|
|
476
|
-
});
|
|
477
|
-
(0, vitest_1.it)("dispatches a non-final handover (isFinal: false) on a tool-call step 1", async () => {
|
|
478
|
-
const requests = [];
|
|
479
|
-
wireFetch(requests);
|
|
480
|
-
const { completion } = await withApiContext(() => chat_server_js_1.chat.startHeadStart({
|
|
481
|
-
agentId: "test-agent",
|
|
482
|
-
chatId: "chat-1",
|
|
483
|
-
messages: userMessages,
|
|
484
|
-
run: async ({ chat: chatHelper }) => (0, ai_1.streamText)({
|
|
485
|
-
...chatHelper.toStreamTextOptions(),
|
|
486
|
-
model: new test_1.MockLanguageModelV3({
|
|
487
|
-
doStream: async () => ({ stream: toolCallStream() }),
|
|
488
|
-
}),
|
|
489
|
-
}),
|
|
490
|
-
}));
|
|
491
|
-
await completion;
|
|
492
|
-
const append = requests.find((r) => r.url.endsWith("/in/append"));
|
|
493
|
-
(0, vitest_1.expect)(append).toBeDefined();
|
|
494
|
-
const appendBody = append.init.body;
|
|
495
|
-
(0, vitest_1.expect)(appendBody).toContain('"kind":"handover"');
|
|
496
|
-
(0, vitest_1.expect)(appendBody).toContain('"isFinal":false');
|
|
497
|
-
});
|
|
498
|
-
(0, vitest_1.it)("merges metadata into the handover-prepare run payload (never to the browser)", async () => {
|
|
499
|
-
const requests = [];
|
|
500
|
-
wireFetch(requests);
|
|
501
|
-
const { completion } = await withApiContext(() => chat_server_js_1.chat.startHeadStart({
|
|
502
|
-
agentId: "test-agent",
|
|
503
|
-
chatId: "chat-1",
|
|
504
|
-
messages: userMessages,
|
|
505
|
-
metadata: { userActorToken: "tr_uat_secret", projectRef: "proj_x" },
|
|
506
|
-
run: async ({ chat: chatHelper }) => (0, ai_1.streamText)({
|
|
507
|
-
...chatHelper.toStreamTextOptions(),
|
|
508
|
-
model: new test_1.MockLanguageModelV3({
|
|
509
|
-
doStream: async () => ({ stream: textStream("ok") }),
|
|
510
|
-
}),
|
|
511
|
-
}),
|
|
512
|
-
}));
|
|
513
|
-
await completion;
|
|
514
|
-
const sessionCreate = requests.find((r) => r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/"));
|
|
515
|
-
const body = JSON.parse(sessionCreate.init.body);
|
|
516
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.metadata.userActorToken).toBe("tr_uat_secret");
|
|
517
|
-
(0, vitest_1.expect)(body.triggerConfig.basePayload.metadata.projectRef).toBe("proj_x");
|
|
518
|
-
});
|
|
519
|
-
(0, vitest_1.it)("signals handover-skip and rejects completion when the warm step throws", async () => {
|
|
520
|
-
const requests = [];
|
|
521
|
-
wireFetch(requests);
|
|
522
|
-
const { completion } = await withApiContext(() => chat_server_js_1.chat.startHeadStart({
|
|
523
|
-
agentId: "test-agent",
|
|
524
|
-
chatId: "chat-1",
|
|
525
|
-
messages: userMessages,
|
|
526
|
-
run: async () => {
|
|
527
|
-
throw new Error("warm step boom");
|
|
528
|
-
},
|
|
529
|
-
}));
|
|
530
|
-
await (0, vitest_1.expect)(completion).rejects.toThrow("warm step boom");
|
|
531
|
-
const append = requests.find((r) => r.url.endsWith("/in/append"));
|
|
532
|
-
(0, vitest_1.expect)(append).toBeDefined();
|
|
533
|
-
(0, vitest_1.expect)(append.init.body).toContain('"kind":"handover-skip"');
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
(0, vitest_1.describe)("chat.toNodeListener", () => {
|
|
537
|
-
/**
|
|
538
|
-
* Build a fake Node IncomingMessage that yields a JSON body.
|
|
539
|
-
* AsyncIterable so the listener can `for await` over it.
|
|
540
|
-
*/
|
|
541
|
-
function fakeNodeRequest(opts) {
|
|
542
|
-
const bodyBytes = opts.body ? new TextEncoder().encode(opts.body) : undefined;
|
|
543
|
-
const headers = {
|
|
544
|
-
host: opts.host ?? "example.com",
|
|
545
|
-
...(opts.body ? { "content-type": "application/json" } : {}),
|
|
546
|
-
...(opts.headers ?? {}),
|
|
547
|
-
};
|
|
548
|
-
const errorListeners = [];
|
|
549
|
-
return {
|
|
550
|
-
method: opts.method ?? "POST",
|
|
551
|
-
url: opts.url ?? "/api/chat",
|
|
552
|
-
headers,
|
|
553
|
-
on(event, listener) {
|
|
554
|
-
if (event === "error")
|
|
555
|
-
errorListeners.push(listener);
|
|
556
|
-
return this;
|
|
557
|
-
},
|
|
558
|
-
async *[Symbol.asyncIterator]() {
|
|
559
|
-
if (bodyBytes)
|
|
560
|
-
yield bodyBytes;
|
|
561
|
-
},
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
function fakeNodeResponse() {
|
|
565
|
-
const writes = [];
|
|
566
|
-
let ended = false;
|
|
567
|
-
let endChunk;
|
|
568
|
-
const closeListeners = [];
|
|
569
|
-
const headers = {};
|
|
570
|
-
const obj = {
|
|
571
|
-
statusCode: 200,
|
|
572
|
-
headersSent: false,
|
|
573
|
-
setHeader(name, value) {
|
|
574
|
-
headers[name.toLowerCase()] = value;
|
|
575
|
-
},
|
|
576
|
-
write(chunk) {
|
|
577
|
-
if (typeof chunk === "string") {
|
|
578
|
-
writes.push(new TextEncoder().encode(chunk));
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
writes.push(chunk);
|
|
582
|
-
}
|
|
583
|
-
obj.headersSent = true;
|
|
584
|
-
return true;
|
|
585
|
-
},
|
|
586
|
-
end(chunk) {
|
|
587
|
-
ended = true;
|
|
588
|
-
endChunk = chunk;
|
|
589
|
-
},
|
|
590
|
-
on(event, listener) {
|
|
591
|
-
if (event === "close")
|
|
592
|
-
closeListeners.push(listener);
|
|
593
|
-
return obj;
|
|
594
|
-
},
|
|
595
|
-
// test helpers
|
|
596
|
-
_written() {
|
|
597
|
-
const all = [...writes];
|
|
598
|
-
if (typeof endChunk === "string")
|
|
599
|
-
all.push(new TextEncoder().encode(endChunk));
|
|
600
|
-
else if (endChunk)
|
|
601
|
-
all.push(endChunk);
|
|
602
|
-
let total = 0;
|
|
603
|
-
for (const c of all)
|
|
604
|
-
total += c.length;
|
|
605
|
-
const merged = new Uint8Array(total);
|
|
606
|
-
let offset = 0;
|
|
607
|
-
for (const c of all) {
|
|
608
|
-
merged.set(c, offset);
|
|
609
|
-
offset += c.length;
|
|
610
|
-
}
|
|
611
|
-
return new TextDecoder().decode(merged);
|
|
612
|
-
},
|
|
613
|
-
_ended: () => ended,
|
|
614
|
-
_headers: () => headers,
|
|
615
|
-
_close: () => {
|
|
616
|
-
for (const l of closeListeners)
|
|
617
|
-
l();
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
|
-
return obj;
|
|
621
|
-
}
|
|
622
|
-
(0, vitest_1.it)("converts the Node request into a Web Request, calls the handler, and forwards the response", async () => {
|
|
623
|
-
const seen = {};
|
|
624
|
-
const webHandler = async (req) => {
|
|
625
|
-
seen.method = req.method;
|
|
626
|
-
seen.url = req.url;
|
|
627
|
-
seen.ct = req.headers.get("content-type");
|
|
628
|
-
seen.body = await req.text();
|
|
629
|
-
return new Response("ok", {
|
|
630
|
-
status: 201,
|
|
631
|
-
headers: { "x-test": "1", "content-type": "text/plain" },
|
|
632
|
-
});
|
|
633
|
-
};
|
|
634
|
-
const listener = chat_server_js_1.chat.toNodeListener(webHandler);
|
|
635
|
-
const req = fakeNodeRequest({ body: '{"hello":"world"}' });
|
|
636
|
-
const res = fakeNodeResponse();
|
|
637
|
-
await listener(req, res);
|
|
638
|
-
(0, vitest_1.expect)(seen.method).toBe("POST");
|
|
639
|
-
(0, vitest_1.expect)(seen.url).toBe("http://example.com/api/chat");
|
|
640
|
-
(0, vitest_1.expect)(seen.ct).toBe("application/json");
|
|
641
|
-
(0, vitest_1.expect)(seen.body).toBe('{"hello":"world"}');
|
|
642
|
-
(0, vitest_1.expect)(res.statusCode).toBe(201);
|
|
643
|
-
(0, vitest_1.expect)(res._headers()["x-test"]).toBe("1");
|
|
644
|
-
(0, vitest_1.expect)(res._written()).toBe("ok");
|
|
645
|
-
(0, vitest_1.expect)(res._ended()).toBe(true);
|
|
646
|
-
});
|
|
647
|
-
(0, vitest_1.it)("streams the Web Response body to the Node response chunk by chunk (no buffering)", async () => {
|
|
648
|
-
const chunkOrder = [];
|
|
649
|
-
const webHandler = async () => {
|
|
650
|
-
const encoder = new TextEncoder();
|
|
651
|
-
const stream = new ReadableStream({
|
|
652
|
-
async start(controller) {
|
|
653
|
-
for (const piece of ["one\n", "two\n", "three\n"]) {
|
|
654
|
-
chunkOrder.push("emit-" + piece.trim());
|
|
655
|
-
controller.enqueue(encoder.encode(piece));
|
|
656
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
657
|
-
}
|
|
658
|
-
controller.close();
|
|
659
|
-
},
|
|
660
|
-
});
|
|
661
|
-
return new Response(stream, {
|
|
662
|
-
status: 200,
|
|
663
|
-
headers: { "content-type": "text/event-stream" },
|
|
664
|
-
});
|
|
665
|
-
};
|
|
666
|
-
const listener = chat_server_js_1.chat.toNodeListener(webHandler);
|
|
667
|
-
const req = fakeNodeRequest({});
|
|
668
|
-
const res = fakeNodeResponse();
|
|
669
|
-
await listener(req, res);
|
|
670
|
-
(0, vitest_1.expect)(res._written()).toBe("one\ntwo\nthree\n");
|
|
671
|
-
(0, vitest_1.expect)(chunkOrder).toEqual(["emit-one", "emit-two", "emit-three"]);
|
|
672
|
-
(0, vitest_1.expect)(res._headers()["content-type"]).toBe("text/event-stream");
|
|
673
|
-
});
|
|
674
|
-
(0, vitest_1.it)("propagates client disconnect to the Web handler via AbortSignal", async () => {
|
|
675
|
-
let signal;
|
|
676
|
-
let aborted = false;
|
|
677
|
-
const webHandler = async (req) => {
|
|
678
|
-
signal = req.signal;
|
|
679
|
-
signal.addEventListener("abort", () => {
|
|
680
|
-
aborted = true;
|
|
681
|
-
});
|
|
682
|
-
// Return a never-ending stream so the listener stays open until close.
|
|
683
|
-
return new Response(new ReadableStream({
|
|
684
|
-
start() {
|
|
685
|
-
// never enqueues
|
|
686
|
-
},
|
|
687
|
-
}));
|
|
688
|
-
};
|
|
689
|
-
const listener = chat_server_js_1.chat.toNodeListener(webHandler);
|
|
690
|
-
const req = fakeNodeRequest({});
|
|
691
|
-
const res = fakeNodeResponse();
|
|
692
|
-
// Run listener in background (it'll hang on the never-ending stream).
|
|
693
|
-
const pending = listener(req, res);
|
|
694
|
-
// Wait a tick for the handler to attach the abort listener.
|
|
695
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
696
|
-
res._close();
|
|
697
|
-
(0, vitest_1.expect)(aborted).toBe(true);
|
|
698
|
-
// Cleanup: the listener will throw (abort) and we don't care about the result.
|
|
699
|
-
await pending.catch(() => { });
|
|
700
|
-
});
|
|
701
|
-
(0, vitest_1.it)("returns 500 with error text if the handler throws before headers are sent", async () => {
|
|
702
|
-
const webHandler = async () => {
|
|
703
|
-
throw new Error("boom");
|
|
704
|
-
};
|
|
705
|
-
const listener = chat_server_js_1.chat.toNodeListener(webHandler);
|
|
706
|
-
const req = fakeNodeRequest({});
|
|
707
|
-
const res = fakeNodeResponse();
|
|
708
|
-
await listener(req, res);
|
|
709
|
-
(0, vitest_1.expect)(res.statusCode).toBe(500);
|
|
710
|
-
(0, vitest_1.expect)(res._written()).toBe("boom");
|
|
711
|
-
});
|
|
712
|
-
});
|
|
713
|
-
//# sourceMappingURL=chat-server.test.js.map
|