@apollo/client-ai-apps 0.5.2 → 0.5.3

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/core/ApolloClient.d.ts +1 -1
  3. package/dist/core/ApolloClient.d.ts.map +1 -1
  4. package/dist/core/ApolloClient.js +1 -1
  5. package/dist/core/ApolloClient.js.map +1 -1
  6. package/dist/mcp/core/ApolloClient.d.ts +6 -1
  7. package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
  8. package/dist/mcp/core/ApolloClient.js +36 -3
  9. package/dist/mcp/core/ApolloClient.js.map +1 -1
  10. package/dist/mcp/core/McpAppManager.d.ts +2 -2
  11. package/dist/mcp/core/McpAppManager.d.ts.map +1 -1
  12. package/dist/mcp/core/McpAppManager.js +3 -3
  13. package/dist/mcp/core/McpAppManager.js.map +1 -1
  14. package/dist/mcp/react/hooks/createHydrationUtils.d.ts +15 -0
  15. package/dist/mcp/react/hooks/createHydrationUtils.d.ts.map +1 -0
  16. package/dist/mcp/react/hooks/createHydrationUtils.js +113 -0
  17. package/dist/mcp/react/hooks/createHydrationUtils.js.map +1 -0
  18. package/dist/mcp/react/index.d.ts +1 -0
  19. package/dist/mcp/react/index.d.ts.map +1 -1
  20. package/dist/mcp/react/index.js +1 -0
  21. package/dist/mcp/react/index.js.map +1 -1
  22. package/dist/openai/core/ApolloClient.d.ts +6 -1
  23. package/dist/openai/core/ApolloClient.d.ts.map +1 -1
  24. package/dist/openai/core/ApolloClient.js +37 -3
  25. package/dist/openai/core/ApolloClient.js.map +1 -1
  26. package/dist/openai/core/McpAppManager.d.ts +2 -2
  27. package/dist/openai/core/McpAppManager.d.ts.map +1 -1
  28. package/dist/openai/core/McpAppManager.js +5 -5
  29. package/dist/openai/core/McpAppManager.js.map +1 -1
  30. package/dist/openai/react/hooks/createHydrationUtils.d.ts +15 -0
  31. package/dist/openai/react/hooks/createHydrationUtils.d.ts.map +1 -0
  32. package/dist/openai/react/hooks/createHydrationUtils.js +113 -0
  33. package/dist/openai/react/hooks/createHydrationUtils.js.map +1 -0
  34. package/dist/openai/react/index.d.ts +1 -0
  35. package/dist/openai/react/index.d.ts.map +1 -1
  36. package/dist/openai/react/index.js +1 -0
  37. package/dist/openai/react/index.js.map +1 -1
  38. package/dist/react/ApolloProvider.js +1 -1
  39. package/dist/react/ApolloProvider.js.map +1 -1
  40. package/dist/react/index.d.ts +4 -0
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +3 -0
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react/index.mcp.d.ts +1 -1
  45. package/dist/react/index.mcp.d.ts.map +1 -1
  46. package/dist/react/index.mcp.js +1 -1
  47. package/dist/react/index.mcp.js.map +1 -1
  48. package/dist/react/index.openai.d.ts +1 -1
  49. package/dist/react/index.openai.d.ts.map +1 -1
  50. package/dist/react/index.openai.js +1 -1
  51. package/dist/react/index.openai.js.map +1 -1
  52. package/dist/react/reactive.d.ts +9 -0
  53. package/dist/react/reactive.d.ts.map +1 -0
  54. package/dist/react/reactive.js +11 -0
  55. package/dist/react/reactive.js.map +1 -0
  56. package/dist/utilities/getToolNamesFromDocument.d.ts +3 -0
  57. package/dist/utilities/getToolNamesFromDocument.d.ts.map +1 -0
  58. package/dist/utilities/getToolNamesFromDocument.js +12 -0
  59. package/dist/utilities/getToolNamesFromDocument.js.map +1 -0
  60. package/dist/utilities/getVariableNamesFromDocument.d.ts +3 -0
  61. package/dist/utilities/getVariableNamesFromDocument.d.ts.map +1 -0
  62. package/dist/utilities/getVariableNamesFromDocument.js +6 -0
  63. package/dist/utilities/getVariableNamesFromDocument.js.map +1 -0
  64. package/dist/utilities/index.d.ts +3 -0
  65. package/dist/utilities/index.d.ts.map +1 -1
  66. package/dist/utilities/index.js +3 -0
  67. package/dist/utilities/index.js.map +1 -1
  68. package/dist/utilities/warnOnVariableMismatch.d.ts +3 -0
  69. package/dist/utilities/warnOnVariableMismatch.d.ts.map +1 -0
  70. package/dist/utilities/warnOnVariableMismatch.js +10 -0
  71. package/dist/utilities/warnOnVariableMismatch.js.map +1 -0
  72. package/package.json +2 -1
  73. package/src/core/ApolloClient.ts +1 -1
  74. package/src/mcp/core/ApolloClient.ts +67 -2
  75. package/src/mcp/core/McpAppManager.ts +3 -3
  76. package/src/mcp/core/__tests__/ApolloClient.test.ts +109 -6
  77. package/src/mcp/link/__tests__/ToolCallLink.test.ts +1 -1
  78. package/src/mcp/react/hooks/__tests__/createHydrationUtils.test.tsx +1228 -0
  79. package/src/mcp/react/hooks/createHydrationUtils.ts +182 -0
  80. package/src/mcp/react/index.ts +1 -0
  81. package/src/openai/core/ApolloClient.ts +68 -2
  82. package/src/openai/core/McpAppManager.ts +5 -5
  83. package/src/openai/core/__tests__/ApolloClient.test.ts +113 -7
  84. package/src/openai/react/hooks/__tests__/createHydrationUtils.test.tsx +1333 -0
  85. package/src/openai/react/hooks/createHydrationUtils.ts +182 -0
  86. package/src/openai/react/index.ts +1 -0
  87. package/src/react/ApolloProvider.tsx +1 -1
  88. package/src/react/index.mcp.ts +1 -0
  89. package/src/react/index.openai.ts +1 -0
  90. package/src/react/index.ts +7 -0
  91. package/src/react/reactive.ts +19 -0
  92. package/src/testing/internal/mcp/graphqlToolResult.ts +5 -5
  93. package/src/utilities/getToolNamesFromDocument.ts +15 -0
  94. package/src/utilities/getVariableNamesFromDocument.ts +9 -0
  95. package/src/utilities/index.ts +3 -0
  96. package/src/utilities/warnOnVariableMismatch.ts +20 -0
