@assistant-ui/react-google-adk 0.0.20 → 0.0.22
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/AdkEventAccumulator.d.ts +1 -0
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +20 -4
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/AdkSessionAdapter.d.ts +4 -4
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +16 -3
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +18 -21
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/server/adkEventStream.d.ts.map +1 -1
- package/dist/server/adkEventStream.js +11 -1
- package/dist/server/adkEventStream.js.map +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/useAdkMessages.d.ts +2 -1
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +19 -1
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts +10 -4
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +68 -29
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +8 -5
- package/src/AdkEventAccumulator.test.ts +127 -0
- package/src/AdkEventAccumulator.ts +31 -5
- package/src/AdkSessionAdapter.test.ts +43 -0
- package/src/AdkSessionAdapter.ts +21 -4
- package/src/convertAdkMessages.test.ts +40 -6
- package/src/convertAdkMessages.ts +16 -15
- package/src/index.ts +1 -0
- package/src/server/adkEventStream.test.ts +38 -0
- package/src/server/adkEventStream.ts +16 -1
- package/src/types.ts +21 -0
- package/src/useAdkMessages.ts +24 -1
- package/src/useAdkRuntime.refetch.test.tsx +336 -0
- package/src/useAdkRuntime.test.ts +161 -0
- package/src/useAdkRuntime.ts +127 -28
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, render, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
+
import { type FC, type ReactNode } from "react";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
|
|
7
|
+
import type {
|
|
8
|
+
AssistantRuntime,
|
|
9
|
+
RemoteThreadListAdapter,
|
|
10
|
+
} from "@assistant-ui/core";
|
|
11
|
+
import { useAui } from "@assistant-ui/store";
|
|
12
|
+
import { useAdkRuntime } from "./useAdkRuntime";
|
|
13
|
+
import type { AdkMessage, AdkThreadSnapshot } from "./types";
|
|
14
|
+
|
|
15
|
+
const deferred = <T,>() => {
|
|
16
|
+
let resolve!: (value: T) => void;
|
|
17
|
+
let reject!: (reason: unknown) => void;
|
|
18
|
+
const promise = new Promise<T>((res, rej) => {
|
|
19
|
+
resolve = res;
|
|
20
|
+
reject = rej;
|
|
21
|
+
});
|
|
22
|
+
return { promise, resolve, reject };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
|
|
26
|
+
list: vi.fn(async () => ({
|
|
27
|
+
threads: [
|
|
28
|
+
{
|
|
29
|
+
status: "regular" as const,
|
|
30
|
+
remoteId: "adk-1",
|
|
31
|
+
externalId: "adk-1",
|
|
32
|
+
title: "Existing ADK session",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
})),
|
|
36
|
+
initialize: vi.fn(async () => ({
|
|
37
|
+
remoteId: "adk-1",
|
|
38
|
+
externalId: "adk-1",
|
|
39
|
+
})),
|
|
40
|
+
rename: vi.fn(async () => {}),
|
|
41
|
+
archive: vi.fn(async () => {}),
|
|
42
|
+
unarchive: vi.fn(async () => {}),
|
|
43
|
+
delete: vi.fn(async () => {}),
|
|
44
|
+
generateTitle: vi.fn(async () => new ReadableStream() as never),
|
|
45
|
+
fetch: vi.fn(async () => ({
|
|
46
|
+
status: "regular" as const,
|
|
47
|
+
remoteId: "adk-1",
|
|
48
|
+
externalId: "adk-1",
|
|
49
|
+
title: "Existing ADK session",
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const wrapperFactory = (runtime: AssistantRuntime) => {
|
|
54
|
+
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
55
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
56
|
+
{children}
|
|
57
|
+
</AssistantRuntimeProvider>
|
|
58
|
+
);
|
|
59
|
+
Wrapper.displayName = "TestWrapper";
|
|
60
|
+
return Wrapper;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const aiMessage = (id: string, text: string): AdkMessage => ({
|
|
64
|
+
id,
|
|
65
|
+
type: "ai",
|
|
66
|
+
content: [{ type: "text", text }],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const renderAdk = async (
|
|
70
|
+
load?: (
|
|
71
|
+
threadId: string,
|
|
72
|
+
options?: { signal?: AbortSignal | undefined },
|
|
73
|
+
) => Promise<AdkThreadSnapshot>,
|
|
74
|
+
) => {
|
|
75
|
+
const streamMock = vi.fn(async function* () {});
|
|
76
|
+
const capture: { runtime: AssistantRuntime | null } = { runtime: null };
|
|
77
|
+
|
|
78
|
+
// the runtime hook's binder mounts inside the provider, so the provider has
|
|
79
|
+
// to be in the same tree as the hook for a thread switch to settle
|
|
80
|
+
const Inner: FC = () => {
|
|
81
|
+
const runtime = useAdkRuntime({
|
|
82
|
+
stream: streamMock as never,
|
|
83
|
+
...(load ? { load } : {}),
|
|
84
|
+
sessionAdapter: makeThreadListAdapter(),
|
|
85
|
+
});
|
|
86
|
+
capture.runtime = runtime;
|
|
87
|
+
return (
|
|
88
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
89
|
+
{null}
|
|
90
|
+
</AssistantRuntimeProvider>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
let unmount!: () => void;
|
|
95
|
+
await act(async () => {
|
|
96
|
+
({ unmount } = render(<Inner />));
|
|
97
|
+
});
|
|
98
|
+
await waitFor(() => expect(capture.runtime).not.toBeNull());
|
|
99
|
+
|
|
100
|
+
await act(async () => {
|
|
101
|
+
await capture.runtime!.threads.switchToThread("adk-1");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return { capture, streamMock, unmount };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
describe("useAdkRuntime refetch", () => {
|
|
108
|
+
it("declares the refetch capability only when a load is supplied", async () => {
|
|
109
|
+
const withLoad = await renderAdk(async () => ({ messages: [] }));
|
|
110
|
+
expect(
|
|
111
|
+
withLoad.capture.runtime!.thread.getState().capabilities.refetchThread,
|
|
112
|
+
).toBe(true);
|
|
113
|
+
|
|
114
|
+
const withoutLoad = await renderAdk();
|
|
115
|
+
expect(
|
|
116
|
+
withoutLoad.capture.runtime!.thread.getState().capabilities.refetchThread,
|
|
117
|
+
).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("refetches in place, keeping the composer draft and the runtime", async () => {
|
|
121
|
+
let call = 0;
|
|
122
|
+
const load = vi.fn(async () => {
|
|
123
|
+
call++;
|
|
124
|
+
return {
|
|
125
|
+
messages: [aiMessage(`m-${call}`, `load ${call}`)],
|
|
126
|
+
} satisfies AdkThreadSnapshot;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const { capture } = await renderAdk(load);
|
|
130
|
+
const wrapper = wrapperFactory(capture.runtime!);
|
|
131
|
+
const { result: auiResult } = renderHook(() => useAui(), { wrapper });
|
|
132
|
+
|
|
133
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
134
|
+
await waitFor(() =>
|
|
135
|
+
expect(
|
|
136
|
+
JSON.stringify(capture.runtime!.thread.getState().messages),
|
|
137
|
+
).toContain("load 1"),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await act(async () => {
|
|
141
|
+
auiResult.current.composer.setText("draft that must survive");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await act(async () => {
|
|
145
|
+
await capture.runtime!.threads.reloadMainThread();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(load).toHaveBeenCalledTimes(2);
|
|
149
|
+
await waitFor(() =>
|
|
150
|
+
expect(
|
|
151
|
+
JSON.stringify(capture.runtime!.thread.getState().messages),
|
|
152
|
+
).toContain("load 2"),
|
|
153
|
+
);
|
|
154
|
+
expect(auiResult.current.composer.getState().text).toBe(
|
|
155
|
+
"draft that must survive",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("swaps the per-turn state over with the messages", async () => {
|
|
160
|
+
let call = 0;
|
|
161
|
+
const load = vi.fn(async () => {
|
|
162
|
+
call++;
|
|
163
|
+
return call === 1
|
|
164
|
+
? {
|
|
165
|
+
messages: [aiMessage("m-1", "first")],
|
|
166
|
+
toolConfirmations: [
|
|
167
|
+
{ toolCallId: "tc-1", toolName: "search", hint: "before" },
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
: {
|
|
171
|
+
messages: [aiMessage("m-2", "second")],
|
|
172
|
+
toolConfirmations: [
|
|
173
|
+
{ toolCallId: "tc-2", toolName: "search", hint: "after" },
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const { capture } = await renderAdk(load as never);
|
|
179
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
180
|
+
|
|
181
|
+
await act(async () => {
|
|
182
|
+
await capture.runtime!.threads.reloadMainThread();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await waitFor(() =>
|
|
186
|
+
expect(
|
|
187
|
+
JSON.stringify(capture.runtime!.thread.getState().messages),
|
|
188
|
+
).toContain("second"),
|
|
189
|
+
);
|
|
190
|
+
// the confirmation from the fresh snapshot replaced the earlier one rather
|
|
191
|
+
// than the thread passing through a cleared state
|
|
192
|
+
const extras = capture.runtime!.thread.getState().extras as {
|
|
193
|
+
toolConfirmations: { hint: string }[];
|
|
194
|
+
};
|
|
195
|
+
expect(extras.toolConfirmations).toHaveLength(1);
|
|
196
|
+
expect(extras.toolConfirmations[0]!.hint).toBe("after");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("leaves the thread untouched when the refetch fails, and rejects", async () => {
|
|
200
|
+
let call = 0;
|
|
201
|
+
const load = vi.fn(async () => {
|
|
202
|
+
call++;
|
|
203
|
+
if (call === 1) return { messages: [aiMessage("m-1", "first")] };
|
|
204
|
+
throw new Error("refetch failed");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const { capture } = await renderAdk(load as never);
|
|
208
|
+
await waitFor(() =>
|
|
209
|
+
expect(
|
|
210
|
+
JSON.stringify(capture.runtime!.thread.getState().messages),
|
|
211
|
+
).toContain("first"),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await act(async () => {
|
|
215
|
+
await expect(capture.runtime!.threads.reloadMainThread()).rejects.toThrow(
|
|
216
|
+
"refetch failed",
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(
|
|
221
|
+
JSON.stringify(capture.runtime!.thread.getState().messages),
|
|
222
|
+
).toContain("first");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("defers to an initial load still in flight rather than taking it over", async () => {
|
|
226
|
+
const pending = deferred<AdkThreadSnapshot>();
|
|
227
|
+
let call = 0;
|
|
228
|
+
const load = vi.fn(async () => {
|
|
229
|
+
call++;
|
|
230
|
+
return call === 1
|
|
231
|
+
? pending.promise
|
|
232
|
+
: { messages: [aiMessage("m-2", "second")] };
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const streamMock = vi.fn(async function* () {});
|
|
236
|
+
const capture: { runtime: AssistantRuntime | null } = { runtime: null };
|
|
237
|
+
const Inner: FC = () => {
|
|
238
|
+
const runtime = useAdkRuntime({
|
|
239
|
+
stream: streamMock as never,
|
|
240
|
+
load: load as never,
|
|
241
|
+
sessionAdapter: makeThreadListAdapter(),
|
|
242
|
+
});
|
|
243
|
+
capture.runtime = runtime;
|
|
244
|
+
return (
|
|
245
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
246
|
+
{null}
|
|
247
|
+
</AssistantRuntimeProvider>
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
await act(async () => {
|
|
251
|
+
render(<Inner />);
|
|
252
|
+
});
|
|
253
|
+
await act(async () => {
|
|
254
|
+
await capture.runtime!.threads.switchToThread("adk-1");
|
|
255
|
+
});
|
|
256
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
257
|
+
|
|
258
|
+
let settled = false;
|
|
259
|
+
act(() => {
|
|
260
|
+
capture.runtime!.threads.reloadMainThread().then(() => {
|
|
261
|
+
settled = true;
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// the refetch waits on the initial load instead of starting a second one
|
|
266
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
267
|
+
expect(settled).toBe(false);
|
|
268
|
+
|
|
269
|
+
await act(async () => {
|
|
270
|
+
pending.resolve({ messages: [aiMessage("m-1", "first")] });
|
|
271
|
+
});
|
|
272
|
+
await waitFor(() => expect(settled).toBe(true));
|
|
273
|
+
expect(
|
|
274
|
+
JSON.stringify(capture.runtime!.thread.getState().messages),
|
|
275
|
+
).toContain("first");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("does not report an aborted load as a failure", async () => {
|
|
279
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
280
|
+
const pending = deferred<AdkThreadSnapshot>();
|
|
281
|
+
let call = 0;
|
|
282
|
+
const load = vi.fn(
|
|
283
|
+
async (_id: string, options?: { signal?: AbortSignal }) => {
|
|
284
|
+
call++;
|
|
285
|
+
if (call === 1) {
|
|
286
|
+
options?.signal?.addEventListener("abort", () =>
|
|
287
|
+
pending.reject(
|
|
288
|
+
Object.assign(new Error("aborted"), { name: "AbortError" }),
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
return pending.promise;
|
|
292
|
+
}
|
|
293
|
+
return { messages: [] };
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const { unmount } = await renderAdk(load as never);
|
|
298
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
299
|
+
|
|
300
|
+
unmount();
|
|
301
|
+
await act(async () => {
|
|
302
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(warn).not.toHaveBeenCalled();
|
|
306
|
+
warn.mockRestore();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("aborts a refetch still in flight when the thread unmounts", async () => {
|
|
310
|
+
const pending = deferred<AdkThreadSnapshot>();
|
|
311
|
+
let call = 0;
|
|
312
|
+
const signals: (AbortSignal | undefined)[] = [];
|
|
313
|
+
const load = vi.fn(
|
|
314
|
+
async (_id: string, options?: { signal?: AbortSignal }) => {
|
|
315
|
+
call++;
|
|
316
|
+
signals.push(options?.signal);
|
|
317
|
+
if (call === 1) return { messages: [] };
|
|
318
|
+
return pending.promise;
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const { capture, unmount } = await renderAdk(load as never);
|
|
323
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
|
|
324
|
+
|
|
325
|
+
act(() => {
|
|
326
|
+
capture.runtime!.threads.reloadMainThread().catch(() => {});
|
|
327
|
+
});
|
|
328
|
+
await waitFor(() => expect(load).toHaveBeenCalledTimes(2));
|
|
329
|
+
expect(signals[1]?.aborted).toBe(false);
|
|
330
|
+
|
|
331
|
+
unmount();
|
|
332
|
+
|
|
333
|
+
expect(signals[1]?.aborted).toBe(true);
|
|
334
|
+
pending.resolve({ messages: [] });
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getPendingCancellations,
|
|
5
5
|
getPendingToolCalls,
|
|
6
6
|
} from "./useAdkRuntime";
|
|
7
|
+
import { convertAdkMessage } from "./convertAdkMessages";
|
|
7
8
|
import type { AppendMessage } from "@assistant-ui/core";
|
|
8
9
|
import type { AdkMessage } from "./types";
|
|
9
10
|
|
|
@@ -209,6 +210,89 @@ describe("getMessageContent", () => {
|
|
|
209
210
|
]);
|
|
210
211
|
});
|
|
211
212
|
|
|
213
|
+
it("emits a file_url part for file parts with sourceType url", () => {
|
|
214
|
+
const result = getMessageContent(
|
|
215
|
+
makeAppendMessage([
|
|
216
|
+
{
|
|
217
|
+
type: "file",
|
|
218
|
+
mimeType: "application/pdf",
|
|
219
|
+
data: "gs://bucket/report.pdf",
|
|
220
|
+
filename: "report.pdf",
|
|
221
|
+
sourceType: "url",
|
|
222
|
+
},
|
|
223
|
+
]),
|
|
224
|
+
);
|
|
225
|
+
expect(result).toEqual([
|
|
226
|
+
{
|
|
227
|
+
type: "file_url",
|
|
228
|
+
url: "gs://bucket/report.pdf",
|
|
229
|
+
mimeType: "application/pdf",
|
|
230
|
+
},
|
|
231
|
+
]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("keeps file parts inline without sourceType", () => {
|
|
235
|
+
const result = getMessageContent(
|
|
236
|
+
makeAppendMessage([
|
|
237
|
+
{
|
|
238
|
+
type: "file",
|
|
239
|
+
mimeType: "application/pdf",
|
|
240
|
+
data: "gs://bucket/report.pdf",
|
|
241
|
+
},
|
|
242
|
+
]),
|
|
243
|
+
);
|
|
244
|
+
expect(result).toEqual([
|
|
245
|
+
{
|
|
246
|
+
type: "file",
|
|
247
|
+
mimeType: "application/pdf",
|
|
248
|
+
data: "gs://bucket/report.pdf",
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("round-trips a file_url part through convert and edit-resend", () => {
|
|
254
|
+
const converted = convertAdkMessage(
|
|
255
|
+
{
|
|
256
|
+
id: "m1",
|
|
257
|
+
type: "human",
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: "file_url",
|
|
261
|
+
url: "gs://bucket/report.pdf",
|
|
262
|
+
mimeType: "application/pdf",
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{},
|
|
267
|
+
);
|
|
268
|
+
const content = (converted as { content: AppendMessage["content"] })
|
|
269
|
+
.content;
|
|
270
|
+
const result = getMessageContent(makeAppendMessage(content));
|
|
271
|
+
expect(result).toEqual([
|
|
272
|
+
{
|
|
273
|
+
type: "file_url",
|
|
274
|
+
url: "gs://bucket/report.pdf",
|
|
275
|
+
mimeType: "application/pdf",
|
|
276
|
+
},
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("ignores sourceType id on file parts", () => {
|
|
281
|
+
const result = getMessageContent(
|
|
282
|
+
makeAppendMessage([
|
|
283
|
+
{
|
|
284
|
+
type: "file",
|
|
285
|
+
mimeType: "application/pdf",
|
|
286
|
+
data: "file-abc123",
|
|
287
|
+
sourceType: "id",
|
|
288
|
+
},
|
|
289
|
+
]),
|
|
290
|
+
);
|
|
291
|
+
expect(result).toEqual([
|
|
292
|
+
{ type: "file", mimeType: "application/pdf", data: "file-abc123" },
|
|
293
|
+
]);
|
|
294
|
+
});
|
|
295
|
+
|
|
212
296
|
it("forwards an audio part as a file block with the format-derived mime type", () => {
|
|
213
297
|
const result = getMessageContent(
|
|
214
298
|
makeAppendMessage([
|
|
@@ -245,6 +329,83 @@ describe("getMessageContent", () => {
|
|
|
245
329
|
]);
|
|
246
330
|
});
|
|
247
331
|
|
|
332
|
+
it("strips a data URL envelope from file data", () => {
|
|
333
|
+
const result = getMessageContent(
|
|
334
|
+
makeAppendMessage([
|
|
335
|
+
{
|
|
336
|
+
type: "file",
|
|
337
|
+
data: "data:application/pdf;base64,QUJD",
|
|
338
|
+
mimeType: "application/pdf",
|
|
339
|
+
filename: "a.pdf",
|
|
340
|
+
},
|
|
341
|
+
]),
|
|
342
|
+
);
|
|
343
|
+
expect(result).toEqual([
|
|
344
|
+
{
|
|
345
|
+
type: "file",
|
|
346
|
+
mimeType: "application/pdf",
|
|
347
|
+
data: "QUJD",
|
|
348
|
+
filename: "a.pdf",
|
|
349
|
+
},
|
|
350
|
+
]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("emits a file_url part for an unmarked http source", () => {
|
|
354
|
+
const result = getMessageContent(
|
|
355
|
+
makeAppendMessage([
|
|
356
|
+
{
|
|
357
|
+
type: "file",
|
|
358
|
+
data: "https://cdn.example.com/a.pdf",
|
|
359
|
+
mimeType: "application/pdf",
|
|
360
|
+
},
|
|
361
|
+
]),
|
|
362
|
+
);
|
|
363
|
+
expect(result).toEqual([
|
|
364
|
+
{
|
|
365
|
+
type: "file_url",
|
|
366
|
+
url: "https://cdn.example.com/a.pdf",
|
|
367
|
+
mimeType: "application/pdf",
|
|
368
|
+
},
|
|
369
|
+
]);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("leaves bare base64 file data untouched", () => {
|
|
373
|
+
const result = getMessageContent(
|
|
374
|
+
makeAppendMessage([
|
|
375
|
+
{ type: "file", data: "QUJD", mimeType: "application/pdf" },
|
|
376
|
+
]),
|
|
377
|
+
);
|
|
378
|
+
expect(result).toEqual([
|
|
379
|
+
{ type: "file", mimeType: "application/pdf", data: "QUJD" },
|
|
380
|
+
]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("round-trips an audio file part through both converters", () => {
|
|
384
|
+
const outbound = getMessageContent(
|
|
385
|
+
makeAppendMessage([
|
|
386
|
+
{
|
|
387
|
+
type: "file",
|
|
388
|
+
data: "data:audio/mp3;base64,QUJD",
|
|
389
|
+
mimeType: "audio/mp3",
|
|
390
|
+
},
|
|
391
|
+
]),
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(outbound).toEqual([
|
|
395
|
+
{ type: "file", mimeType: "audio/mp3", data: "QUJD" },
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
const inbound = convertAdkMessage(
|
|
399
|
+
{ id: "m1", type: "human", content: outbound } as never,
|
|
400
|
+
{},
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(inbound).toMatchObject({
|
|
404
|
+
role: "user",
|
|
405
|
+
content: [{ type: "file", data: "QUJD", mimeType: "audio/mp3" }],
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
248
409
|
it("skips data parts while keeping surrounding text", () => {
|
|
249
410
|
const result = getMessageContent(
|
|
250
411
|
makeAppendMessage([
|