@copilotkit/vue 1.78.0 → 1.79.0
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/index.cjs +1 -1
- package/dist/index.mjs +2 -2
- package/dist/use-render-activity-message-DxYudLWg.cjs +70 -0
- package/dist/use-render-activity-message-DxYudLWg.cjs.map +1 -0
- package/dist/{use-render-activity-message-B9MOLfR_.js → use-render-activity-message-sBoXH1Ew.js} +2359 -2373
- package/dist/use-render-activity-message-sBoXH1Ew.js.map +1 -0
- package/dist/v2/hooks/use-agent.d.ts.map +1 -1
- package/dist/v2/hooks/use-threads.d.ts.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/providers/CopilotKitProvider.types.d.ts +7 -2
- package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts +2 -1
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/v2/hooks/__tests__/use-agent-stability.test.ts +11 -0
- package/src/v2/hooks/__tests__/use-threads.test.ts +27 -6
- package/src/v2/hooks/use-agent.ts +28 -22
- package/src/v2/hooks/use-threads.ts +9 -8
- package/src/v2/lib/__tests__/transcription-client.test.ts +2 -0
- package/src/v2/lib/transcription-client.ts +1 -1
- package/src/v2/providers/CopilotKitProvider.types.ts +7 -1
- package/src/v2/providers/CopilotKitProvider.vue +62 -12
- package/src/v2/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.ts +598 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +8 -3
- package/dist/use-render-activity-message-B9MOLfR_.js.map +0 -1
- package/dist/use-render-activity-message-eN0gdzOa.cjs +0 -70
- package/dist/use-render-activity-message-eN0gdzOa.cjs.map +0 -1
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1937: the headers builder is evaluated when a request is SENT, not during
|
|
3
|
+
* render/setup. Mirrors
|
|
4
|
+
* `react-core/src/v2/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.tsx`.
|
|
5
|
+
*/
|
|
6
|
+
import { mount } from "@vue/test-utils";
|
|
7
|
+
import { defineComponent, h, nextTick } from "vue";
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
10
|
+
import type { CopilotKitHeadersSource } from "@copilotkit/core";
|
|
11
|
+
import CopilotKitProvider from "../CopilotKitProvider.vue";
|
|
12
|
+
import { useCopilotKit } from "../useCopilotKit";
|
|
13
|
+
import { useAgent } from "../../hooks/use-agent";
|
|
14
|
+
import { useThreads } from "../../hooks/use-threads";
|
|
15
|
+
import { transcribeAudio } from "../../lib/transcription-client";
|
|
16
|
+
import type { CopilotKitCoreVue } from "../../lib/vue-core";
|
|
17
|
+
|
|
18
|
+
type Call = { url: string; auth: string | null; publicApiKey: string | null };
|
|
19
|
+
|
|
20
|
+
function stubFetch(calls: Call[]) {
|
|
21
|
+
return vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
22
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
23
|
+
const headers = new Headers(init?.headers ?? (input as Request).headers);
|
|
24
|
+
calls.push({
|
|
25
|
+
url,
|
|
26
|
+
auth: headers.get("authorization"),
|
|
27
|
+
publicApiKey: headers.get("x-copilotcloud-public-api-key"),
|
|
28
|
+
});
|
|
29
|
+
let body: unknown = {};
|
|
30
|
+
try {
|
|
31
|
+
body = init?.body ? JSON.parse(String(init.body)) : {};
|
|
32
|
+
} catch {
|
|
33
|
+
// Non-JSON body (e.g. connect/transcribe form data) — irrelevant here.
|
|
34
|
+
}
|
|
35
|
+
const method = (body as { method?: string }).method;
|
|
36
|
+
if (url.endsWith("/info") || method === "info") {
|
|
37
|
+
return new Response(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
agents: { default: { name: "default", description: "" } },
|
|
41
|
+
}),
|
|
42
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (url.includes("/threads?")) {
|
|
46
|
+
return new Response(JSON.stringify({ threads: [], nextCursor: null }), {
|
|
47
|
+
status: 200,
|
|
48
|
+
headers: { "content-type": "application/json" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// A non-2xx here would read as a runtime health signal (core re-probes
|
|
52
|
+
// /info on a failed request while Connected). The run/connect body
|
|
53
|
+
// itself is irrelevant to every assertion here (only request headers are
|
|
54
|
+
// inspected), so keep every other request a plain 200.
|
|
55
|
+
return new Response("{}", {
|
|
56
|
+
status: 200,
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let token = "tok-1";
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
vi.unstubAllGlobals();
|
|
66
|
+
token = "tok-1";
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Poll a predicate for about `ms`, flushing Vue's reactivity each tick. */
|
|
70
|
+
async function pollFor(predicate: () => boolean, ms = 50): Promise<void> {
|
|
71
|
+
const start = Date.now();
|
|
72
|
+
while (Date.now() - start < ms) {
|
|
73
|
+
if (predicate()) return;
|
|
74
|
+
await nextTick();
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mountCore(props: Record<string, unknown>): {
|
|
80
|
+
getCore: () => CopilotKitCoreVue;
|
|
81
|
+
wrapper: ReturnType<typeof mount>;
|
|
82
|
+
} {
|
|
83
|
+
let core: CopilotKitCoreVue | undefined;
|
|
84
|
+
const Probe = defineComponent({
|
|
85
|
+
setup() {
|
|
86
|
+
const { copilotkit } = useCopilotKit();
|
|
87
|
+
core = copilotkit.value;
|
|
88
|
+
return () => null;
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const wrapper = mount(CopilotKitProvider, {
|
|
93
|
+
props,
|
|
94
|
+
slots: { default: () => h(Probe) },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
wrapper,
|
|
99
|
+
getCore: () => {
|
|
100
|
+
if (!core) throw new Error("CopilotKit core not available");
|
|
101
|
+
return core;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("CopilotKitProvider — headers builder evaluated at send time (#1937)", () => {
|
|
107
|
+
it("a run carries the builder's current token when nothing about the provider changed", async () => {
|
|
108
|
+
const calls: Call[] = [];
|
|
109
|
+
vi.stubGlobal("fetch", stubFetch(calls));
|
|
110
|
+
|
|
111
|
+
const { getCore } = mountCore({
|
|
112
|
+
runtimeUrl: "http://rt.test/api/copilotkit",
|
|
113
|
+
headers: (() => ({
|
|
114
|
+
Authorization: `Bearer ${token}`,
|
|
115
|
+
})) as CopilotKitHeadersSource,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await vi.waitFor(() => expect(getCore().getAgent("default")).toBeDefined());
|
|
119
|
+
const callsBeforeRun = calls.length;
|
|
120
|
+
|
|
121
|
+
// Token rotates (e.g. a Clerk re-mint). Nothing about the provider's
|
|
122
|
+
// props changes — no re-render/re-setup exists to eagerly re-resolve.
|
|
123
|
+
token = "tok-2";
|
|
124
|
+
|
|
125
|
+
const agent = getCore().getAgent("default")!;
|
|
126
|
+
await getCore()
|
|
127
|
+
.runAgent({ agent })
|
|
128
|
+
.catch(() => {});
|
|
129
|
+
|
|
130
|
+
const runCalls = calls.slice(callsBeforeRun);
|
|
131
|
+
expect(runCalls.length).toBeGreaterThan(0);
|
|
132
|
+
expect(runCalls.every((c) => c.auth === "Bearer tok-2")).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("an async builder is awaited", async () => {
|
|
136
|
+
const calls: Call[] = [];
|
|
137
|
+
vi.stubGlobal("fetch", stubFetch(calls));
|
|
138
|
+
|
|
139
|
+
const { getCore } = mountCore({
|
|
140
|
+
runtimeUrl: "http://rt.test/api/copilotkit",
|
|
141
|
+
headers: (async () => {
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
143
|
+
return { Authorization: `Bearer ${token}` };
|
|
144
|
+
}) as CopilotKitHeadersSource,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await vi.waitFor(() => expect(getCore().getAgent("default")).toBeDefined());
|
|
148
|
+
const callsBeforeRun = calls.length;
|
|
149
|
+
|
|
150
|
+
token = "tok-2";
|
|
151
|
+
|
|
152
|
+
const agent = getCore().getAgent("default")!;
|
|
153
|
+
await getCore()
|
|
154
|
+
.runAgent({ agent })
|
|
155
|
+
.catch(() => {});
|
|
156
|
+
|
|
157
|
+
const runCalls = calls.slice(callsBeforeRun);
|
|
158
|
+
expect(runCalls.length).toBeGreaterThan(0);
|
|
159
|
+
expect(runCalls.every((c) => c.auth === "Bearer tok-2")).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("a provisional proxy's first connect carries the fresh token", async () => {
|
|
163
|
+
const calls: Call[] = [];
|
|
164
|
+
let releaseInfo!: (value: Response) => void;
|
|
165
|
+
vi.stubGlobal(
|
|
166
|
+
"fetch",
|
|
167
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
168
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
169
|
+
const headers = new Headers(
|
|
170
|
+
init?.headers ?? (input as Request).headers,
|
|
171
|
+
);
|
|
172
|
+
calls.push({
|
|
173
|
+
url,
|
|
174
|
+
auth: headers.get("authorization"),
|
|
175
|
+
publicApiKey: headers.get("x-copilotcloud-public-api-key"),
|
|
176
|
+
});
|
|
177
|
+
if (url.endsWith("/info")) {
|
|
178
|
+
// Held open: the runtime stays Connecting, so `useAgent` hands
|
|
179
|
+
// back a provisional `ProxiedCopilotRuntimeAgent` instead of the
|
|
180
|
+
// real synced one.
|
|
181
|
+
return new Promise<Response>((resolve) => {
|
|
182
|
+
releaseInfo = resolve;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return new Response("", { status: 200 });
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
let agentRef!: {
|
|
190
|
+
headers?: Record<string, string>;
|
|
191
|
+
fetch: typeof fetch;
|
|
192
|
+
};
|
|
193
|
+
const Child = defineComponent({
|
|
194
|
+
setup() {
|
|
195
|
+
const { agent } = useAgent({ agentId: "default" });
|
|
196
|
+
agentRef = agent.value as unknown as typeof agentRef;
|
|
197
|
+
return () => null;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let core!: CopilotKitCoreVue;
|
|
202
|
+
const Probe = defineComponent({
|
|
203
|
+
setup() {
|
|
204
|
+
const { copilotkit } = useCopilotKit();
|
|
205
|
+
core = copilotkit.value;
|
|
206
|
+
return () => null;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
mount(CopilotKitProvider, {
|
|
211
|
+
props: {
|
|
212
|
+
runtimeUrl: "http://rt.test/api/copilotkit",
|
|
213
|
+
headers: (() => ({
|
|
214
|
+
Authorization: `Bearer ${token}`,
|
|
215
|
+
})) as CopilotKitHeadersSource,
|
|
216
|
+
},
|
|
217
|
+
slots: { default: () => [h(Child), h(Probe)] },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await nextTick();
|
|
221
|
+
expect(core.getAgent("default")).toBeUndefined();
|
|
222
|
+
|
|
223
|
+
token = "tok-2";
|
|
224
|
+
const callsBeforeConnect = calls.length;
|
|
225
|
+
|
|
226
|
+
// Call the proxy's OWN installed fetch directly — this is exactly what a
|
|
227
|
+
// real `/agent/default/connect` send does, without pulling in the
|
|
228
|
+
// AG-UI stream-parsing machinery a fabricated empty response can't
|
|
229
|
+
// satisfy. `applyHeadersToAgent` installed this as `ɵruntimeFetch` when
|
|
230
|
+
// the provisional was constructed, so it resolves headers fresh on
|
|
231
|
+
// every call, never a baked-in snapshot from construction time.
|
|
232
|
+
await agentRef.fetch(
|
|
233
|
+
"http://rt.test/api/copilotkit/agent/default/connect",
|
|
234
|
+
{
|
|
235
|
+
method: "POST",
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const connectCalls = calls.slice(callsBeforeConnect);
|
|
240
|
+
expect(connectCalls.length).toBeGreaterThan(0);
|
|
241
|
+
expect(connectCalls.every((c) => c.auth === "Bearer tok-2")).toBe(true);
|
|
242
|
+
// `releaseInfo` is intentionally never called: this test only needs the
|
|
243
|
+
// runtime held in "Connecting" long enough to observe the provisional
|
|
244
|
+
// proxy's own fetch.
|
|
245
|
+
void releaseInfo;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it.each([
|
|
249
|
+
{ label: "no publicApiKey", publicApiKey: undefined },
|
|
250
|
+
// With a publicApiKey, `ɵwithHeaderDefaults` is no longer a passthrough:
|
|
251
|
+
// it wraps the builder in a NEW closure on every call (non-empty
|
|
252
|
+
// defaults), so this row is the one that actually exercises the memo —
|
|
253
|
+
// the plain-builder row above still passes even without it, because
|
|
254
|
+
// `ɵwithHeaderDefaults` returns the source unchanged when there is
|
|
255
|
+
// nothing to fill in.
|
|
256
|
+
{ label: "with publicApiKey (Cloud)", publicApiKey: "pk_test_123" },
|
|
257
|
+
])(
|
|
258
|
+
"inline builder across 20 prop updates: 0 extra setHeaders ($label)",
|
|
259
|
+
async ({ publicApiKey }) => {
|
|
260
|
+
const calls: Call[] = [];
|
|
261
|
+
vi.stubGlobal("fetch", stubFetch(calls));
|
|
262
|
+
|
|
263
|
+
const { getCore, wrapper } = mountCore({
|
|
264
|
+
runtimeUrl: "http://rt.test/api/copilotkit",
|
|
265
|
+
publicApiKey,
|
|
266
|
+
headers: (() => ({
|
|
267
|
+
Authorization: `Bearer ${token}`,
|
|
268
|
+
})) as CopilotKitHeadersSource,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
await vi.waitFor(() =>
|
|
272
|
+
expect(getCore().getAgent("default")).toBeDefined(),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const setHeadersSpy = vi.spyOn(getCore(), "setHeaders");
|
|
276
|
+
|
|
277
|
+
for (let i = 0; i < 20; i++) {
|
|
278
|
+
// A NEW inline arrow every update: identity churns even though the
|
|
279
|
+
// builder's behavior never does.
|
|
280
|
+
await wrapper.setProps({
|
|
281
|
+
headers: (() => ({
|
|
282
|
+
Authorization: `Bearer ${token}`,
|
|
283
|
+
})) as CopilotKitHeadersSource,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await pollFor(() => false, 30);
|
|
288
|
+
|
|
289
|
+
expect(setHeadersSpy).toHaveBeenCalledTimes(0);
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
it("the public API key header is added when headers omit it", async () => {
|
|
294
|
+
const calls: Call[] = [];
|
|
295
|
+
vi.stubGlobal("fetch", stubFetch(calls));
|
|
296
|
+
|
|
297
|
+
mountCore({
|
|
298
|
+
runtimeUrl: "http://rt.test/api/copilotkit",
|
|
299
|
+
publicApiKey: "pk_test_123",
|
|
300
|
+
headers: (() => ({})) as CopilotKitHeadersSource,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await vi.waitFor(() => {
|
|
304
|
+
const infoCall = calls.find((c) => c.url.endsWith("/info"));
|
|
305
|
+
expect(infoCall).toBeDefined();
|
|
306
|
+
expect(infoCall?.publicApiKey).toBe("pk_test_123");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("a token change does not re-dispatch the thread context", async () => {
|
|
311
|
+
const calls: Call[] = [];
|
|
312
|
+
vi.stubGlobal("fetch", stubFetch(calls));
|
|
313
|
+
|
|
314
|
+
const Threads = defineComponent({
|
|
315
|
+
setup() {
|
|
316
|
+
useThreads({ agentId: "default" });
|
|
317
|
+
return () => null;
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
let core!: CopilotKitCoreVue;
|
|
322
|
+
const Probe = defineComponent({
|
|
323
|
+
setup() {
|
|
324
|
+
const { copilotkit } = useCopilotKit();
|
|
325
|
+
core = copilotkit.value;
|
|
326
|
+
return () => null;
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
mount(CopilotKitProvider, {
|
|
331
|
+
props: {
|
|
332
|
+
runtimeUrl: "http://rt.test/api/copilotkit",
|
|
333
|
+
headers: (() => ({
|
|
334
|
+
Authorization: `Bearer ${token}`,
|
|
335
|
+
})) as CopilotKitHeadersSource,
|
|
336
|
+
},
|
|
337
|
+
slots: { default: () => [h(Threads), h(Probe)] },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await vi.waitFor(() => expect(core.getAgent("default")).toBeDefined());
|
|
341
|
+
|
|
342
|
+
const store = await vi.waitFor(() => {
|
|
343
|
+
const s = core.getThreadStore("default");
|
|
344
|
+
expect(s).toBeDefined();
|
|
345
|
+
return s!;
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Wait for the first (mount) context dispatch before spying, so the spy
|
|
349
|
+
// only observes dispatches caused by the token rotation below.
|
|
350
|
+
await vi.waitFor(() => {
|
|
351
|
+
expect(calls.some((c) => c.url.includes("/threads?"))).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const setContextSpy = vi.spyOn(store, "setContext");
|
|
355
|
+
|
|
356
|
+
token = "tok-2";
|
|
357
|
+
// A run actually changes the resolved snapshot (routes through
|
|
358
|
+
// `ɵruntimeFetch`, which resolves the builder's now-current token) — a
|
|
359
|
+
// rotation nobody ever reads is not a real test of "the snapshot changed
|
|
360
|
+
// but nothing re-dispatched".
|
|
361
|
+
const agent = core.getAgent("default")!;
|
|
362
|
+
await core.runAgent({ agent }).catch(() => {});
|
|
363
|
+
|
|
364
|
+
// Force a Vue-level trigger entirely unrelated to headers. The OLD
|
|
365
|
+
// values-keyed `headersKey` would have re-evaluated (and seen the
|
|
366
|
+
// now-changed snapshot) the next time ANYTHING pokes Vue's reactivity
|
|
367
|
+
// around `copilotkit` — this is that poke.
|
|
368
|
+
core.setRenderToolCalls([]);
|
|
369
|
+
await pollFor(() => false, 50);
|
|
370
|
+
|
|
371
|
+
expect(setContextSpy).toHaveBeenCalledTimes(0);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("transcribeAudio never re-sends a stale header snapshot key (#1937)", async () => {
|
|
375
|
+
const calls: Call[] = [];
|
|
376
|
+
let signedIn = true;
|
|
377
|
+
vi.stubGlobal(
|
|
378
|
+
"fetch",
|
|
379
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
380
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
381
|
+
const headers = new Headers(
|
|
382
|
+
init?.headers ?? (input as Request).headers,
|
|
383
|
+
);
|
|
384
|
+
calls.push({
|
|
385
|
+
url,
|
|
386
|
+
auth: headers.get("authorization"),
|
|
387
|
+
publicApiKey: null,
|
|
388
|
+
});
|
|
389
|
+
if (url.endsWith("/info")) {
|
|
390
|
+
return new Response(
|
|
391
|
+
JSON.stringify({
|
|
392
|
+
version: "1.0.0",
|
|
393
|
+
agents: { default: { name: "default", description: "" } },
|
|
394
|
+
}),
|
|
395
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (url.endsWith("/transcribe")) {
|
|
399
|
+
return new Response(
|
|
400
|
+
JSON.stringify({ text: "hi", size: 1, type: "audio/webm" }),
|
|
401
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return new Response("{}", { status: 200 });
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const { getCore } = mountCore({
|
|
409
|
+
runtimeUrl: "http://rt.test/api/copilotkit",
|
|
410
|
+
headers: (() =>
|
|
411
|
+
signedIn
|
|
412
|
+
? { Authorization: "Bearer x" }
|
|
413
|
+
: {}) as CopilotKitHeadersSource,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
await vi.waitFor(() => expect(getCore().getAgent("default")).toBeDefined());
|
|
417
|
+
|
|
418
|
+
// Force an actual resolution while signed in, so the header source's
|
|
419
|
+
// last snapshot is non-empty — otherwise a stale-merge bug has nothing
|
|
420
|
+
// stale to leak, and the assertion below would pass vacuously.
|
|
421
|
+
await transcribeAudio(getCore(), new Blob(["a"], { type: "audio/webm" }));
|
|
422
|
+
const firstCall = calls.find((c) => c.url.endsWith("/transcribe"));
|
|
423
|
+
expect(firstCall?.auth).toBe("Bearer x");
|
|
424
|
+
|
|
425
|
+
calls.length = 0;
|
|
426
|
+
signedIn = false;
|
|
427
|
+
|
|
428
|
+
await transcribeAudio(getCore(), new Blob(["a"], { type: "audio/webm" }));
|
|
429
|
+
const secondCall = calls.find((c) => c.url.endsWith("/transcribe"));
|
|
430
|
+
expect(secondCall).toBeDefined();
|
|
431
|
+
expect(secondCall!.auth).toBeNull();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("a plain HttpAgent's headers refresh via applyHeadersToAgent when ɵheadersGeneration changes, not on header VALUES alone", async () => {
|
|
435
|
+
const directAgent = new HttpAgent({ url: "http://direct.test/agent" });
|
|
436
|
+
|
|
437
|
+
let agentRef!: { headers?: Record<string, string> };
|
|
438
|
+
const Child = defineComponent({
|
|
439
|
+
setup() {
|
|
440
|
+
const { agent } = useAgent({ agentId: "direct" });
|
|
441
|
+
agentRef = agent.value as unknown as typeof agentRef;
|
|
442
|
+
return () => null;
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const { getCore } = (() => {
|
|
447
|
+
let core!: CopilotKitCoreVue;
|
|
448
|
+
const Probe = defineComponent({
|
|
449
|
+
setup() {
|
|
450
|
+
const { copilotkit } = useCopilotKit();
|
|
451
|
+
core = copilotkit.value;
|
|
452
|
+
return () => null;
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
mount(CopilotKitProvider, {
|
|
456
|
+
props: {
|
|
457
|
+
agents__unsafe_dev_only: { direct: directAgent },
|
|
458
|
+
headers: {
|
|
459
|
+
Authorization: "Bearer static-1",
|
|
460
|
+
} as CopilotKitHeadersSource,
|
|
461
|
+
},
|
|
462
|
+
slots: { default: () => [h(Child), h(Probe)] },
|
|
463
|
+
});
|
|
464
|
+
return { getCore: () => core };
|
|
465
|
+
})();
|
|
466
|
+
|
|
467
|
+
await nextTick();
|
|
468
|
+
expect(agentRef.headers?.Authorization).toBe("Bearer static-1");
|
|
469
|
+
|
|
470
|
+
// Mutate the resolved snapshot IN PLACE — no `setHeaders()`, so
|
|
471
|
+
// `ɵheadersGeneration` never bumps. The watcher must not react to this:
|
|
472
|
+
// it is keyed on the generation, never on header VALUES.
|
|
473
|
+
getCore().headers.Authorization = "Bearer mutated-in-place";
|
|
474
|
+
await nextTick();
|
|
475
|
+
expect(agentRef.headers?.Authorization).toBe("Bearer static-1");
|
|
476
|
+
|
|
477
|
+
// A real `setHeaders()` bumps `ɵheadersGeneration` — THAT is what the
|
|
478
|
+
// watcher is keyed on, and it re-applies via `applyHeadersToAgent`.
|
|
479
|
+
getCore().setHeaders({ Authorization: "Bearer static-2" });
|
|
480
|
+
await nextTick();
|
|
481
|
+
|
|
482
|
+
expect(agentRef.headers?.Authorization).toBe("Bearer static-2");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("changing the headers prop re-dispatches the thread context exactly once (onHeadersChanged -> triggerRef)", async () => {
|
|
486
|
+
// The provider subscribes to core's `onHeadersChanged` and calls
|
|
487
|
+
// `triggerRef(copilotkit)` so Vue's reactivity actually re-reads
|
|
488
|
+
// `ɵheadersGeneration`-keyed dependents (`headersKey` in use-threads.ts,
|
|
489
|
+
// the HttpAgent watcher in use-agent.ts) when a real source change
|
|
490
|
+
// lands. Without it, nothing pokes Vue: `ɵheadersGeneration` is a plain
|
|
491
|
+
// property read on a non-reactive core instance, not a ref.
|
|
492
|
+
const calls: Call[] = [];
|
|
493
|
+
vi.stubGlobal("fetch", stubFetch(calls));
|
|
494
|
+
|
|
495
|
+
const Threads = defineComponent({
|
|
496
|
+
setup() {
|
|
497
|
+
useThreads({ agentId: "default" });
|
|
498
|
+
return () => null;
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
let core!: CopilotKitCoreVue;
|
|
503
|
+
const Probe = defineComponent({
|
|
504
|
+
setup() {
|
|
505
|
+
const { copilotkit } = useCopilotKit();
|
|
506
|
+
core = copilotkit.value;
|
|
507
|
+
return () => null;
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const wrapper = mount(CopilotKitProvider, {
|
|
512
|
+
props: {
|
|
513
|
+
runtimeUrl: "http://rt.test/api/copilotkit",
|
|
514
|
+
headers: {
|
|
515
|
+
Authorization: "Bearer static-1",
|
|
516
|
+
} as CopilotKitHeadersSource,
|
|
517
|
+
},
|
|
518
|
+
slots: { default: () => [h(Threads), h(Probe)] },
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await vi.waitFor(() => expect(core.getAgent("default")).toBeDefined());
|
|
522
|
+
const store = await vi.waitFor(() => {
|
|
523
|
+
const s = core.getThreadStore("default");
|
|
524
|
+
expect(s).toBeDefined();
|
|
525
|
+
return s!;
|
|
526
|
+
});
|
|
527
|
+
// Wait for the first (mount) context dispatch before spying, so the spy
|
|
528
|
+
// only observes dispatches caused by the prop change below.
|
|
529
|
+
await vi.waitFor(() => {
|
|
530
|
+
expect(calls.some((c) => c.url.includes("/threads?"))).toBe(true);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const setContextSpy = vi.spyOn(store, "setContext");
|
|
534
|
+
|
|
535
|
+
// A record-to-record change (the case a strict-reference `setSource`
|
|
536
|
+
// dedup can't collapse) — one real source change, one `onHeadersChanged`.
|
|
537
|
+
await wrapper.setProps({
|
|
538
|
+
headers: { Authorization: "Bearer static-2" } as CopilotKitHeadersSource,
|
|
539
|
+
});
|
|
540
|
+
await pollFor(() => setContextSpy.mock.calls.length > 0, 100);
|
|
541
|
+
|
|
542
|
+
expect(setContextSpy).toHaveBeenCalledTimes(1);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("resolveAgent does not re-run after a builder token rotation + a run + a forced copilotkit trigger", async () => {
|
|
546
|
+
// `resolveAgent`'s watch used to key one of its sources on
|
|
547
|
+
// `JSON.stringify(...headers...)`. A run resolves the builder's CURRENT
|
|
548
|
+
// token and changes that snapshot; the next time ANYTHING pokes Vue's
|
|
549
|
+
// reactivity around `copilotkit` (unrelated to headers), the stale key
|
|
550
|
+
// would differ and (combined with `registeredProxy` forcing the whole
|
|
551
|
+
// watch to skip its diff — see the comment on the watch itself)
|
|
552
|
+
// re-run `resolveAgent`, calling `core.getAgent` and force-triggering
|
|
553
|
+
// `agent` even though nothing about the bound agent itself changed.
|
|
554
|
+
const calls: Call[] = [];
|
|
555
|
+
vi.stubGlobal("fetch", stubFetch(calls));
|
|
556
|
+
|
|
557
|
+
const Child = defineComponent({
|
|
558
|
+
setup() {
|
|
559
|
+
useAgent({ agentId: "default" });
|
|
560
|
+
return () => null;
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
let core!: CopilotKitCoreVue;
|
|
565
|
+
const Probe = defineComponent({
|
|
566
|
+
setup() {
|
|
567
|
+
const { copilotkit } = useCopilotKit();
|
|
568
|
+
core = copilotkit.value;
|
|
569
|
+
return () => null;
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
mount(CopilotKitProvider, {
|
|
574
|
+
props: {
|
|
575
|
+
runtimeUrl: "http://rt.test/api/copilotkit",
|
|
576
|
+
headers: (() => ({
|
|
577
|
+
Authorization: `Bearer ${token}`,
|
|
578
|
+
})) as CopilotKitHeadersSource,
|
|
579
|
+
},
|
|
580
|
+
slots: { default: () => [h(Child), h(Probe)] },
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await vi.waitFor(() => expect(core.getAgent("default")).toBeDefined());
|
|
584
|
+
await nextTick();
|
|
585
|
+
|
|
586
|
+
token = "tok-2";
|
|
587
|
+
const agent = core.getAgent("default")!;
|
|
588
|
+
await core.runAgent({ agent }).catch(() => {});
|
|
589
|
+
|
|
590
|
+
const getAgentSpy = vi.spyOn(core, "getAgent");
|
|
591
|
+
// Force an unrelated Vue-level trigger on `copilotkit` — same technique
|
|
592
|
+
// as the thread-context test above.
|
|
593
|
+
core.setRenderToolCalls([]);
|
|
594
|
+
await pollFor(() => false, 50);
|
|
595
|
+
|
|
596
|
+
expect(getAgentSpy).toHaveBeenCalledTimes(0);
|
|
597
|
+
});
|
|
598
|
+
});
|
|
@@ -106,7 +106,12 @@ describe("CopilotKitProvider", () => {
|
|
|
106
106
|
);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
it("supports function-valued headers and
|
|
109
|
+
it("supports function-valued headers and resolves the builder's current token at send time (#1937)", async () => {
|
|
110
|
+
// The builder is evaluated when a request is actually sent, never
|
|
111
|
+
// eagerly during render/setup — so this asserts through
|
|
112
|
+
// `resolveHeaders()` (the resolving call a run/connect makes) rather
|
|
113
|
+
// than the passive `headers` snapshot, which stays empty until
|
|
114
|
+
// something has actually resolved it.
|
|
110
115
|
const authToken = ref("initial");
|
|
111
116
|
const { getCore } = mountWithProvider(() => h("div"), {
|
|
112
117
|
runtimeUrl: "/api/copilotkit",
|
|
@@ -115,7 +120,7 @@ describe("CopilotKitProvider", () => {
|
|
|
115
120
|
}),
|
|
116
121
|
});
|
|
117
122
|
|
|
118
|
-
expect(getCore().
|
|
123
|
+
expect(await getCore().resolveHeaders()).toMatchObject({
|
|
119
124
|
Authorization: "Bearer initial",
|
|
120
125
|
});
|
|
121
126
|
|
|
@@ -123,7 +128,7 @@ describe("CopilotKitProvider", () => {
|
|
|
123
128
|
await nextTick();
|
|
124
129
|
await nextTick();
|
|
125
130
|
|
|
126
|
-
expect(getCore().
|
|
131
|
+
expect(await getCore().resolveHeaders()).toMatchObject({
|
|
127
132
|
Authorization: "Bearer updated",
|
|
128
133
|
});
|
|
129
134
|
});
|