@@ -0,0 +1,1333 @@
1
+ import { afterEach, describe, test, expect, vi } from "vitest";
2
+ import {
3
+ createRenderStream,
4
+ disableActEnvironment,
5
+ renderHookToSnapshotStream,
6
+ } from "@testing-library/react-render-stream";
7
+ import { InMemoryCache, gql, type TypedDocumentNode } from "@apollo/client";
8
+ import { createHydrationUtils } from "../createHydrationUtils.js";
9
+ import { reactive } from "../../../../react/reactive.js";
10
+ import { ApolloClient } from "../../../core/ApolloClient.js";
11
+ import {
12
+ graphqlToolResult,
13
+ minimalHostContextWithToolName,
14
+ mockApplicationManifest,
15
+ mockMcpHost,
16
+ spyOnConsole,
17
+ stubOpenAiGlobals,
18
+ } from "../../../../testing/internal/index.js";
19
+ import { ApolloProvider } from "../../../../react/ApolloProvider.js";
20
+ import { StrictMode } from "react";
21
+ import assert from "node:assert";
22
+
23
+ afterEach(() => {
24
+ vi.unstubAllGlobals();
25
+ });
26
+
27
+ const PRODUCTS_QUERY: TypedDocumentNode<
28
+ { products: Array<{ __typename: "Product"; id: string }> },
29
+ { category: string; page: number; sortBy: string }
30
+ > = gql`
31
+ query Products($category: String!, $page: Int!, $sortBy: String!)
32
+ @tool(name: "GetProductsByCategory") {
33
+ products(category: $category, page: $page, sortBy: $sortBy) {
34
+ id
35
+ }
36
+ }
37
+ `;
38
+
39
+ test("returns tool input value when tool matches", async () => {
40
+ using _ = spyOnConsole("debug");
41
+ stubOpenAiGlobals({
42
+ toolResponseMetadata: {},
43
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
44
+ });
45
+ const client = new ApolloClient({
46
+ cache: new InMemoryCache(),
47
+ manifest: mockApplicationManifest(),
48
+ });
49
+
50
+ using host = await mockMcpHost({
51
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
52
+ });
53
+ host.onCleanup(() => client.stop());
54
+
55
+ host.sendToolInput({
56
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
57
+ });
58
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
59
+
60
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
61
+
62
+ using _disabledAct = disableActEnvironment();
63
+ const { takeSnapshot } = await renderHookToSnapshotStream(
64
+ () =>
65
+ useHydratedVariables({
66
+ category: "music",
67
+ page: 1,
68
+ sortBy: "name",
69
+ }),
70
+ {
71
+ wrapper: ({ children }) => (
72
+ <ApolloProvider client={client}>{children}</ApolloProvider>
73
+ ),
74
+ }
75
+ );
76
+
77
+ const [variables] = await takeSnapshot();
78
+ expect(variables).toStrictEqual({
79
+ category: "electronics",
80
+ page: 1,
81
+ sortBy: "title",
82
+ });
83
+
84
+ await expect(takeSnapshot).not.toRerender();
85
+ });
86
+
87
+ test("returns user-provided variables when tool name does not match", async () => {
88
+ using _ = spyOnConsole("debug");
89
+ stubOpenAiGlobals({
90
+ toolResponseMetadata: {},
91
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
92
+ });
93
+ const client = new ApolloClient({
94
+ cache: new InMemoryCache(),
95
+ manifest: mockApplicationManifest({
96
+ operations: [
97
+ {
98
+ ...mockApplicationManifest().operations[0],
99
+ tools: [{ name: "OtherTool", description: "A different tool" }],
100
+ },
101
+ ],
102
+ }),
103
+ });
104
+
105
+ using host = await mockMcpHost({
106
+ hostContext: minimalHostContextWithToolName("OtherTool"),
107
+ });
108
+ host.onCleanup(() => client.stop());
109
+
110
+ host.sendToolInput({
111
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
112
+ });
113
+ host.sendToolResult(graphqlToolResult({ data: { product: null } }));
114
+
115
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
116
+
117
+ using _disabledAct = disableActEnvironment();
118
+ const { takeSnapshot } = await renderHookToSnapshotStream(
119
+ () =>
120
+ useHydratedVariables({
121
+ category: "music",
122
+ page: 1,
123
+ sortBy: "name",
124
+ }),
125
+ {
126
+ wrapper: ({ children }) => (
127
+ <ApolloProvider client={client}>{children}</ApolloProvider>
128
+ ),
129
+ }
130
+ );
131
+
132
+ const [variables] = await takeSnapshot();
133
+ expect(variables).toStrictEqual({
134
+ category: "music",
135
+ page: 1,
136
+ sortBy: "name",
137
+ });
138
+
139
+ await expect(takeSnapshot).not.toRerender();
140
+ });
141
+
142
+ test("filters tool input not in document's variable definitions when tool matches", async () => {
143
+ using _ = spyOnConsole("debug");
144
+ stubOpenAiGlobals({
145
+ toolResponseMetadata: {},
146
+ toolInput: { id: "1" },
147
+ });
148
+
149
+ const query: TypedDocumentNode<unknown, { id: string }> = gql`
150
+ query GetProduct($id: ID!) @tool(name: "GetProduct") {
151
+ product(id: $id) {
152
+ id
153
+ }
154
+ }
155
+ `;
156
+ const { useHydratedVariables } = createHydrationUtils(query);
157
+
158
+ const client = new ApolloClient({
159
+ cache: new InMemoryCache(),
160
+ manifest: mockApplicationManifest(),
161
+ });
162
+
163
+ using host = await mockMcpHost({
164
+ hostContext: minimalHostContextWithToolName("GetProduct"),
165
+ });
166
+ host.onCleanup(() => client.stop());
167
+
168
+ host.sendToolInput({ arguments: { id: "1" } });
169
+ host.sendToolResult(graphqlToolResult({ data: { product: null } }));
170
+
171
+ using _disabledAct = disableActEnvironment();
172
+ const { takeSnapshot } = await renderHookToSnapshotStream(
173
+ () => useHydratedVariables({ id: "default" }),
174
+ {
175
+ wrapper: ({ children }) => (
176
+ <ApolloProvider client={client}>{children}</ApolloProvider>
177
+ ),
178
+ }
179
+ );
180
+
181
+ const [variables] = await takeSnapshot();
182
+ expect(variables).toStrictEqual({ id: "1" });
183
+
184
+ await expect(takeSnapshot).not.toRerender();
185
+ });
186
+
187
+ test("does not add user-provided variables not in document's variable definitions when tool matches", async () => {
188
+ using _ = spyOnConsole("debug");
189
+ stubOpenAiGlobals({
190
+ toolResponseMetadata: {},
191
+ toolInput: { id: "1" },
192
+ });
193
+
194
+ const query: TypedDocumentNode<unknown, { id: string }> = gql`
195
+ query GetProduct($id: ID!) @tool(name: "GetProduct") {
196
+ product(id: $id) {
197
+ id
198
+ }
199
+ }
200
+ `;
201
+ const { useHydratedVariables } = createHydrationUtils(query);
202
+
203
+ const client = new ApolloClient({
204
+ cache: new InMemoryCache(),
205
+ manifest: mockApplicationManifest(),
206
+ });
207
+
208
+ using host = await mockMcpHost({
209
+ hostContext: minimalHostContextWithToolName("GetProduct"),
210
+ });
211
+ host.onCleanup(() => client.stop());
212
+
213
+ host.sendToolInput({ arguments: { id: "1" } });
214
+ host.sendToolResult(graphqlToolResult({ data: { product: null } }));
215
+
216
+ using _disabledAct = disableActEnvironment();
217
+ const { takeSnapshot } = await renderHookToSnapshotStream(
218
+ () => useHydratedVariables({ id: "default", notInDoc: true } as any),
219
+ {
220
+ wrapper: ({ children }) => (
221
+ <ApolloProvider client={client}>{children}</ApolloProvider>
222
+ ),
223
+ }
224
+ );
225
+
226
+ const [variables] = await takeSnapshot();
227
+ expect(variables).toStrictEqual({ id: "1" });
228
+
229
+ await expect(takeSnapshot).not.toRerender();
230
+ });
231
+
232
+ test("filters user-provided variables not in document's variable definitions when tool does not match", async () => {
233
+ using _ = spyOnConsole("debug");
234
+ stubOpenAiGlobals({
235
+ toolResponseMetadata: {},
236
+ toolInput: { id: "1" },
237
+ });
238
+
239
+ const query: TypedDocumentNode<unknown, { id: string }> = gql`
240
+ query GetProduct($id: ID!) @tool(name: "GetProduct") {
241
+ product(id: $id) {
242
+ id
243
+ }
244
+ }
245
+ `;
246
+ const { useHydratedVariables } = createHydrationUtils(query);
247
+
248
+ const client = new ApolloClient({
249
+ cache: new InMemoryCache(),
250
+ manifest: mockApplicationManifest(),
251
+ });
252
+
253
+ using host = await mockMcpHost({
254
+ hostContext: minimalHostContextWithToolName("OtherTool"),
255
+ });
256
+ host.onCleanup(() => client.stop());
257
+
258
+ host.sendToolInput({ arguments: { id: "1" } });
259
+ host.sendToolResult(graphqlToolResult({ data: { product: null } }));
260
+
261
+ using _disabledAct = disableActEnvironment();
262
+ const { takeSnapshot } = await renderHookToSnapshotStream(
263
+ () => useHydratedVariables({ id: "default", notInDoc: true } as any),
264
+ {
265
+ wrapper: ({ children }) => (
266
+ <ApolloProvider client={client}>{children}</ApolloProvider>
267
+ ),
268
+ }
269
+ );
270
+
271
+ const [variables] = await takeSnapshot();
272
+ expect(variables).toStrictEqual({ id: "default" });
273
+
274
+ await expect(takeSnapshot).not.toRerender();
275
+ });
276
+
277
+ test("setVariables shallow-merges a partial update", async () => {
278
+ using _ = spyOnConsole("debug");
279
+ stubOpenAiGlobals({
280
+ toolResponseMetadata: {},
281
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
282
+ });
283
+ const client = new ApolloClient({
284
+ cache: new InMemoryCache(),
285
+ manifest: mockApplicationManifest(),
286
+ });
287
+
288
+ using host = await mockMcpHost({
289
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
290
+ });
291
+ host.onCleanup(() => client.stop());
292
+
293
+ host.sendToolInput({
294
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
295
+ });
296
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
297
+
298
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
299
+
300
+ using _disabledAct = disableActEnvironment();
301
+ const { takeSnapshot } = await renderHookToSnapshotStream(
302
+ () =>
303
+ useHydratedVariables({
304
+ category: "music",
305
+ page: 1,
306
+ sortBy: "name",
307
+ }),
308
+ {
309
+ wrapper: ({ children }) => (
310
+ <ApolloProvider client={client}>{children}</ApolloProvider>
311
+ ),
312
+ }
313
+ );
314
+
315
+ const [initialVariables, setVariables] = await takeSnapshot();
316
+ expect(initialVariables).toStrictEqual({
317
+ category: "electronics",
318
+ page: 1,
319
+ sortBy: "title",
320
+ });
321
+
322
+ setVariables({ page: 2 });
323
+
324
+ const [updatedVariables] = await takeSnapshot();
325
+ expect(updatedVariables).toStrictEqual({
326
+ category: "electronics",
327
+ page: 2,
328
+ sortBy: "title",
329
+ });
330
+
331
+ await expect(takeSnapshot).not.toRerender();
332
+ });
333
+
334
+ test("setVariables supports updater function", async () => {
335
+ using _ = spyOnConsole("debug");
336
+ stubOpenAiGlobals({
337
+ toolResponseMetadata: {},
338
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
339
+ });
340
+ const client = new ApolloClient({
341
+ cache: new InMemoryCache(),
342
+ manifest: mockApplicationManifest(),
343
+ });
344
+
345
+ using host = await mockMcpHost({
346
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
347
+ });
348
+ host.onCleanup(() => client.stop());
349
+
350
+ host.sendToolInput({
351
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
352
+ });
353
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
354
+
355
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
356
+
357
+ using _disabledAct = disableActEnvironment();
358
+ const { takeSnapshot } = await renderHookToSnapshotStream(
359
+ () =>
360
+ useHydratedVariables({
361
+ category: "music",
362
+ page: 1,
363
+ sortBy: "name",
364
+ }),
365
+ {
366
+ wrapper: ({ children }) => (
367
+ <ApolloProvider client={client}>{children}</ApolloProvider>
368
+ ),
369
+ }
370
+ );
371
+
372
+ const [initialVariables, setVariables] = await takeSnapshot();
373
+ expect(initialVariables).toStrictEqual({
374
+ category: "electronics",
375
+ page: 1,
376
+ sortBy: "title",
377
+ });
378
+
379
+ setVariables((prev) => ({ page: prev.page + 1 }));
380
+
381
+ const [updatedVariables] = await takeSnapshot();
382
+ expect(updatedVariables).toStrictEqual({
383
+ category: "electronics",
384
+ page: 2,
385
+ sortBy: "title",
386
+ });
387
+
388
+ await expect(takeSnapshot).not.toRerender();
389
+ });
390
+
391
+ test("setVariables ignores reactive variable keys (object form)", async () => {
392
+ using _ = spyOnConsole("debug", "warn");
393
+ stubOpenAiGlobals({
394
+ toolResponseMetadata: {},
395
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
396
+ });
397
+ const client = new ApolloClient({
398
+ cache: new InMemoryCache(),
399
+ manifest: mockApplicationManifest(),
400
+ });
401
+
402
+ using host = await mockMcpHost({
403
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
404
+ });
405
+ host.onCleanup(() => client.stop());
406
+
407
+ host.sendToolInput({
408
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
409
+ });
410
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
411
+
412
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
413
+
414
+ using _disabledAct = disableActEnvironment();
415
+ const { takeSnapshot } = await renderHookToSnapshotStream(
416
+ () =>
417
+ useHydratedVariables({
418
+ category: reactive("music"),
419
+ page: 1,
420
+ sortBy: "name",
421
+ }),
422
+ {
423
+ wrapper: ({ children }) => (
424
+ <ApolloProvider client={client}>{children}</ApolloProvider>
425
+ ),
426
+ }
427
+ );
428
+
429
+ const [initialVariables, setVariables] = await takeSnapshot();
430
+ expect(initialVariables).toStrictEqual({
431
+ category: "electronics",
432
+ page: 1,
433
+ sortBy: "title",
434
+ });
435
+
436
+ // @ts-expect-error category is reactive
437
+ setVariables({ category: "sports" });
438
+
439
+ expect(console.warn).toHaveBeenCalledWith(
440
+ expect.stringContaining('Attempted to set reactive variable "category"')
441
+ );
442
+ await expect(takeSnapshot).not.toRerender();
443
+ });
444
+
445
+ test("setVariables ignores reactive variable keys (callback form)", async () => {
446
+ using _ = spyOnConsole("debug", "warn");
447
+ stubOpenAiGlobals({
448
+ toolResponseMetadata: {},
449
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
450
+ });
451
+ const client = new ApolloClient({
452
+ cache: new InMemoryCache(),
453
+ manifest: mockApplicationManifest(),
454
+ });
455
+
456
+ using host = await mockMcpHost({
457
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
458
+ });
459
+ host.onCleanup(() => client.stop());
460
+
461
+ host.sendToolInput({
462
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
463
+ });
464
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
465
+
466
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
467
+
468
+ using _disabledAct = disableActEnvironment();
469
+ const { takeSnapshot } = await renderHookToSnapshotStream(
470
+ () =>
471
+ useHydratedVariables({
472
+ category: reactive("music"),
473
+ page: 1,
474
+ sortBy: "name",
475
+ }),
476
+ {
477
+ wrapper: ({ children }) => (
478
+ <ApolloProvider client={client}>{children}</ApolloProvider>
479
+ ),
480
+ }
481
+ );
482
+
483
+ using _warn = spyOnConsole("warn");
484
+
485
+ const [initialVariables, setVariables] = await takeSnapshot();
486
+ expect(initialVariables).toStrictEqual({
487
+ category: "electronics",
488
+ page: 1,
489
+ sortBy: "title",
490
+ });
491
+
492
+ // @ts-expect-error category is reactive
493
+ setVariables(() => ({ category: "sports" }));
494
+
495
+ expect(console.warn).toHaveBeenCalledWith(
496
+ expect.stringContaining('Attempted to set reactive variable "category"')
497
+ );
498
+ await expect(takeSnapshot).not.toRerender();
499
+ });
500
+
501
+ test("state variable is not reset when component re-renders with new input", async () => {
502
+ using _ = spyOnConsole("debug");
503
+ stubOpenAiGlobals({
504
+ toolResponseMetadata: {},
505
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
506
+ });
507
+ const client = new ApolloClient({
508
+ cache: new InMemoryCache(),
509
+ manifest: mockApplicationManifest(),
510
+ });
511
+
512
+ using host = await mockMcpHost({
513
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
514
+ });
515
+ host.onCleanup(() => client.stop());
516
+
517
+ host.sendToolInput({
518
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
519
+ });
520
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
521
+
522
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
523
+
524
+ using _disabledAct = disableActEnvironment();
525
+ const { takeSnapshot, getCurrentSnapshot, rerender } =
526
+ await renderHookToSnapshotStream(
527
+ ({ sortBy }) =>
528
+ useHydratedVariables({
529
+ category: "music",
530
+ page: 1,
531
+ sortBy,
532
+ }),
533
+ {
534
+ initialProps: { sortBy: "name" },
535
+ wrapper: ({ children }) => (
536
+ <ApolloProvider client={client}>{children}</ApolloProvider>
537
+ ),
538
+ }
539
+ );
540
+
541
+ {
542
+ const [variables] = await takeSnapshot();
543
+
544
+ expect(variables).toStrictEqual({
545
+ category: "electronics",
546
+ page: 1,
547
+ sortBy: "title",
548
+ });
549
+ }
550
+
551
+ const [, setVariables] = getCurrentSnapshot();
552
+ setVariables({ sortBy: "price" });
553
+
554
+ {
555
+ const [variables] = await takeSnapshot();
556
+
557
+ expect(variables).toStrictEqual({
558
+ category: "electronics",
559
+ page: 1,
560
+ sortBy: "price",
561
+ });
562
+ }
563
+
564
+ rerender({ sortBy: "ignored" });
565
+
566
+ {
567
+ const [variables] = await takeSnapshot();
568
+
569
+ expect(variables).toStrictEqual({
570
+ category: "electronics",
571
+ page: 1,
572
+ sortBy: "price",
573
+ });
574
+ }
575
+
576
+ await expect(takeSnapshot).not.toRerender();
577
+ });
578
+
579
+ test("returns tool input value for reactive variable when tool matches", async () => {
580
+ using _ = spyOnConsole("debug");
581
+ stubOpenAiGlobals({
582
+ toolResponseMetadata: {},
583
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
584
+ });
585
+ const client = new ApolloClient({
586
+ cache: new InMemoryCache(),
587
+ manifest: mockApplicationManifest(),
588
+ });
589
+
590
+ using host = await mockMcpHost({
591
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
592
+ });
593
+ host.onCleanup(() => client.stop());
594
+
595
+ host.sendToolInput({
596
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
597
+ });
598
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
599
+
600
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
601
+
602
+ using _disabledAct = disableActEnvironment();
603
+ const { takeSnapshot } = await renderHookToSnapshotStream(
604
+ () =>
605
+ useHydratedVariables({
606
+ category: reactive("music"),
607
+ page: 1,
608
+ sortBy: "name",
609
+ }),
610
+ {
611
+ wrapper: ({ children }) => (
612
+ <ApolloProvider client={client}>{children}</ApolloProvider>
613
+ ),
614
+ }
615
+ );
616
+
617
+ const [variables] = await takeSnapshot();
618
+ expect(variables).toStrictEqual({
619
+ category: "electronics",
620
+ page: 1,
621
+ sortBy: "title",
622
+ });
623
+
624
+ await expect(takeSnapshot).not.toRerender();
625
+ });
626
+
627
+ test("reactive variable follows the provided value after it updates", async () => {
628
+ using _ = spyOnConsole("debug");
629
+ stubOpenAiGlobals({
630
+ toolResponseMetadata: {},
631
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
632
+ });
633
+ const client = new ApolloClient({
634
+ cache: new InMemoryCache(),
635
+ manifest: mockApplicationManifest(),
636
+ });
637
+
638
+ using host = await mockMcpHost({
639
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
640
+ });
641
+ host.onCleanup(() => client.stop());
642
+
643
+ host.sendToolInput({
644
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
645
+ });
646
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
647
+
648
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
649
+
650
+ using _disabledAct = disableActEnvironment();
651
+
652
+ const { takeSnapshot, rerender } = await renderHookToSnapshotStream(
653
+ ({ category }) =>
654
+ useHydratedVariables({
655
+ category: reactive(category),
656
+ page: 1,
657
+ sortBy: "name",
658
+ }),
659
+ {
660
+ initialProps: { category: "default" },
661
+ wrapper: ({ children }) => (
662
+ <ApolloProvider client={client}>{children}</ApolloProvider>
663
+ ),
664
+ }
665
+ );
666
+
667
+ {
668
+ const [variables] = await takeSnapshot();
669
+
670
+ expect(variables).toStrictEqual({
671
+ category: "electronics",
672
+ page: 1,
673
+ sortBy: "title",
674
+ });
675
+ }
676
+
677
+ rerender({ category: "music" });
678
+
679
+ {
680
+ const [variables] = await takeSnapshot();
681
+
682
+ expect(variables).toStrictEqual({
683
+ category: "music",
684
+ page: 1,
685
+ sortBy: "title",
686
+ });
687
+ }
688
+
689
+ rerender({ category: "sports" });
690
+
691
+ {
692
+ const [variables] = await takeSnapshot();
693
+
694
+ expect(variables).toStrictEqual({
695
+ category: "sports",
696
+ page: 1,
697
+ sortBy: "title",
698
+ });
699
+ }
700
+
701
+ rerender({ category: "default" });
702
+
703
+ {
704
+ const [variables] = await takeSnapshot();
705
+
706
+ expect(variables).toStrictEqual({
707
+ category: "default",
708
+ page: 1,
709
+ sortBy: "title",
710
+ });
711
+ }
712
+
713
+ await expect(takeSnapshot).not.toRerender();
714
+ });
715
+
716
+ test("reactive variable returns provided value when tool name does not match", async () => {
717
+ using _ = spyOnConsole("debug");
718
+ stubOpenAiGlobals({
719
+ toolResponseMetadata: {},
720
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
721
+ });
722
+ const client = new ApolloClient({
723
+ cache: new InMemoryCache(),
724
+ manifest: mockApplicationManifest({
725
+ operations: [
726
+ {
727
+ ...mockApplicationManifest().operations[0],
728
+ tools: [{ name: "OtherTool", description: "A different tool" }],
729
+ },
730
+ ],
731
+ }),
732
+ });
733
+
734
+ using host = await mockMcpHost({
735
+ hostContext: minimalHostContextWithToolName("OtherTool"),
736
+ });
737
+ host.onCleanup(() => client.stop());
738
+
739
+ host.sendToolInput({
740
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
741
+ });
742
+ host.sendToolResult(graphqlToolResult({ data: { product: null } }));
743
+
744
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
745
+
746
+ using _disabledAct = disableActEnvironment();
747
+ const { takeSnapshot } = await renderHookToSnapshotStream(
748
+ () =>
749
+ useHydratedVariables({
750
+ category: reactive("music"),
751
+ page: 1,
752
+ sortBy: "name",
753
+ }),
754
+ {
755
+ wrapper: ({ children }) => (
756
+ <ApolloProvider client={client}>{children}</ApolloProvider>
757
+ ),
758
+ }
759
+ );
760
+
761
+ const [variables] = await takeSnapshot();
762
+ expect(variables).toStrictEqual({
763
+ category: "music",
764
+ page: 1,
765
+ sortBy: "name",
766
+ });
767
+
768
+ await expect(takeSnapshot).not.toRerender();
769
+ });
770
+
771
+ test("optional state variable is omitted from result when tool input omits it", async () => {
772
+ using _ = spyOnConsole("debug");
773
+ stubOpenAiGlobals({
774
+ toolResponseMetadata: {},
775
+ toolInput: { category: "electronics" },
776
+ });
777
+
778
+ const query: TypedDocumentNode<
779
+ any,
780
+ { category: string; page?: number | null }
781
+ > = gql`
782
+ query OptionalProduct($category: String!, $page: Int)
783
+ @tool(name: "GetProductsByCategory") {
784
+ products(category: $category, page: $page) {
785
+ id
786
+ }
787
+ }
788
+ `;
789
+
790
+ const client = new ApolloClient({
791
+ cache: new InMemoryCache(),
792
+ manifest: mockApplicationManifest(),
793
+ });
794
+
795
+ using host = await mockMcpHost({
796
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
797
+ });
798
+ host.onCleanup(() => client.stop());
799
+
800
+ host.sendToolInput({ arguments: { category: "electronics" } });
801
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
802
+
803
+ const { useHydratedVariables } = createHydrationUtils(query);
804
+
805
+ using _disabledAct = disableActEnvironment();
806
+ const { takeSnapshot } = await renderHookToSnapshotStream(
807
+ () => useHydratedVariables({ category: "music", page: 1 }),
808
+ {
809
+ wrapper: ({ children }) => (
810
+ <ApolloProvider client={client}>{children}</ApolloProvider>
811
+ ),
812
+ }
813
+ );
814
+
815
+ const [variables] = await takeSnapshot();
816
+ expect(variables).toStrictEqual({ category: "electronics" });
817
+
818
+ await expect(takeSnapshot).not.toRerender();
819
+ });
820
+
821
+ test("optional reactive variable is omitted from result when tool input omits it, then appears once reactive prop changes", async () => {
822
+ using _ = spyOnConsole("debug");
823
+ stubOpenAiGlobals({
824
+ toolResponseMetadata: {},
825
+ toolInput: { category: "electronics" },
826
+ });
827
+
828
+ const query: TypedDocumentNode<
829
+ unknown,
830
+ { category: string; page?: number | null }
831
+ > = gql`
832
+ query OptionalProduct($category: String!, $page: Int)
833
+ @tool(name: "GetProductsByCategory") {
834
+ products(category: $category, page: $page) {
835
+ id
836
+ }
837
+ }
838
+ `;
839
+
840
+ const client = new ApolloClient({
841
+ cache: new InMemoryCache(),
842
+ manifest: mockApplicationManifest(),
843
+ });
844
+
845
+ using host = await mockMcpHost({
846
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
847
+ });
848
+ host.onCleanup(() => client.stop());
849
+
850
+ host.sendToolInput({ arguments: { category: "electronics" } });
851
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
852
+
853
+ const { useHydratedVariables } = createHydrationUtils(query);
854
+
855
+ using _disabledAct = disableActEnvironment();
856
+
857
+ const { takeSnapshot, rerender } = await renderHookToSnapshotStream(
858
+ ({ page }) =>
859
+ useHydratedVariables({
860
+ category: "music",
861
+ page: reactive(page),
862
+ }),
863
+ {
864
+ initialProps: { page: 1 },
865
+ wrapper: ({ children }) => (
866
+ <ApolloProvider client={client}>{children}</ApolloProvider>
867
+ ),
868
+ }
869
+ );
870
+
871
+ {
872
+ const [variables] = await takeSnapshot();
873
+
874
+ expect(variables).toStrictEqual({ category: "electronics" });
875
+ }
876
+
877
+ rerender({ page: 2 });
878
+
879
+ {
880
+ const [variables] = await takeSnapshot();
881
+
882
+ expect(variables).toStrictEqual({ category: "electronics", page: 2 });
883
+ }
884
+
885
+ await expect(takeSnapshot).not.toRerender();
886
+ });
887
+
888
+ test("handles optional variables initially omitted from user-variables when tool input matches", async () => {
889
+ using _ = spyOnConsole("debug");
890
+ stubOpenAiGlobals({
891
+ toolResponseMetadata: {},
892
+ toolInput: { category: "electronics", page: 1 },
893
+ });
894
+
895
+ const query: TypedDocumentNode<
896
+ any,
897
+ { category: string; page?: number | null }
898
+ > = gql`
899
+ query OptionalProduct($category: String!, $page: Int)
900
+ @tool(name: "GetProductsByCategory") {
901
+ products(category: $category, page: $page) {
902
+ id
903
+ }
904
+ }
905
+ `;
906
+
907
+ const client = new ApolloClient({
908
+ cache: new InMemoryCache(),
909
+ manifest: mockApplicationManifest(),
910
+ });
911
+
912
+ using host = await mockMcpHost({
913
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
914
+ });
915
+ host.onCleanup(() => client.stop());
916
+
917
+ host.sendToolInput({ arguments: { category: "electronics", page: 1 } });
918
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
919
+
920
+ const { useHydratedVariables } = createHydrationUtils(query);
921
+
922
+ using _disabledAct = disableActEnvironment();
923
+ const { takeSnapshot } = await renderHookToSnapshotStream(
924
+ () => useHydratedVariables({ category: "music" }),
925
+ {
926
+ wrapper: ({ children }) => (
927
+ <ApolloProvider client={client}>{children}</ApolloProvider>
928
+ ),
929
+ }
930
+ );
931
+
932
+ const [variables, setVariables] = await takeSnapshot();
933
+ expect(variables).toStrictEqual({ category: "electronics", page: 1 });
934
+
935
+ setVariables({ page: 2 });
936
+
937
+ {
938
+ const [variables] = await takeSnapshot();
939
+
940
+ expect(variables).toStrictEqual({
941
+ category: "electronics",
942
+ page: 2,
943
+ });
944
+ }
945
+
946
+ await expect(takeSnapshot).not.toRerender();
947
+ });
948
+
949
+ test("handles optional variables initially omitted from user-variables when tool input doesn't match", async () => {
950
+ using _ = spyOnConsole("debug");
951
+ stubOpenAiGlobals({
952
+ toolResponseMetadata: {},
953
+ toolInput: { category: "electronics", page: 1 },
954
+ });
955
+
956
+ const query: TypedDocumentNode<
957
+ any,
958
+ { category: string; page?: number | null }
959
+ > = gql`
960
+ query OptionalProduct($category: String!, $page: Int)
961
+ @tool(name: "GetProductsByCategory") {
962
+ products(category: $category, page: $page) {
963
+ id
964
+ }
965
+ }
966
+ `;
967
+
968
+ const client = new ApolloClient({
969
+ cache: new InMemoryCache(),
970
+ manifest: mockApplicationManifest(),
971
+ });
972
+
973
+ using host = await mockMcpHost({
974
+ hostContext: minimalHostContextWithToolName("OtherTool"),
975
+ });
976
+ host.onCleanup(() => client.stop());
977
+
978
+ host.sendToolInput({ arguments: { category: "electronics", page: 1 } });
979
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
980
+
981
+ const { useHydratedVariables } = createHydrationUtils(query);
982
+
983
+ using _disabledAct = disableActEnvironment();
984
+ const { takeSnapshot } = await renderHookToSnapshotStream(
985
+ () => useHydratedVariables({ category: "music" }),
986
+ {
987
+ wrapper: ({ children }) => (
988
+ <ApolloProvider client={client}>{children}</ApolloProvider>
989
+ ),
990
+ }
991
+ );
992
+
993
+ const [variables, setVariables] = await takeSnapshot();
994
+ expect(variables).toStrictEqual({ category: "music" });
995
+
996
+ setVariables({ page: 2 });
997
+
998
+ {
999
+ const [variables] = await takeSnapshot();
1000
+
1001
+ expect(variables).toStrictEqual({
1002
+ category: "music",
1003
+ page: 2,
1004
+ });
1005
+ }
1006
+
1007
+ await expect(takeSnapshot).not.toRerender();
1008
+ });
1009
+
1010
+ test("returned variables are referentially stable between re-renders when nothing changes", async () => {
1011
+ using _ = spyOnConsole("debug");
1012
+ stubOpenAiGlobals({
1013
+ toolResponseMetadata: {},
1014
+ toolInput: { category: "electronics", page: 1, sortBy: "title" },
1015
+ });
1016
+ const client = new ApolloClient({
1017
+ cache: new InMemoryCache(),
1018
+ manifest: mockApplicationManifest(),
1019
+ });
1020
+
1021
+ using host = await mockMcpHost({
1022
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
1023
+ });
1024
+ host.onCleanup(() => client.stop());
1025
+
1026
+ host.sendToolInput({
1027
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
1028
+ });
1029
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
1030
+
1031
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
1032
+
1033
+ using _disabledAct = disableActEnvironment();
1034
+ const { takeSnapshot, rerender } = await renderHookToSnapshotStream(
1035
+ () =>
1036
+ useHydratedVariables({
1037
+ category: "music",
1038
+ page: 1,
1039
+ sortBy: "name",
1040
+ }),
1041
+ {
1042
+ wrapper: ({ children }) => (
1043
+ <ApolloProvider client={client}>{children}</ApolloProvider>
1044
+ ),
1045
+ }
1046
+ );
1047
+
1048
+ const [initialVariables] = await takeSnapshot();
1049
+ expect(initialVariables).toStrictEqual({
1050
+ category: "electronics",
1051
+ page: 1,
1052
+ sortBy: "title",
1053
+ });
1054
+
1055
+ rerender();
1056
+
1057
+ {
1058
+ const [variables] = await takeSnapshot();
1059
+
1060
+ expect(variables).toBe(initialVariables);
1061
+ }
1062
+
1063
+ rerender();
1064
+
1065
+ {
1066
+ const [variables] = await takeSnapshot();
1067
+
1068
+ expect(variables).toBe(initialVariables);
1069
+ }
1070
+
1071
+ await expect(takeSnapshot).not.toRerender();
1072
+ });
1073
+
1074
+ test("state variable is hydrated when tool name matches one of multiple @tool directives", async () => {
1075
+ using _ = spyOnConsole("debug");
1076
+ stubOpenAiGlobals({
1077
+ toolResponseMetadata: {},
1078
+ toolInput: {
1079
+ category: "electronics",
1080
+ page: 2,
1081
+ sortBy: "price",
1082
+ },
1083
+ });
1084
+
1085
+ const MULTI_TOOL_QUERY: TypedDocumentNode<
1086
+ unknown,
1087
+ { category: string; page: number; sortBy: string }
1088
+ > = gql`
1089
+ query Products($category: String!, $page: Int!, $sortBy: String!)
1090
+ @tool(name: "GetProducts")
1091
+ @tool(name: "GetProductsAlt") {
1092
+ products(category: $category, page: $page, sortBy: $sortBy) {
1093
+ id
1094
+ }
1095
+ }
1096
+ `;
1097
+
1098
+ const client = new ApolloClient({
1099
+ cache: new InMemoryCache(),
1100
+ manifest: mockApplicationManifest(),
1101
+ });
1102
+
1103
+ using host = await mockMcpHost({
1104
+ hostContext: minimalHostContextWithToolName("GetProductsAlt"),
1105
+ });
1106
+ host.onCleanup(() => client.stop());
1107
+
1108
+ host.sendToolInput({
1109
+ arguments: {
1110
+ category: "electronics",
1111
+ page: 2,
1112
+ sortBy: "price",
1113
+ },
1114
+ });
1115
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
1116
+
1117
+ const { useHydratedVariables } = createHydrationUtils(MULTI_TOOL_QUERY);
1118
+
1119
+ using _disabledAct = disableActEnvironment();
1120
+ const { takeSnapshot } = await renderHookToSnapshotStream(
1121
+ () =>
1122
+ useHydratedVariables({
1123
+ category: "music",
1124
+ page: 1,
1125
+ sortBy: "name",
1126
+ }),
1127
+ {
1128
+ wrapper: ({ children }) => (
1129
+ <ApolloProvider client={client}>{children}</ApolloProvider>
1130
+ ),
1131
+ }
1132
+ );
1133
+
1134
+ const [variables] = await takeSnapshot();
1135
+ expect(variables).toStrictEqual({
1136
+ category: "electronics",
1137
+ page: 2,
1138
+ sortBy: "price",
1139
+ });
1140
+
1141
+ await expect(takeSnapshot).not.toRerender();
1142
+ });
1143
+
1144
+ test("hydrated variables are only used the first time the component mounts, then uses user vars after", async () => {
1145
+ using _ = spyOnConsole("debug");
1146
+ stubOpenAiGlobals({
1147
+ toolResponseMetadata: {},
1148
+ toolInput: {
1149
+ category: "electronics",
1150
+ page: 1,
1151
+ sortBy: "title",
1152
+ },
1153
+ });
1154
+
1155
+ const client = new ApolloClient({
1156
+ cache: new InMemoryCache(),
1157
+ manifest: mockApplicationManifest(),
1158
+ });
1159
+
1160
+ using host = await mockMcpHost({
1161
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
1162
+ });
1163
+ host.onCleanup(() => client.stop());
1164
+
1165
+ host.sendToolInput({
1166
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
1167
+ });
1168
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
1169
+
1170
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
1171
+
1172
+ using _disabledAct = disableActEnvironment();
1173
+ const { replaceSnapshot, render, takeRender } = createRenderStream<
1174
+ ReturnType<typeof useHydratedVariables> | undefined
1175
+ >();
1176
+
1177
+ function HydratedVariables() {
1178
+ replaceSnapshot(
1179
+ useHydratedVariables({ category: "music", page: 1, sortBy: "name" })
1180
+ );
1181
+
1182
+ return null;
1183
+ }
1184
+
1185
+ function App({ mounted }: { mounted: boolean }) {
1186
+ replaceSnapshot(undefined);
1187
+ return mounted && <HydratedVariables />;
1188
+ }
1189
+
1190
+ const { rerender } = await render(<App mounted />, {
1191
+ wrapper: ({ children }) => (
1192
+ <ApolloProvider client={client}>{children}</ApolloProvider>
1193
+ ),
1194
+ });
1195
+
1196
+ {
1197
+ const { snapshot } = await takeRender();
1198
+
1199
+ assert(snapshot);
1200
+ expect(snapshot[0]).toStrictEqual({
1201
+ category: "electronics",
1202
+ page: 1,
1203
+ sortBy: "title",
1204
+ });
1205
+ }
1206
+
1207
+ await rerender(<App mounted={false} />);
1208
+
1209
+ {
1210
+ const { snapshot } = await takeRender();
1211
+
1212
+ expect(snapshot).toBeUndefined();
1213
+ }
1214
+
1215
+ await rerender(<App mounted />);
1216
+
1217
+ {
1218
+ const { snapshot } = await takeRender();
1219
+
1220
+ assert(snapshot);
1221
+ expect(snapshot[0]).toStrictEqual({
1222
+ category: "music",
1223
+ page: 1,
1224
+ sortBy: "name",
1225
+ });
1226
+ }
1227
+
1228
+ expect(takeRender).not.toRerender();
1229
+ });
1230
+
1231
+ test("hydrated variables are only used the first time the component mounts, then uses user vars after in React strict mode", async () => {
1232
+ using _ = spyOnConsole("debug");
1233
+ stubOpenAiGlobals({
1234
+ toolResponseMetadata: {},
1235
+ toolInput: {
1236
+ category: "electronics",
1237
+ page: 1,
1238
+ sortBy: "title",
1239
+ },
1240
+ });
1241
+
1242
+ const client = new ApolloClient({
1243
+ cache: new InMemoryCache(),
1244
+ manifest: mockApplicationManifest(),
1245
+ });
1246
+
1247
+ using host = await mockMcpHost({
1248
+ hostContext: minimalHostContextWithToolName("GetProductsByCategory"),
1249
+ });
1250
+ host.onCleanup(() => client.stop());
1251
+
1252
+ host.sendToolInput({
1253
+ arguments: { category: "electronics", page: 1, sortBy: "title" },
1254
+ });
1255
+ host.sendToolResult(graphqlToolResult({ data: { products: [] } }));
1256
+
1257
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
1258
+
1259
+ using _disabledAct = disableActEnvironment();
1260
+
1261
+ const { replaceSnapshot, render, takeRender } = createRenderStream<
1262
+ ReturnType<typeof useHydratedVariables> | undefined
1263
+ >();
1264
+
1265
+ function HydratedVariables() {
1266
+ replaceSnapshot(
1267
+ useHydratedVariables({ category: "music", page: 1, sortBy: "name" })
1268
+ );
1269
+
1270
+ return null;
1271
+ }
1272
+
1273
+ function App({ mounted }: { mounted: boolean }) {
1274
+ replaceSnapshot(undefined);
1275
+ return mounted && <HydratedVariables />;
1276
+ }
1277
+
1278
+ const { rerender } = await render(<App mounted />, {
1279
+ wrapper: ({ children }) => (
1280
+ <StrictMode>
1281
+ <ApolloProvider client={client}>{children}</ApolloProvider>
1282
+ </StrictMode>
1283
+ ),
1284
+ });
1285
+
1286
+ {
1287
+ const { snapshot } = await takeRender();
1288
+
1289
+ assert(snapshot);
1290
+ expect(snapshot[0]).toStrictEqual({
1291
+ category: "electronics",
1292
+ page: 1,
1293
+ sortBy: "title",
1294
+ });
1295
+ }
1296
+
1297
+ await rerender(<App mounted={false} />);
1298
+
1299
+ {
1300
+ const { snapshot } = await takeRender();
1301
+
1302
+ expect(snapshot).toBeUndefined();
1303
+ }
1304
+
1305
+ await rerender(<App mounted />);
1306
+
1307
+ {
1308
+ const { snapshot } = await takeRender();
1309
+
1310
+ assert(snapshot);
1311
+ expect(snapshot[0]).toStrictEqual({
1312
+ category: "music",
1313
+ page: 1,
1314
+ sortBy: "name",
1315
+ });
1316
+ }
1317
+
1318
+ expect(takeRender).not.toRerender();
1319
+ });
1320
+
1321
+ describe.skip("type tests", () => {
1322
+ test("TypeScript rejects variables not defined in TVariables", () => {
1323
+ const { useHydratedVariables } = createHydrationUtils(PRODUCTS_QUERY);
1324
+
1325
+ useHydratedVariables({
1326
+ category: "test",
1327
+ page: 1,
1328
+ sortBy: "asc",
1329
+ // @ts-expect-error extra key 'unknownVar' is not in TVariables
1330
+ unknownVar: "bad",
1331
+ });
1332
+ });
1333
+ });