@copilotkitnext/runtime 0.0.1
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/.cursor/rules/runtime.always.mdc +9 -0
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-check-types.log +4 -0
- package/.turbo/turbo-lint.log +56 -0
- package/.turbo/turbo-test$colon$coverage.log +149 -0
- package/.turbo/turbo-test.log +107 -0
- package/LICENSE +11 -0
- package/README-RUNNERS.md +78 -0
- package/dist/index.d.mts +245 -0
- package/dist/index.d.ts +245 -0
- package/dist/index.js +1873 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1841 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.mjs +3 -0
- package/package.json +62 -0
- package/src/__tests__/get-runtime-info.test.ts +117 -0
- package/src/__tests__/handle-run.test.ts +69 -0
- package/src/__tests__/handle-transcribe.test.ts +289 -0
- package/src/__tests__/in-process-agent-runner-messages.test.ts +599 -0
- package/src/__tests__/in-process-agent-runner.test.ts +726 -0
- package/src/__tests__/middleware.test.ts +432 -0
- package/src/__tests__/routing.test.ts +257 -0
- package/src/endpoint.ts +150 -0
- package/src/handler.ts +3 -0
- package/src/handlers/get-runtime-info.ts +50 -0
- package/src/handlers/handle-connect.ts +144 -0
- package/src/handlers/handle-run.ts +156 -0
- package/src/handlers/handle-transcribe.ts +126 -0
- package/src/index.ts +8 -0
- package/src/middleware.ts +232 -0
- package/src/runner/__tests__/enterprise-runner.test.ts +992 -0
- package/src/runner/__tests__/event-compaction.test.ts +253 -0
- package/src/runner/__tests__/in-memory-runner.test.ts +483 -0
- package/src/runner/__tests__/sqlite-runner.test.ts +975 -0
- package/src/runner/agent-runner.ts +27 -0
- package/src/runner/enterprise.ts +653 -0
- package/src/runner/event-compaction.ts +250 -0
- package/src/runner/in-memory.ts +322 -0
- package/src/runner/index.ts +0 -0
- package/src/runner/sqlite.ts +481 -0
- package/src/runtime.ts +53 -0
- package/src/transcription-service/transcription-service-openai.ts +29 -0
- package/src/transcription-service/transcription-service.ts +11 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.mjs +15 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { vi, type MockedFunction } from "vitest";
|
|
2
|
+
import { createCopilotEndpoint } from "../endpoint";
|
|
3
|
+
import { CopilotRuntime } from "../runtime";
|
|
4
|
+
import { logger } from "@copilotkitnext/shared";
|
|
5
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
6
|
+
import { WebhookStage } from "../middleware";
|
|
7
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
const dummyRuntime = (opts: Partial<CopilotRuntime> = {}) => {
|
|
10
|
+
const runtime = new CopilotRuntime({
|
|
11
|
+
agents: { agent: {} as unknown as AbstractAgent },
|
|
12
|
+
...opts,
|
|
13
|
+
});
|
|
14
|
+
return runtime;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe("CopilotEndpoint middleware", () => {
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
// restore global fetch if it was mocked
|
|
21
|
+
if (fetchMock) {
|
|
22
|
+
global.fetch = originalFetch;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let originalFetch: typeof fetch;
|
|
27
|
+
let fetchMock: MockedFunction<typeof fetch> | null = null;
|
|
28
|
+
|
|
29
|
+
const setupFetchMock = (beforeUrl: string, afterUrl: string) => {
|
|
30
|
+
originalFetch = global.fetch;
|
|
31
|
+
fetchMock = vi.fn().mockImplementation(async (url: string) => {
|
|
32
|
+
if (url === beforeUrl) {
|
|
33
|
+
const body = {
|
|
34
|
+
headers: { "x-modified": "yes" },
|
|
35
|
+
body: { foo: "bar" },
|
|
36
|
+
};
|
|
37
|
+
return new Response(JSON.stringify(body), {
|
|
38
|
+
status: 200,
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (url === afterUrl) {
|
|
43
|
+
return new Response(null, { status: 204 });
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Unexpected fetch URL: ${url}`);
|
|
46
|
+
});
|
|
47
|
+
// Override global fetch for the duration of this test
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
it("processes request through middleware and handler", async () => {
|
|
54
|
+
const originalRequest = new Request("https://example.com/info");
|
|
55
|
+
const modifiedRequest = new Request("https://example.com/info", {
|
|
56
|
+
headers: { "x-modified": "yes" },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const before = vi.fn().mockResolvedValue(modifiedRequest);
|
|
60
|
+
const after = vi.fn().mockResolvedValue(undefined);
|
|
61
|
+
|
|
62
|
+
const runtime = dummyRuntime({
|
|
63
|
+
beforeRequestMiddleware: before,
|
|
64
|
+
afterRequestMiddleware: after,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
68
|
+
const response = await endpoint.fetch(originalRequest);
|
|
69
|
+
|
|
70
|
+
expect(before).toHaveBeenCalledWith({
|
|
71
|
+
runtime,
|
|
72
|
+
request: originalRequest,
|
|
73
|
+
path: expect.any(String),
|
|
74
|
+
});
|
|
75
|
+
expect(after).toHaveBeenCalledWith({
|
|
76
|
+
runtime,
|
|
77
|
+
response,
|
|
78
|
+
path: expect.any(String),
|
|
79
|
+
});
|
|
80
|
+
// The response should contain version info from the /info endpoint
|
|
81
|
+
const body = await response.json();
|
|
82
|
+
expect(body).toHaveProperty("version");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("logs and returns Response error from beforeRequestMiddleware", async () => {
|
|
86
|
+
const errorResponse = new Response("Error", { status: 400 });
|
|
87
|
+
const before = vi.fn().mockRejectedValue(errorResponse);
|
|
88
|
+
const after = vi.fn();
|
|
89
|
+
const runtime = dummyRuntime({
|
|
90
|
+
beforeRequestMiddleware: before,
|
|
91
|
+
afterRequestMiddleware: after,
|
|
92
|
+
});
|
|
93
|
+
const logSpy = vi
|
|
94
|
+
.spyOn(logger, "error")
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
.mockImplementation(() => undefined as any);
|
|
97
|
+
|
|
98
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
99
|
+
const response = await endpoint.fetch(
|
|
100
|
+
new Request("https://example.com/info")
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(response.status).toBe(400);
|
|
104
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
err: errorResponse,
|
|
107
|
+
url: "https://example.com/info",
|
|
108
|
+
path: expect.any(String),
|
|
109
|
+
}),
|
|
110
|
+
"Error running before request middleware"
|
|
111
|
+
);
|
|
112
|
+
expect(after).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("logs and returns 500 error from beforeRequestMiddleware", async () => {
|
|
116
|
+
const error = new Error("before");
|
|
117
|
+
const before = vi.fn().mockRejectedValue(error);
|
|
118
|
+
const after = vi.fn();
|
|
119
|
+
const runtime = dummyRuntime({
|
|
120
|
+
beforeRequestMiddleware: before,
|
|
121
|
+
afterRequestMiddleware: after,
|
|
122
|
+
});
|
|
123
|
+
const logSpy = vi
|
|
124
|
+
.spyOn(logger, "error")
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
126
|
+
.mockImplementation(() => undefined as any);
|
|
127
|
+
|
|
128
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
129
|
+
|
|
130
|
+
const response = await endpoint.fetch(
|
|
131
|
+
new Request("https://example.com/info")
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Hono catches errors and returns them as 500 responses
|
|
135
|
+
expect(response.status).toBe(500);
|
|
136
|
+
|
|
137
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
err: error,
|
|
140
|
+
url: "https://example.com/info",
|
|
141
|
+
path: expect.any(String),
|
|
142
|
+
}),
|
|
143
|
+
"Error running before request middleware"
|
|
144
|
+
);
|
|
145
|
+
expect(after).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("logs error from handler", async () => {
|
|
149
|
+
// Create a mock agent that throws an error
|
|
150
|
+
const before = vi.fn();
|
|
151
|
+
const after = vi.fn();
|
|
152
|
+
const errorAgent = {
|
|
153
|
+
clone: () => {
|
|
154
|
+
throw new Error("Agent error");
|
|
155
|
+
},
|
|
156
|
+
} as unknown as AbstractAgent;
|
|
157
|
+
|
|
158
|
+
const runtime = dummyRuntime({
|
|
159
|
+
beforeRequestMiddleware: before,
|
|
160
|
+
afterRequestMiddleware: after,
|
|
161
|
+
agents: { errorAgent },
|
|
162
|
+
});
|
|
163
|
+
const logSpy = vi
|
|
164
|
+
.spyOn(logger, "error")
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
|
+
.mockImplementation(() => undefined as any);
|
|
167
|
+
|
|
168
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
169
|
+
|
|
170
|
+
const response = await endpoint.fetch(
|
|
171
|
+
new Request("https://example.com/agent/errorAgent/run", {
|
|
172
|
+
method: "POST",
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Hono catches errors and returns them as 500 responses
|
|
177
|
+
expect(response.status).toBe(500);
|
|
178
|
+
|
|
179
|
+
// The actual handler logs the error, not the middleware
|
|
180
|
+
expect(logSpy).toHaveBeenCalled();
|
|
181
|
+
// After middleware is called even on error
|
|
182
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
183
|
+
expect(after).toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("logs but does not rethrow error from afterRequestMiddleware", async () => {
|
|
187
|
+
const error = new Error("after");
|
|
188
|
+
const before = vi.fn();
|
|
189
|
+
const after = vi.fn().mockRejectedValue(error);
|
|
190
|
+
const runtime = dummyRuntime({
|
|
191
|
+
beforeRequestMiddleware: before,
|
|
192
|
+
afterRequestMiddleware: after,
|
|
193
|
+
});
|
|
194
|
+
const logSpy = vi
|
|
195
|
+
.spyOn(logger, "error")
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
197
|
+
.mockImplementation(() => undefined as any);
|
|
198
|
+
|
|
199
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
200
|
+
const response = await endpoint.fetch(
|
|
201
|
+
new Request("https://example.com/info")
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
await new Promise((r) => setImmediate(r));
|
|
205
|
+
|
|
206
|
+
expect(response).toBeInstanceOf(Response);
|
|
207
|
+
expect(after).toHaveBeenCalledWith({
|
|
208
|
+
runtime,
|
|
209
|
+
response,
|
|
210
|
+
path: expect.any(String),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await new Promise((r) => setImmediate(r));
|
|
214
|
+
|
|
215
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
216
|
+
expect.objectContaining({
|
|
217
|
+
err: error,
|
|
218
|
+
url: "https://example.com/info",
|
|
219
|
+
}),
|
|
220
|
+
"Error running after request middleware"
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("processes request through webhook middleware URLs", async () => {
|
|
225
|
+
const beforeURL = "https://hooks.example.com/before";
|
|
226
|
+
const afterURL = "https://hooks.example.com/after";
|
|
227
|
+
setupFetchMock(beforeURL, afterURL);
|
|
228
|
+
|
|
229
|
+
const runtime = dummyRuntime({
|
|
230
|
+
beforeRequestMiddleware: beforeURL,
|
|
231
|
+
afterRequestMiddleware: afterURL,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
235
|
+
const response = await endpoint.fetch(
|
|
236
|
+
new Request("https://example.com/info", {
|
|
237
|
+
headers: { foo: "bar" },
|
|
238
|
+
method: "GET",
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Wait a bit more for async afterRequestMiddleware
|
|
243
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
244
|
+
|
|
245
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
246
|
+
|
|
247
|
+
// Assert payload for before-hook
|
|
248
|
+
const beforeCall = fetchMock!.mock.calls[0];
|
|
249
|
+
expect(beforeCall[0]).toBe(beforeURL);
|
|
250
|
+
expect(beforeCall[1]).toBeDefined();
|
|
251
|
+
expect(beforeCall[1]!.body).toBeDefined();
|
|
252
|
+
const beforePayload = JSON.parse(beforeCall[1]!.body as string);
|
|
253
|
+
expect(beforePayload).toMatchObject({
|
|
254
|
+
method: "GET",
|
|
255
|
+
path: "/info",
|
|
256
|
+
query: "",
|
|
257
|
+
headers: expect.objectContaining({ foo: "bar" }),
|
|
258
|
+
});
|
|
259
|
+
const headers = beforeCall[1]!.headers as Record<string, string>;
|
|
260
|
+
expect(headers["X-CopilotKit-Webhook-Stage"]).toBe(
|
|
261
|
+
WebhookStage.BeforeRequest
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Assert payload for after-hook
|
|
265
|
+
const afterCall = fetchMock!.mock.calls[1];
|
|
266
|
+
expect(afterCall[0]).toBe(afterURL);
|
|
267
|
+
expect(afterCall[1]).toBeDefined();
|
|
268
|
+
expect(afterCall[1]!.body).toBeDefined();
|
|
269
|
+
const afterPayload = JSON.parse(afterCall[1]!.body as string);
|
|
270
|
+
expect(afterPayload).toMatchObject({
|
|
271
|
+
status: 200,
|
|
272
|
+
headers: expect.objectContaining({
|
|
273
|
+
"content-type": "application/json",
|
|
274
|
+
}),
|
|
275
|
+
body: expect.any(String),
|
|
276
|
+
});
|
|
277
|
+
const afterHeaders = afterCall[1]!.headers as Record<string, string>;
|
|
278
|
+
expect(afterHeaders["X-CopilotKit-Webhook-Stage"]).toBe(
|
|
279
|
+
WebhookStage.AfterRequest
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Response should still be successful
|
|
283
|
+
expect(response.status).toBe(200);
|
|
284
|
+
const body = await response.json();
|
|
285
|
+
expect(body).toHaveProperty("version");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("applies webhook middleware request modifications", async () => {
|
|
289
|
+
const beforeURL = "https://hooks.example.com/before";
|
|
290
|
+
const afterURL = "https://hooks.example.com/after";
|
|
291
|
+
setupFetchMock(beforeURL, afterURL);
|
|
292
|
+
|
|
293
|
+
const runtime = dummyRuntime({
|
|
294
|
+
beforeRequestMiddleware: beforeURL,
|
|
295
|
+
afterRequestMiddleware: afterURL,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
299
|
+
|
|
300
|
+
// Make a POST request to info endpoint since it's simpler
|
|
301
|
+
const response = await endpoint.fetch(
|
|
302
|
+
new Request("https://example.com/info", {
|
|
303
|
+
headers: { foo: "bar" },
|
|
304
|
+
method: "GET",
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Should get a successful response
|
|
309
|
+
expect(response.status).toBe(200);
|
|
310
|
+
|
|
311
|
+
// Wait for async afterRequestMiddleware
|
|
312
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
313
|
+
|
|
314
|
+
// The webhook middleware should have been called
|
|
315
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("handles webhook middleware timeout", async () => {
|
|
319
|
+
const beforeURL = "https://hooks.example.com/before";
|
|
320
|
+
originalFetch = global.fetch;
|
|
321
|
+
|
|
322
|
+
// Create an AbortController to simulate timeout
|
|
323
|
+
let abortSignal: AbortSignal | undefined;
|
|
324
|
+
fetchMock = vi
|
|
325
|
+
.fn()
|
|
326
|
+
.mockImplementation(async (_url: string, init?: RequestInit) => {
|
|
327
|
+
abortSignal = init?.signal;
|
|
328
|
+
// Wait for abort signal
|
|
329
|
+
return new Promise<Response>((_resolve, reject) => {
|
|
330
|
+
if (abortSignal) {
|
|
331
|
+
abortSignal.addEventListener("abort", () => {
|
|
332
|
+
reject(new Error("Aborted"));
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
338
|
+
// @ts-ignore
|
|
339
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
340
|
+
|
|
341
|
+
const runtime = dummyRuntime({
|
|
342
|
+
beforeRequestMiddleware: beforeURL,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
346
|
+
|
|
347
|
+
// Should return 502 on timeout
|
|
348
|
+
const response = await endpoint.fetch(
|
|
349
|
+
new Request("https://example.com/info")
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
expect(response.status).toBe(502);
|
|
353
|
+
|
|
354
|
+
// Verify that the fetch was aborted due to timeout
|
|
355
|
+
expect(abortSignal?.aborted).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("handles webhook middleware error responses", async () => {
|
|
359
|
+
const beforeURL = "https://hooks.example.com/before";
|
|
360
|
+
originalFetch = global.fetch;
|
|
361
|
+
fetchMock = vi.fn().mockImplementation(async () => {
|
|
362
|
+
return new Response("Bad request", { status: 400 });
|
|
363
|
+
});
|
|
364
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
365
|
+
// @ts-ignore
|
|
366
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
367
|
+
|
|
368
|
+
const runtime = dummyRuntime({
|
|
369
|
+
beforeRequestMiddleware: beforeURL,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
373
|
+
|
|
374
|
+
// Should pass through error response
|
|
375
|
+
const response = await endpoint.fetch(
|
|
376
|
+
new Request("https://example.com/info")
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(response.status).toBe(400);
|
|
380
|
+
expect(await response.text()).toBe("Bad request");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("handles webhook middleware server error", async () => {
|
|
384
|
+
const beforeURL = "https://hooks.example.com/before";
|
|
385
|
+
originalFetch = global.fetch;
|
|
386
|
+
fetchMock = vi.fn().mockImplementation(async () => {
|
|
387
|
+
return new Response("Server error", { status: 500 });
|
|
388
|
+
});
|
|
389
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
390
|
+
// @ts-ignore
|
|
391
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
392
|
+
|
|
393
|
+
const runtime = dummyRuntime({
|
|
394
|
+
beforeRequestMiddleware: beforeURL,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
398
|
+
|
|
399
|
+
// Should return 502 on server error
|
|
400
|
+
const response = await endpoint.fetch(
|
|
401
|
+
new Request("https://example.com/info")
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
expect(response.status).toBe(502);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("handles webhook middleware 204 response", async () => {
|
|
408
|
+
const beforeURL = "https://hooks.example.com/before";
|
|
409
|
+
originalFetch = global.fetch;
|
|
410
|
+
fetchMock = vi.fn().mockImplementation(async () => {
|
|
411
|
+
return new Response(null, { status: 204 });
|
|
412
|
+
});
|
|
413
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
414
|
+
// @ts-ignore
|
|
415
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
416
|
+
|
|
417
|
+
const runtime = dummyRuntime({
|
|
418
|
+
beforeRequestMiddleware: beforeURL,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
422
|
+
|
|
423
|
+
// Should continue with original request on 204
|
|
424
|
+
const response = await endpoint.fetch(
|
|
425
|
+
new Request("https://example.com/info")
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
expect(response.status).toBe(200);
|
|
429
|
+
const body = await response.json();
|
|
430
|
+
expect(body).toHaveProperty("version");
|
|
431
|
+
});
|
|
432
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { createCopilotEndpoint } from "../endpoint";
|
|
2
|
+
import { CopilotRuntime } from "../runtime";
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
5
|
+
|
|
6
|
+
describe("CopilotEndpoint routing", () => {
|
|
7
|
+
// Helper function to create a Request object with a given URL
|
|
8
|
+
const createRequest = (url: string, method: string = "GET"): Request => {
|
|
9
|
+
return new Request(url, { method });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Create a mock runtime with a basic agent
|
|
13
|
+
const createMockRuntime = () => {
|
|
14
|
+
const createMockAgent = () => {
|
|
15
|
+
const agent: unknown = {
|
|
16
|
+
execute: async () => ({ events: [] }),
|
|
17
|
+
};
|
|
18
|
+
(agent as { clone: () => unknown }).clone = () => createMockAgent();
|
|
19
|
+
return agent as AbstractAgent;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return new CopilotRuntime({
|
|
23
|
+
agents: {
|
|
24
|
+
default: createMockAgent(),
|
|
25
|
+
myAgent: createMockAgent(),
|
|
26
|
+
agent123: createMockAgent(),
|
|
27
|
+
"my-agent": createMockAgent(),
|
|
28
|
+
my_agent: createMockAgent(),
|
|
29
|
+
testAgent: createMockAgent(),
|
|
30
|
+
test: createMockAgent(),
|
|
31
|
+
"test%20agent": createMockAgent(),
|
|
32
|
+
"test agent": createMockAgent(),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Helper to test routing
|
|
38
|
+
const testRoute = async (
|
|
39
|
+
url: string,
|
|
40
|
+
method: string = "GET",
|
|
41
|
+
body?: unknown
|
|
42
|
+
) => {
|
|
43
|
+
const runtime = createMockRuntime();
|
|
44
|
+
const endpoint = createCopilotEndpoint({ runtime, basePath: "/" });
|
|
45
|
+
const requestInit: RequestInit = { method };
|
|
46
|
+
if (body) {
|
|
47
|
+
requestInit.body = JSON.stringify(body);
|
|
48
|
+
requestInit.headers = { "Content-Type": "application/json" };
|
|
49
|
+
}
|
|
50
|
+
const request = createRequest(url, method);
|
|
51
|
+
const response = await endpoint.fetch(request);
|
|
52
|
+
return response;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
describe("RunAgent route pattern", () => {
|
|
56
|
+
it("should match agent run URL with simple agent name", async () => {
|
|
57
|
+
const response = await testRoute(
|
|
58
|
+
"https://example.com/agent/myAgent/run",
|
|
59
|
+
"POST",
|
|
60
|
+
{
|
|
61
|
+
agentId: "myAgent",
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Should not be 404
|
|
66
|
+
expect(response.status).not.toBe(404);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should match agent run URL with alphanumeric agent name", async () => {
|
|
70
|
+
const response = await testRoute(
|
|
71
|
+
"https://example.com/agent/agent123/run",
|
|
72
|
+
"POST",
|
|
73
|
+
{
|
|
74
|
+
agentId: "agent123",
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(response.status).not.toBe(404);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should match agent run URL with hyphenated agent name", async () => {
|
|
82
|
+
const response = await testRoute(
|
|
83
|
+
"https://example.com/agent/my-agent/run",
|
|
84
|
+
"POST",
|
|
85
|
+
{
|
|
86
|
+
agentId: "my-agent",
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(response.status).not.toBe(404);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should match agent run URL with underscored agent name", async () => {
|
|
94
|
+
const response = await testRoute(
|
|
95
|
+
"https://example.com/agent/my_agent/run",
|
|
96
|
+
"POST",
|
|
97
|
+
{
|
|
98
|
+
agentId: "my_agent",
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(response.status).not.toBe(404);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should not match agent run URL with empty agent name", async () => {
|
|
106
|
+
const response = await testRoute(
|
|
107
|
+
"https://example.com/agent//run",
|
|
108
|
+
"POST"
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(response.status).toBe(404);
|
|
112
|
+
const body = await response.json();
|
|
113
|
+
expect(body).toEqual({ error: "Not found" });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should not match partial agent run URL", async () => {
|
|
117
|
+
const response = await testRoute(
|
|
118
|
+
"https://example.com/agent/myAgent",
|
|
119
|
+
"POST"
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(response.status).toBe(404);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should not match agent run URL with extra path segments", async () => {
|
|
126
|
+
const response = await testRoute(
|
|
127
|
+
"https://example.com/agent/myAgent/run/extra",
|
|
128
|
+
"POST"
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(response.status).toBe(404);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("GetRuntimeInfo route pattern (/info endpoint)", () => {
|
|
136
|
+
it("should match simple info URL", async () => {
|
|
137
|
+
const response = await testRoute("https://example.com/info");
|
|
138
|
+
|
|
139
|
+
expect(response.status).toBe(200);
|
|
140
|
+
const body = await response.json();
|
|
141
|
+
expect(body).toHaveProperty("version");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should match info URL with query parameters", async () => {
|
|
145
|
+
const response = await testRoute("https://example.com/info?param=value");
|
|
146
|
+
|
|
147
|
+
expect(response.status).toBe(200);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should not match non-info URLs", async () => {
|
|
151
|
+
const response = await testRoute("https://example.com/agents");
|
|
152
|
+
|
|
153
|
+
expect(response.status).toBe(404);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("Transcribe route pattern (/transcribe endpoint)", () => {
|
|
158
|
+
it("should match simple transcribe URL", async () => {
|
|
159
|
+
// Transcribe expects POST method and audio data
|
|
160
|
+
const response = await testRoute(
|
|
161
|
+
"https://example.com/transcribe",
|
|
162
|
+
"POST",
|
|
163
|
+
{}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// It might return an error since we're not providing audio, but it shouldn't be 404
|
|
167
|
+
expect(response.status).not.toBe(404);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should match transcribe URL with query parameters", async () => {
|
|
171
|
+
const response = await testRoute(
|
|
172
|
+
"https://example.com/transcribe?format=json",
|
|
173
|
+
"POST",
|
|
174
|
+
{}
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(response.status).not.toBe(404);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should not match transcribe URLs with extra path segments", async () => {
|
|
181
|
+
const response = await testRoute(
|
|
182
|
+
"https://example.com/transcribe/extra",
|
|
183
|
+
"POST"
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(response.status).toBe(404);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("Unmatched routes (404 behavior)", () => {
|
|
191
|
+
it("should return 404 for root path", async () => {
|
|
192
|
+
const response = await testRoute("https://example.com/");
|
|
193
|
+
|
|
194
|
+
expect(response.status).toBe(404);
|
|
195
|
+
const body = await response.json();
|
|
196
|
+
expect(body).toEqual({ error: "Not found" });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should return 404 for unknown paths", async () => {
|
|
200
|
+
const response = await testRoute("https://example.com/unknown/path");
|
|
201
|
+
|
|
202
|
+
expect(response.status).toBe(404);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should return 404 for malformed agent paths", async () => {
|
|
206
|
+
const response = await testRoute("https://example.com/agent/run", "POST");
|
|
207
|
+
|
|
208
|
+
expect(response.status).toBe(404);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should return 404 for agents path", async () => {
|
|
212
|
+
const response = await testRoute("https://example.com/agents");
|
|
213
|
+
|
|
214
|
+
expect(response.status).toBe(404);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("Edge cases", () => {
|
|
219
|
+
it("should handle URLs with different domains", async () => {
|
|
220
|
+
const response = await testRoute(
|
|
221
|
+
"http://localhost:3000/agent/test/run",
|
|
222
|
+
"POST",
|
|
223
|
+
{
|
|
224
|
+
agentId: "test",
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(response.status).not.toBe(404);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should handle URLs with ports for info endpoint", async () => {
|
|
232
|
+
const response = await testRoute("https://api.example.com:8080/info");
|
|
233
|
+
|
|
234
|
+
expect(response.status).toBe(200);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should handle URLs with ports for transcribe endpoint", async () => {
|
|
238
|
+
const response = await testRoute(
|
|
239
|
+
"https://api.example.com:8080/transcribe",
|
|
240
|
+
"POST",
|
|
241
|
+
{}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(response.status).not.toBe(404);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should handle URLs with special characters in agent names", async () => {
|
|
248
|
+
const response = await testRoute(
|
|
249
|
+
"https://example.com/agent/test%20agent/run",
|
|
250
|
+
"POST",
|
|
251
|
+
{ agentId: "test%20agent" }
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(response.status).not.toBe(404);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|