@apollo/client-ai-apps 0.6.5 → 0.7.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/CHANGELOG.md +62 -0
- package/CONTRIBUTING.md +195 -0
- package/README.md +74 -0
- package/dist/core/AbstractApolloClient.d.ts +33 -0
- package/dist/core/AbstractApolloClient.d.ts.map +1 -0
- package/dist/core/AbstractApolloClient.js +129 -0
- package/dist/core/AbstractApolloClient.js.map +1 -0
- package/dist/core/ApolloClient.d.ts +3 -7
- package/dist/core/ApolloClient.d.ts.map +1 -1
- package/dist/core/ApolloClient.js +5 -4
- package/dist/core/ApolloClient.js.map +1 -1
- package/dist/{mcp/core → core}/McpAppManager.d.ts +14 -10
- package/dist/core/McpAppManager.d.ts.map +1 -0
- package/dist/core/McpAppManager.js +56 -0
- package/dist/core/McpAppManager.js.map +1 -0
- package/dist/core/typeRegistration.d.ts +0 -14
- package/dist/core/typeRegistration.d.ts.map +1 -1
- package/dist/core/typeRegistration.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mcp.d.ts +0 -1
- package/dist/index.mcp.d.ts.map +1 -1
- package/dist/index.mcp.js +0 -1
- package/dist/index.mcp.js.map +1 -1
- package/dist/index.openai.d.ts +0 -1
- package/dist/index.openai.d.ts.map +1 -1
- package/dist/index.openai.js +0 -1
- package/dist/index.openai.js.map +1 -1
- package/dist/link/ToolCallLink.d.ts +6 -1
- package/dist/link/ToolCallLink.d.ts.map +1 -1
- package/dist/link/ToolCallLink.js +17 -4
- package/dist/link/ToolCallLink.js.map +1 -1
- package/dist/link/ToolHydrationLink.d.ts +21 -0
- package/dist/link/ToolHydrationLink.d.ts.map +1 -0
- package/dist/link/ToolHydrationLink.js +57 -0
- package/dist/link/ToolHydrationLink.js.map +1 -0
- package/dist/mcp/core/ApolloClient.d.ts +3 -20
- package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
- package/dist/mcp/core/ApolloClient.js +20 -101
- package/dist/mcp/core/ApolloClient.js.map +1 -1
- package/dist/mcp/index.d.ts +0 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +0 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/openai/core/ApolloClient.d.ts +3 -20
- package/dist/openai/core/ApolloClient.d.ts.map +1 -1
- package/dist/openai/core/ApolloClient.js +36 -101
- package/dist/openai/core/ApolloClient.js.map +1 -1
- package/dist/openai/index.d.ts +0 -1
- package/dist/openai/index.d.ts.map +1 -1
- package/dist/openai/index.js +0 -1
- package/dist/openai/index.js.map +1 -1
- package/dist/openai/react/index.d.ts +0 -7
- package/dist/openai/react/index.d.ts.map +1 -1
- package/dist/openai/react/index.js +0 -7
- package/dist/openai/react/index.js.map +1 -1
- package/dist/react/ApolloProvider.d.ts.map +1 -1
- package/dist/react/ApolloProvider.js +1 -1
- package/dist/react/ApolloProvider.js.map +1 -1
- package/dist/{mcp/react/hooks → react}/createHydrationUtils.d.ts +1 -1
- package/dist/react/createHydrationUtils.d.ts.map +1 -0
- package/dist/{mcp/react/hooks → react}/createHydrationUtils.js +7 -9
- package/dist/react/createHydrationUtils.js.map +1 -0
- package/dist/react/hooks/internal/useApolloClient.d.ts +3 -0
- package/dist/react/hooks/internal/useApolloClient.d.ts.map +1 -0
- package/dist/{mcp/react/hooks → react/hooks/internal}/useApolloClient.js +3 -3
- package/dist/react/hooks/internal/useApolloClient.js.map +1 -0
- package/dist/react/hooks/useApp.d.ts.map +1 -0
- package/dist/react/hooks/useApp.js +5 -0
- package/dist/react/hooks/useApp.js.map +1 -0
- package/dist/react/hooks/useHostContext.d.ts.map +1 -0
- package/dist/{openai/react → react}/hooks/useHostContext.js +1 -1
- package/dist/react/hooks/useHostContext.js.map +1 -0
- package/dist/react/hooks/useToolInfo.d.ts +3 -0
- package/dist/react/hooks/useToolInfo.d.ts.map +1 -0
- package/dist/react/hooks/useToolInfo.js +5 -0
- package/dist/react/hooks/useToolInfo.js.map +1 -0
- package/dist/react/hooks/useToolMetadata.d.ts +2 -0
- package/dist/react/hooks/useToolMetadata.d.ts.map +1 -0
- package/dist/react/hooks/useToolMetadata.js +5 -0
- package/dist/react/hooks/useToolMetadata.js.map +1 -0
- package/dist/react/index.d.ts +5 -16
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -19
- package/dist/react/index.js.map +1 -1
- package/dist/utilities/connectToHost.d.ts +3 -0
- package/dist/utilities/connectToHost.d.ts.map +1 -0
- package/dist/utilities/connectToHost.js +11 -0
- package/dist/utilities/connectToHost.js.map +1 -0
- package/dist/utilities/index.d.ts +1 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +1 -0
- package/dist/utilities/index.js.map +1 -1
- package/package.json +5 -22
- package/src/core/AbstractApolloClient.ts +217 -0
- package/src/core/ApolloClient.ts +8 -10
- package/src/core/McpAppManager.ts +106 -0
- package/src/core/typeRegistration.ts +0 -15
- package/src/index.mcp.ts +0 -1
- package/src/index.openai.ts +0 -1
- package/src/index.ts +1 -6
- package/src/link/ToolCallLink.ts +27 -5
- package/src/link/ToolHydrationLink.ts +90 -0
- package/src/link/__tests__/ToolCallLink.test.ts +99 -0
- package/src/mcp/core/ApolloClient.ts +32 -170
- package/src/mcp/core/__tests__/ApolloClient.test.ts +398 -140
- package/src/mcp/index.ts +0 -1
- package/src/openai/core/ApolloClient.ts +48 -166
- package/src/openai/core/__tests__/ApolloClient.test.ts +680 -185
- package/src/openai/index.ts +0 -1
- package/src/openai/react/index.ts +0 -7
- package/src/react/ApolloProvider.tsx +1 -6
- package/src/react/__tests__/ApolloProvider/mcp.test.tsx +66 -29
- package/src/react/__tests__/ApolloProvider/openai.test.tsx +16 -41
- package/src/react/__tests__/createHydrationUtils.test.tsx +1260 -0
- package/src/{mcp/react/hooks → react}/createHydrationUtils.ts +7 -10
- package/src/react/hooks/__tests__/useApp.test.tsx +46 -0
- package/src/react/hooks/__tests__/useHostContext.test.tsx +99 -0
- package/src/react/hooks/__tests__/useToolInfo.test.tsx +98 -0
- package/src/react/hooks/__tests__/useToolMetadata.test.tsx +58 -0
- package/src/{mcp/react/hooks → react/hooks/internal}/useApolloClient.ts +3 -3
- package/src/{mcp/react → react}/hooks/useApp.ts +1 -1
- package/src/{openai/react → react}/hooks/useHostContext.ts +1 -1
- package/src/react/hooks/useToolInfo.ts +6 -0
- package/src/react/hooks/useToolMetadata.ts +5 -0
- package/src/react/index.ts +5 -36
- package/src/testing/internal/graphql/parseManifestOperation.ts +87 -0
- package/src/testing/internal/index.ts +3 -0
- package/src/testing/internal/matchers/index.ts +1 -0
- package/src/testing/internal/matchers/toEmitAnything.ts +43 -0
- package/src/testing/internal/matchers/types.ts +1 -0
- package/src/testing/internal/mcp/mockMcpHost.ts +25 -4
- package/src/testing/internal/tests/eachHostEnv.ts +22 -0
- package/src/testing/internal/utilities/createHostEnv.ts +117 -0
- package/src/utilities/connectToHost.ts +13 -0
- package/src/utilities/index.ts +1 -0
- package/tsconfig.vite.json +1 -1
- package/vitest.config.ts +13 -0
- package/dist/mcp/core/McpAppManager.d.ts.map +0 -1
- package/dist/mcp/core/McpAppManager.js +0 -88
- package/dist/mcp/core/McpAppManager.js.map +0 -1
- package/dist/mcp/link/ToolCallLink.d.ts +0 -28
- package/dist/mcp/link/ToolCallLink.d.ts.map +0 -1
- package/dist/mcp/link/ToolCallLink.js +0 -35
- package/dist/mcp/link/ToolCallLink.js.map +0 -1
- package/dist/mcp/react/hooks/createHydrationUtils.d.ts.map +0 -1
- package/dist/mcp/react/hooks/createHydrationUtils.js.map +0 -1
- package/dist/mcp/react/hooks/useApolloClient.d.ts +0 -3
- package/dist/mcp/react/hooks/useApolloClient.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useApolloClient.js.map +0 -1
- package/dist/mcp/react/hooks/useApp.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useApp.js +0 -5
- package/dist/mcp/react/hooks/useApp.js.map +0 -1
- package/dist/mcp/react/hooks/useHostContext.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useHostContext.js +0 -7
- package/dist/mcp/react/hooks/useHostContext.js.map +0 -1
- package/dist/mcp/react/hooks/useToolInfo.d.ts +0 -3
- package/dist/mcp/react/hooks/useToolInfo.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useToolInfo.js +0 -10
- package/dist/mcp/react/hooks/useToolInfo.js.map +0 -1
- package/dist/mcp/react/hooks/useToolInput.d.ts +0 -7
- package/dist/mcp/react/hooks/useToolInput.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useToolInput.js +0 -9
- package/dist/mcp/react/hooks/useToolInput.js.map +0 -1
- package/dist/mcp/react/hooks/useToolMetadata.d.ts +0 -2
- package/dist/mcp/react/hooks/useToolMetadata.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useToolMetadata.js +0 -5
- package/dist/mcp/react/hooks/useToolMetadata.js.map +0 -1
- package/dist/mcp/react/hooks/useToolName.d.ts +0 -7
- package/dist/mcp/react/hooks/useToolName.d.ts.map +0 -1
- package/dist/mcp/react/hooks/useToolName.js +0 -9
- package/dist/mcp/react/hooks/useToolName.js.map +0 -1
- package/dist/mcp/react/index.d.ts +0 -8
- package/dist/mcp/react/index.d.ts.map +0 -1
- package/dist/mcp/react/index.js +0 -8
- package/dist/mcp/react/index.js.map +0 -1
- package/dist/openai/core/McpAppManager.d.ts +0 -37
- package/dist/openai/core/McpAppManager.d.ts.map +0 -1
- package/dist/openai/core/McpAppManager.js +0 -97
- package/dist/openai/core/McpAppManager.js.map +0 -1
- package/dist/openai/link/ToolCallLink.d.ts +0 -28
- package/dist/openai/link/ToolCallLink.d.ts.map +0 -1
- package/dist/openai/link/ToolCallLink.js +0 -35
- package/dist/openai/link/ToolCallLink.js.map +0 -1
- package/dist/openai/react/hooks/createHydrationUtils.d.ts +0 -15
- package/dist/openai/react/hooks/createHydrationUtils.d.ts.map +0 -1
- package/dist/openai/react/hooks/createHydrationUtils.js +0 -113
- package/dist/openai/react/hooks/createHydrationUtils.js.map +0 -1
- package/dist/openai/react/hooks/useApp.d.ts +0 -2
- package/dist/openai/react/hooks/useApp.d.ts.map +0 -1
- package/dist/openai/react/hooks/useApp.js +0 -5
- package/dist/openai/react/hooks/useApp.js.map +0 -1
- package/dist/openai/react/hooks/useHostContext.d.ts +0 -2
- package/dist/openai/react/hooks/useHostContext.d.ts.map +0 -1
- package/dist/openai/react/hooks/useHostContext.js.map +0 -1
- package/dist/openai/react/hooks/useToolInfo.d.ts +0 -3
- package/dist/openai/react/hooks/useToolInfo.d.ts.map +0 -1
- package/dist/openai/react/hooks/useToolInfo.js +0 -10
- package/dist/openai/react/hooks/useToolInfo.js.map +0 -1
- package/dist/openai/react/hooks/useToolInput.d.ts +0 -7
- package/dist/openai/react/hooks/useToolInput.d.ts.map +0 -1
- package/dist/openai/react/hooks/useToolInput.js +0 -9
- package/dist/openai/react/hooks/useToolInput.js.map +0 -1
- package/dist/openai/react/hooks/useToolMetadata.d.ts +0 -2
- package/dist/openai/react/hooks/useToolMetadata.d.ts.map +0 -1
- package/dist/openai/react/hooks/useToolMetadata.js +0 -5
- package/dist/openai/react/hooks/useToolMetadata.js.map +0 -1
- package/dist/openai/react/hooks/useToolName.d.ts +0 -7
- package/dist/openai/react/hooks/useToolName.d.ts.map +0 -1
- package/dist/openai/react/hooks/useToolName.js +0 -9
- package/dist/openai/react/hooks/useToolName.js.map +0 -1
- package/dist/react/index.mcp.d.ts +0 -3
- package/dist/react/index.mcp.d.ts.map +0 -1
- package/dist/react/index.mcp.js +0 -3
- package/dist/react/index.mcp.js.map +0 -1
- package/dist/react/index.openai.d.ts +0 -3
- package/dist/react/index.openai.d.ts.map +0 -1
- package/dist/react/index.openai.js +0 -3
- package/dist/react/index.openai.js.map +0 -1
- package/dist/react/missingHook.d.ts +0 -2
- package/dist/react/missingHook.d.ts.map +0 -1
- package/dist/react/missingHook.js +0 -6
- package/dist/react/missingHook.js.map +0 -1
- package/src/mcp/core/McpAppManager.ts +0 -136
- package/src/mcp/link/ToolCallLink.ts +0 -40
- package/src/mcp/link/__tests__/ToolCallLink.test.ts +0 -113
- package/src/mcp/react/hooks/__tests__/createHydrationUtils.test.tsx +0 -1228
- package/src/mcp/react/hooks/__tests__/useApp.test.tsx +0 -46
- package/src/mcp/react/hooks/__tests__/useHostContext.test.tsx +0 -95
- package/src/mcp/react/hooks/__tests__/useToolInfo.test.tsx +0 -53
- package/src/mcp/react/hooks/__tests__/useToolInput.test.tsx +0 -50
- package/src/mcp/react/hooks/__tests__/useToolMetadata.test.tsx +0 -53
- package/src/mcp/react/hooks/__tests__/useToolName.test.tsx +0 -50
- package/src/mcp/react/hooks/useHostContext.ts +0 -14
- package/src/mcp/react/hooks/useToolInfo.ts +0 -13
- package/src/mcp/react/hooks/useToolInput.ts +0 -10
- package/src/mcp/react/hooks/useToolMetadata.ts +0 -5
- package/src/mcp/react/hooks/useToolName.ts +0 -10
- package/src/mcp/react/index.ts +0 -7
- package/src/openai/core/McpAppManager.ts +0 -148
- package/src/openai/link/ToolCallLink.ts +0 -40
- package/src/openai/react/hooks/__tests__/createHydrationUtils.test.tsx +0 -1333
- package/src/openai/react/hooks/__tests__/useToolInfo.test.tsx +0 -92
- package/src/openai/react/hooks/__tests__/useToolInput.test.tsx +0 -85
- package/src/openai/react/hooks/__tests__/useToolMetadata.test.tsx +0 -86
- package/src/openai/react/hooks/__tests__/useToolName.test.tsx +0 -50
- package/src/openai/react/hooks/createHydrationUtils.ts +0 -182
- package/src/openai/react/hooks/useApp.ts +0 -5
- package/src/openai/react/hooks/useToolInfo.ts +0 -13
- package/src/openai/react/hooks/useToolInput.ts +0 -10
- package/src/openai/react/hooks/useToolMetadata.ts +0 -5
- package/src/openai/react/hooks/useToolName.ts +0 -10
- package/src/react/index.mcp.ts +0 -10
- package/src/react/index.openai.ts +0 -10
- package/src/react/missingHook.ts +0 -9
- /package/dist/{mcp/react → react}/hooks/useApp.d.ts +0 -0
- /package/dist/{mcp/react → react}/hooks/useHostContext.d.ts +0 -0
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { expect, test, describe, vi } from "vitest";
|
|
2
2
|
import { ApolloClient } from "../ApolloClient.js";
|
|
3
|
-
import { parse } from "graphql";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { parse, type DocumentNode } from "graphql";
|
|
4
|
+
import {
|
|
5
|
+
ApolloLink,
|
|
6
|
+
HttpLink,
|
|
7
|
+
InMemoryCache,
|
|
8
|
+
NetworkStatus,
|
|
9
|
+
gql,
|
|
10
|
+
} from "@apollo/client";
|
|
11
|
+
import { ToolCallLink } from "../../../link/ToolCallLink.js";
|
|
7
12
|
import {
|
|
8
13
|
graphqlToolResult,
|
|
9
14
|
minimalHostContextWithToolName,
|
|
10
15
|
mockApplicationManifest,
|
|
11
16
|
mockMcpHost,
|
|
17
|
+
ObservableStream,
|
|
18
|
+
parseManifestOperation,
|
|
12
19
|
spyOnConsole,
|
|
13
20
|
stubOpenAiGlobals,
|
|
14
21
|
} from "../../../testing/internal/index.js";
|
|
@@ -152,19 +159,27 @@ describe("prefetchData", () => {
|
|
|
152
159
|
test("caches tool response when data is provided", async () => {
|
|
153
160
|
stubOpenAiGlobals({ toolInput: { id: 1 } });
|
|
154
161
|
using _ = spyOnConsole("debug");
|
|
155
|
-
const client = new ApolloClient({
|
|
156
|
-
cache: new InMemoryCache(),
|
|
157
|
-
manifest: mockApplicationManifest(),
|
|
158
|
-
});
|
|
159
|
-
using host = await mockMcpHost({
|
|
160
|
-
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
161
|
-
});
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
const query = gql`
|
|
164
|
+
query Product($id: ID!)
|
|
165
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
166
|
+
product(id: $id) {
|
|
167
|
+
id
|
|
168
|
+
title
|
|
169
|
+
rating
|
|
170
|
+
price
|
|
171
|
+
description
|
|
172
|
+
images
|
|
173
|
+
__typename
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
const { client, host } = await setup({ query });
|
|
179
|
+
using _host = host;
|
|
164
180
|
|
|
165
181
|
host.sendToolInput({ arguments: { id: 1 } });
|
|
166
182
|
host.sendToolResult({
|
|
167
|
-
content: [],
|
|
168
183
|
structuredContent: {
|
|
169
184
|
result: {
|
|
170
185
|
data: {
|
|
@@ -184,25 +199,39 @@ describe("prefetchData", () => {
|
|
|
184
199
|
|
|
185
200
|
await client.connect();
|
|
186
201
|
|
|
187
|
-
expect(
|
|
188
|
-
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
await expect(
|
|
203
|
+
client.query({ query, variables: { id: 1 } })
|
|
204
|
+
).resolves.toStrictEqual({
|
|
205
|
+
data: {
|
|
206
|
+
product: {
|
|
207
|
+
id: "1",
|
|
208
|
+
title: "Pen",
|
|
209
|
+
rating: 5,
|
|
210
|
+
price: 1.0,
|
|
211
|
+
description: "Awesome pen",
|
|
212
|
+
images: [],
|
|
213
|
+
__typename: "Product",
|
|
197
214
|
},
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(client.extract()).toStrictEqual({
|
|
219
|
+
"Product:1": {
|
|
220
|
+
__typename: "Product",
|
|
221
|
+
description: "Awesome pen",
|
|
222
|
+
id: "1",
|
|
223
|
+
images: [],
|
|
224
|
+
price: 1,
|
|
225
|
+
rating: 5,
|
|
226
|
+
title: "Pen",
|
|
227
|
+
},
|
|
228
|
+
ROOT_QUERY: {
|
|
229
|
+
__typename: "Query",
|
|
230
|
+
'product({"id":1})': {
|
|
231
|
+
__ref: "Product:1",
|
|
203
232
|
},
|
|
204
|
-
}
|
|
205
|
-
|
|
233
|
+
},
|
|
234
|
+
});
|
|
206
235
|
});
|
|
207
236
|
|
|
208
237
|
test("caches prefetched data when prefetched data is provided", async () => {
|
|
@@ -286,39 +315,42 @@ describe("prefetchData", () => {
|
|
|
286
315
|
test("caches both prefetch and tool response when both are provided", async () => {
|
|
287
316
|
stubOpenAiGlobals({ toolInput: { id: 1 } });
|
|
288
317
|
using _ = spyOnConsole("debug");
|
|
318
|
+
|
|
319
|
+
const prefetchQuery = gql`
|
|
320
|
+
query TopProducts
|
|
321
|
+
@tool(description: "Shows the currently highest rated products.")
|
|
322
|
+
@prefetch {
|
|
323
|
+
topProducts {
|
|
324
|
+
id
|
|
325
|
+
title
|
|
326
|
+
rating
|
|
327
|
+
price
|
|
328
|
+
__typename
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
const query = gql`
|
|
334
|
+
query Product($id: ID!)
|
|
335
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
336
|
+
product(id: $id) {
|
|
337
|
+
id
|
|
338
|
+
title
|
|
339
|
+
rating
|
|
340
|
+
price
|
|
341
|
+
description
|
|
342
|
+
images
|
|
343
|
+
__typename
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
`;
|
|
347
|
+
|
|
289
348
|
const client = new ApolloClient({
|
|
290
349
|
cache: new InMemoryCache(),
|
|
291
350
|
manifest: mockApplicationManifest({
|
|
292
351
|
operations: [
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
name: "Product",
|
|
296
|
-
type: "query",
|
|
297
|
-
body: "query Product($id: ID!) {\n product(id: $id) {\n id\n title\n rating\n price\n description\n images\n __typename\n }\n}",
|
|
298
|
-
variables: { id: "ID" },
|
|
299
|
-
prefetch: false,
|
|
300
|
-
tools: [
|
|
301
|
-
{
|
|
302
|
-
name: "GetProduct",
|
|
303
|
-
description: "Shows the details page for a specific product.",
|
|
304
|
-
},
|
|
305
|
-
],
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
id: "cd0d52159b9003e791de97c6a76efa03d34fe00cee278d1a3f4bfcec5fb3e1e6",
|
|
309
|
-
name: "TopProducts",
|
|
310
|
-
type: "query",
|
|
311
|
-
body: "query TopProducts {\n topProducts {\n id\n title\n rating\n price\n __typename\n }\n}",
|
|
312
|
-
variables: {},
|
|
313
|
-
prefetch: true,
|
|
314
|
-
prefetchID: "__anonymous",
|
|
315
|
-
tools: [
|
|
316
|
-
{
|
|
317
|
-
name: "TopProducts",
|
|
318
|
-
description: "Shows the currently highest rated products.",
|
|
319
|
-
},
|
|
320
|
-
],
|
|
321
|
-
},
|
|
352
|
+
parseManifestOperation(prefetchQuery),
|
|
353
|
+
parseManifestOperation(query),
|
|
322
354
|
],
|
|
323
355
|
}),
|
|
324
356
|
});
|
|
@@ -330,7 +362,6 @@ describe("prefetchData", () => {
|
|
|
330
362
|
|
|
331
363
|
host.sendToolInput({ arguments: { id: 1 } });
|
|
332
364
|
host.sendToolResult({
|
|
333
|
-
content: [],
|
|
334
365
|
structuredContent: {
|
|
335
366
|
result: {
|
|
336
367
|
data: {
|
|
@@ -367,7 +398,53 @@ describe("prefetchData", () => {
|
|
|
367
398
|
|
|
368
399
|
await client.connect();
|
|
369
400
|
|
|
370
|
-
expect(
|
|
401
|
+
await expect(
|
|
402
|
+
client.query({ query, variables: { id: 1 } })
|
|
403
|
+
).resolves.toStrictEqual({
|
|
404
|
+
data: {
|
|
405
|
+
product: {
|
|
406
|
+
id: "1",
|
|
407
|
+
title: "Pen",
|
|
408
|
+
rating: 5,
|
|
409
|
+
price: 1.0,
|
|
410
|
+
description: "Awesome pen",
|
|
411
|
+
images: [],
|
|
412
|
+
__typename: "Product",
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(client.extract()).toMatchInlineSnapshot(
|
|
418
|
+
{
|
|
419
|
+
"Product:1": {
|
|
420
|
+
__typename: "Product",
|
|
421
|
+
description: "Awesome pen",
|
|
422
|
+
id: "1",
|
|
423
|
+
images: [],
|
|
424
|
+
price: 1,
|
|
425
|
+
rating: 5,
|
|
426
|
+
title: "Pen",
|
|
427
|
+
},
|
|
428
|
+
"Product:2": {
|
|
429
|
+
__typename: "Product",
|
|
430
|
+
id: "2",
|
|
431
|
+
price: 999.99,
|
|
432
|
+
rating: 5,
|
|
433
|
+
title: "iPhone 17",
|
|
434
|
+
},
|
|
435
|
+
ROOT_QUERY: {
|
|
436
|
+
__typename: "Query",
|
|
437
|
+
'product({"id":1})': {
|
|
438
|
+
__ref: "Product:1",
|
|
439
|
+
},
|
|
440
|
+
topProducts: [
|
|
441
|
+
{
|
|
442
|
+
__ref: "Product:2",
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
`
|
|
371
448
|
{
|
|
372
449
|
"Product:1": {
|
|
373
450
|
"__typename": "Product",
|
|
@@ -397,21 +474,30 @@ describe("prefetchData", () => {
|
|
|
397
474
|
],
|
|
398
475
|
},
|
|
399
476
|
}
|
|
400
|
-
`
|
|
477
|
+
`
|
|
478
|
+
);
|
|
401
479
|
});
|
|
402
480
|
|
|
403
481
|
test("excludes extra inputs when writing to cache", async () => {
|
|
404
482
|
stubOpenAiGlobals({ toolInput: { id: 1, myOtherThing: 2 } });
|
|
405
483
|
using _ = spyOnConsole("debug");
|
|
406
|
-
const client = new ApolloClient({
|
|
407
|
-
cache: new InMemoryCache(),
|
|
408
|
-
manifest: mockApplicationManifest(),
|
|
409
|
-
});
|
|
410
|
-
using host = await mockMcpHost({
|
|
411
|
-
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
412
|
-
});
|
|
413
484
|
|
|
414
|
-
|
|
485
|
+
const query = gql`
|
|
486
|
+
query Product($id: ID!)
|
|
487
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
488
|
+
product(id: $id) {
|
|
489
|
+
id
|
|
490
|
+
title
|
|
491
|
+
rating
|
|
492
|
+
price
|
|
493
|
+
description
|
|
494
|
+
images
|
|
495
|
+
__typename
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
`;
|
|
499
|
+
const { client, host } = await setup({ query });
|
|
500
|
+
using _host = host;
|
|
415
501
|
|
|
416
502
|
host.sendToolInput({ arguments: { id: 1, myOtherThing: 2 } });
|
|
417
503
|
host.sendToolResult({
|
|
@@ -435,25 +521,39 @@ describe("prefetchData", () => {
|
|
|
435
521
|
|
|
436
522
|
await client.connect();
|
|
437
523
|
|
|
438
|
-
expect(
|
|
439
|
-
{
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
"
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
524
|
+
await expect(
|
|
525
|
+
client.query({ query, variables: { id: 1 } })
|
|
526
|
+
).resolves.toStrictEqual({
|
|
527
|
+
data: {
|
|
528
|
+
product: {
|
|
529
|
+
id: "1",
|
|
530
|
+
title: "Pen",
|
|
531
|
+
rating: 5,
|
|
532
|
+
price: 1.0,
|
|
533
|
+
description: "Awesome pen",
|
|
534
|
+
images: [],
|
|
535
|
+
__typename: "Product",
|
|
448
536
|
},
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
expect(client.extract()).toStrictEqual({
|
|
541
|
+
"Product:1": {
|
|
542
|
+
__typename: "Product",
|
|
543
|
+
description: "Awesome pen",
|
|
544
|
+
id: "1",
|
|
545
|
+
images: [],
|
|
546
|
+
price: 1,
|
|
547
|
+
rating: 5,
|
|
548
|
+
title: "Pen",
|
|
549
|
+
},
|
|
550
|
+
ROOT_QUERY: {
|
|
551
|
+
__typename: "Query",
|
|
552
|
+
'product({"id":1})': {
|
|
553
|
+
__ref: "Product:1",
|
|
454
554
|
},
|
|
455
|
-
}
|
|
456
|
-
|
|
555
|
+
},
|
|
556
|
+
});
|
|
457
557
|
});
|
|
458
558
|
});
|
|
459
559
|
|
|
@@ -473,7 +573,8 @@ test("reads result data from toolResponseMetadata.structuredContent", async () =
|
|
|
473
573
|
using _ = spyOnConsole("debug");
|
|
474
574
|
|
|
475
575
|
const query = gql`
|
|
476
|
-
query Product($id: ID!)
|
|
576
|
+
query Product($id: ID!)
|
|
577
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
477
578
|
product(id: $id) @private {
|
|
478
579
|
id
|
|
479
580
|
title
|
|
@@ -482,36 +583,23 @@ test("reads result data from toolResponseMetadata.structuredContent", async () =
|
|
|
482
583
|
}
|
|
483
584
|
`;
|
|
484
585
|
|
|
485
|
-
const client =
|
|
486
|
-
|
|
487
|
-
manifest: mockApplicationManifest({
|
|
488
|
-
operations: [
|
|
489
|
-
{
|
|
490
|
-
id: "c43af26552874026c3fb346148c5795896aa2f3a872410a0a2621cffee25291c",
|
|
491
|
-
name: "Product",
|
|
492
|
-
type: "query",
|
|
493
|
-
body: print(query),
|
|
494
|
-
variables: { id: "ID" },
|
|
495
|
-
prefetch: false,
|
|
496
|
-
tools: [{ name: "GetProduct", description: "Get a product" }],
|
|
497
|
-
},
|
|
498
|
-
],
|
|
499
|
-
}),
|
|
500
|
-
});
|
|
501
|
-
using host = await mockMcpHost({
|
|
502
|
-
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
503
|
-
});
|
|
504
|
-
host.onCleanup(() => client.stop());
|
|
586
|
+
const { client, host } = await setup({ query });
|
|
587
|
+
using _host = host;
|
|
505
588
|
|
|
506
589
|
host.sendToolInput({ arguments: { id: "1" } });
|
|
507
590
|
host.sendToolResult({
|
|
508
|
-
content: [],
|
|
509
591
|
structuredContent: {},
|
|
510
592
|
});
|
|
511
593
|
|
|
512
594
|
await client.connect();
|
|
513
595
|
|
|
514
|
-
expect(
|
|
596
|
+
await expect(
|
|
597
|
+
client.query({ query, variables: { id: "1" } })
|
|
598
|
+
).resolves.toStrictEqual({
|
|
599
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
expect(client.extract()).toStrictEqual({
|
|
515
603
|
"Product:1": {
|
|
516
604
|
__typename: "Product",
|
|
517
605
|
id: "1",
|
|
@@ -540,7 +628,9 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
|
|
|
540
628
|
using _ = spyOnConsole("debug");
|
|
541
629
|
|
|
542
630
|
const prefetchQuery = gql`
|
|
543
|
-
query TopProducts
|
|
631
|
+
query TopProducts
|
|
632
|
+
@tool(description: "Shows the currently highest rated products.")
|
|
633
|
+
@prefetch {
|
|
544
634
|
topProducts {
|
|
545
635
|
id
|
|
546
636
|
title
|
|
@@ -550,7 +640,8 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
|
|
|
550
640
|
`;
|
|
551
641
|
|
|
552
642
|
const query = gql`
|
|
553
|
-
query Product($id: ID!)
|
|
643
|
+
query Product($id: ID!)
|
|
644
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
554
645
|
product(id: $id) @private {
|
|
555
646
|
id
|
|
556
647
|
title
|
|
@@ -563,30 +654,8 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
|
|
|
563
654
|
cache: new InMemoryCache(),
|
|
564
655
|
manifest: mockApplicationManifest({
|
|
565
656
|
operations: [
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
name: "TopProducts",
|
|
569
|
-
body: print(prefetchQuery),
|
|
570
|
-
type: "query",
|
|
571
|
-
variables: {},
|
|
572
|
-
prefetch: true,
|
|
573
|
-
prefetchID: "__anonymous",
|
|
574
|
-
tools: [
|
|
575
|
-
{
|
|
576
|
-
name: "TopProducts",
|
|
577
|
-
description: "Shows the currently highest rated products.",
|
|
578
|
-
},
|
|
579
|
-
],
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
id: "2",
|
|
583
|
-
name: "Product",
|
|
584
|
-
body: print(query),
|
|
585
|
-
type: "query",
|
|
586
|
-
variables: { id: "ID" },
|
|
587
|
-
prefetch: false,
|
|
588
|
-
tools: [{ name: "GetProduct", description: "Get a product" }],
|
|
589
|
-
},
|
|
657
|
+
parseManifestOperation(prefetchQuery),
|
|
658
|
+
parseManifestOperation(query),
|
|
590
659
|
],
|
|
591
660
|
}),
|
|
592
661
|
});
|
|
@@ -597,7 +666,6 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
|
|
|
597
666
|
|
|
598
667
|
host.sendToolInput({ arguments: { id: "2" } });
|
|
599
668
|
host.sendToolResult({
|
|
600
|
-
content: [],
|
|
601
669
|
structuredContent: {
|
|
602
670
|
prefetch: {
|
|
603
671
|
__anonymous: {
|
|
@@ -611,6 +679,12 @@ test("merges prefetch from structuredContent and result from toolResponseMetadat
|
|
|
611
679
|
|
|
612
680
|
await client.connect();
|
|
613
681
|
|
|
682
|
+
await expect(
|
|
683
|
+
client.query({ query, variables: { id: "2" } })
|
|
684
|
+
).resolves.toStrictEqual({
|
|
685
|
+
data: { product: { id: "2", title: "iPad", __typename: "Product" } },
|
|
686
|
+
});
|
|
687
|
+
|
|
614
688
|
expect(client.extract()).toEqual({
|
|
615
689
|
"Product:1": {
|
|
616
690
|
__typename: "Product",
|
|
@@ -646,7 +720,8 @@ test("toolResponseMetadata.structuredContent wins over structuredContent", async
|
|
|
646
720
|
using _ = spyOnConsole("debug");
|
|
647
721
|
|
|
648
722
|
const query = gql`
|
|
649
|
-
query Product($id: ID!)
|
|
723
|
+
query Product($id: ID!)
|
|
724
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
650
725
|
product(id: $id) {
|
|
651
726
|
id
|
|
652
727
|
title @private
|
|
@@ -655,30 +730,11 @@ test("toolResponseMetadata.structuredContent wins over structuredContent", async
|
|
|
655
730
|
}
|
|
656
731
|
`;
|
|
657
732
|
|
|
658
|
-
const client =
|
|
659
|
-
|
|
660
|
-
manifest: mockApplicationManifest({
|
|
661
|
-
operations: [
|
|
662
|
-
{
|
|
663
|
-
id: "1",
|
|
664
|
-
name: "Product",
|
|
665
|
-
body: print(query),
|
|
666
|
-
type: "query",
|
|
667
|
-
variables: { id: "ID" },
|
|
668
|
-
prefetch: false,
|
|
669
|
-
tools: [{ name: "GetProduct", description: "Get a product" }],
|
|
670
|
-
},
|
|
671
|
-
],
|
|
672
|
-
}),
|
|
673
|
-
});
|
|
674
|
-
using host = await mockMcpHost({
|
|
675
|
-
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
676
|
-
});
|
|
677
|
-
host.onCleanup(() => client.stop());
|
|
733
|
+
const { client, host } = await setup({ query });
|
|
734
|
+
using _host = host;
|
|
678
735
|
|
|
679
736
|
host.sendToolInput({ arguments: { id: "1" } });
|
|
680
737
|
host.sendToolResult({
|
|
681
|
-
content: [],
|
|
682
738
|
structuredContent: {
|
|
683
739
|
result: {
|
|
684
740
|
data: {
|
|
@@ -690,7 +746,13 @@ test("toolResponseMetadata.structuredContent wins over structuredContent", async
|
|
|
690
746
|
|
|
691
747
|
await client.connect();
|
|
692
748
|
|
|
693
|
-
expect(
|
|
749
|
+
await expect(
|
|
750
|
+
client.query({ query, variables: { id: "1" } })
|
|
751
|
+
).resolves.toStrictEqual({
|
|
752
|
+
data: { product: { id: "1", title: "Meta title", __typename: "Product" } },
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
expect(client.extract()).toStrictEqual({
|
|
694
756
|
"Product:1": {
|
|
695
757
|
__typename: "Product",
|
|
696
758
|
id: "1",
|
|
@@ -723,14 +785,24 @@ test("connects using window.openai.toolOutput when tool-result notification is n
|
|
|
723
785
|
toolInput: { id: "1" },
|
|
724
786
|
});
|
|
725
787
|
using _ = spyOnConsole("debug");
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
788
|
+
|
|
789
|
+
const query = gql`
|
|
790
|
+
query Product($id: ID!)
|
|
791
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
792
|
+
product(id: $id) {
|
|
793
|
+
id
|
|
794
|
+
title
|
|
795
|
+
rating
|
|
796
|
+
price
|
|
797
|
+
description
|
|
798
|
+
images
|
|
799
|
+
__typename
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
`;
|
|
803
|
+
|
|
804
|
+
const { client, host } = await setup({ query });
|
|
805
|
+
using _host = host;
|
|
734
806
|
|
|
735
807
|
host.sendToolInput({ arguments: { id: "1" } });
|
|
736
808
|
// No host.sendToolResult() — simulates page reload where ChatGPT does not
|
|
@@ -742,25 +814,39 @@ test("connects using window.openai.toolOutput when tool-result notification is n
|
|
|
742
814
|
// before `using host` disposes and closes the app connection.
|
|
743
815
|
await new Promise((resolve) => setImmediate(resolve));
|
|
744
816
|
|
|
745
|
-
expect(
|
|
746
|
-
{
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
"
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
817
|
+
await expect(
|
|
818
|
+
client.query({ query, variables: { id: "1" } })
|
|
819
|
+
).resolves.toStrictEqual({
|
|
820
|
+
data: {
|
|
821
|
+
product: {
|
|
822
|
+
id: "1",
|
|
823
|
+
title: "Pen",
|
|
824
|
+
rating: 5,
|
|
825
|
+
price: 1.0,
|
|
826
|
+
description: "Awesome pen",
|
|
827
|
+
images: [],
|
|
828
|
+
__typename: "Product",
|
|
755
829
|
},
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
expect(client.extract()).toStrictEqual({
|
|
834
|
+
"Product:1": {
|
|
835
|
+
__typename: "Product",
|
|
836
|
+
description: "Awesome pen",
|
|
837
|
+
id: "1",
|
|
838
|
+
images: [],
|
|
839
|
+
price: 1,
|
|
840
|
+
rating: 5,
|
|
841
|
+
title: "Pen",
|
|
842
|
+
},
|
|
843
|
+
ROOT_QUERY: {
|
|
844
|
+
__typename: "Query",
|
|
845
|
+
'product({"id":"1"})': {
|
|
846
|
+
__ref: "Product:1",
|
|
761
847
|
},
|
|
762
|
-
}
|
|
763
|
-
|
|
848
|
+
},
|
|
849
|
+
});
|
|
764
850
|
});
|
|
765
851
|
|
|
766
852
|
describe("custom links", () => {
|
|
@@ -900,6 +986,392 @@ describe("custom links", () => {
|
|
|
900
986
|
});
|
|
901
987
|
});
|
|
902
988
|
|
|
989
|
+
test("serves tool result data on network-only query without calling execute tool", async () => {
|
|
990
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
991
|
+
using _ = spyOnConsole("debug");
|
|
992
|
+
const query = gql`
|
|
993
|
+
query Product($id: ID!)
|
|
994
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
995
|
+
product(id: $id) {
|
|
996
|
+
id
|
|
997
|
+
title
|
|
998
|
+
__typename
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
`;
|
|
1002
|
+
|
|
1003
|
+
const data = {
|
|
1004
|
+
product: { id: "1", title: "Pen", __typename: "Product" },
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
const { client, host } = await setup({ query });
|
|
1008
|
+
using _host = host;
|
|
1009
|
+
|
|
1010
|
+
const execute = vi.fn();
|
|
1011
|
+
host.mockToolCall("execute", execute);
|
|
1012
|
+
|
|
1013
|
+
host.sendToolResult({ structuredContent: { result: { data } } });
|
|
1014
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1015
|
+
|
|
1016
|
+
await client.connect();
|
|
1017
|
+
|
|
1018
|
+
await expect(
|
|
1019
|
+
client.query({
|
|
1020
|
+
query,
|
|
1021
|
+
variables: { id: "1" },
|
|
1022
|
+
fetchPolicy: "network-only",
|
|
1023
|
+
})
|
|
1024
|
+
).resolves.toStrictEqual({ data });
|
|
1025
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test("calls execute tool on second network-only query after hydration is consumed", async () => {
|
|
1029
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1030
|
+
using _ = spyOnConsole("debug");
|
|
1031
|
+
const query = gql`
|
|
1032
|
+
query Product($id: ID!)
|
|
1033
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1034
|
+
product(id: $id) {
|
|
1035
|
+
id
|
|
1036
|
+
title
|
|
1037
|
+
__typename
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
`;
|
|
1041
|
+
|
|
1042
|
+
const { client, host } = await setup({ query });
|
|
1043
|
+
using _host = host;
|
|
1044
|
+
|
|
1045
|
+
host.mockToolCall("execute", () => ({
|
|
1046
|
+
structuredContent: {
|
|
1047
|
+
data: {
|
|
1048
|
+
product: { id: "1", title: "Updated Pen", __typename: "Product" },
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
}));
|
|
1052
|
+
|
|
1053
|
+
host.sendToolResult({
|
|
1054
|
+
structuredContent: {
|
|
1055
|
+
result: {
|
|
1056
|
+
data: {
|
|
1057
|
+
product: { id: "1", title: "Pen", __typename: "Product" },
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1063
|
+
|
|
1064
|
+
await client.connect();
|
|
1065
|
+
|
|
1066
|
+
await client.query({
|
|
1067
|
+
query,
|
|
1068
|
+
variables: { id: "1" },
|
|
1069
|
+
fetchPolicy: "network-only",
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
await expect(
|
|
1073
|
+
client.query({
|
|
1074
|
+
query,
|
|
1075
|
+
variables: { id: "1" },
|
|
1076
|
+
fetchPolicy: "network-only",
|
|
1077
|
+
})
|
|
1078
|
+
).resolves.toStrictEqual({
|
|
1079
|
+
data: {
|
|
1080
|
+
product: { id: "1", title: "Updated Pen", __typename: "Product" },
|
|
1081
|
+
},
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test("serves tool result data on cache-and-network query without calling execute tool", async () => {
|
|
1086
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1087
|
+
using _ = spyOnConsole("debug");
|
|
1088
|
+
const query = gql`
|
|
1089
|
+
query Product($id: ID!)
|
|
1090
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1091
|
+
product(id: $id) {
|
|
1092
|
+
id
|
|
1093
|
+
title
|
|
1094
|
+
__typename
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
`;
|
|
1098
|
+
|
|
1099
|
+
const { client, host } = await setup({ query });
|
|
1100
|
+
using _host = host;
|
|
1101
|
+
|
|
1102
|
+
const execute = vi.fn();
|
|
1103
|
+
host.mockToolCall("execute", execute);
|
|
1104
|
+
|
|
1105
|
+
host.sendToolResult({
|
|
1106
|
+
structuredContent: {
|
|
1107
|
+
result: {
|
|
1108
|
+
data: {
|
|
1109
|
+
product: { id: "1", title: "Pen", __typename: "Product" },
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1115
|
+
|
|
1116
|
+
await client.connect();
|
|
1117
|
+
|
|
1118
|
+
const stream = new ObservableStream(
|
|
1119
|
+
client.watchQuery({
|
|
1120
|
+
query,
|
|
1121
|
+
variables: { id: "1" },
|
|
1122
|
+
fetchPolicy: "cache-and-network",
|
|
1123
|
+
})
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
// The hydrated result is emitted synchronously so we won't observe a loading
|
|
1127
|
+
// state like we normally would with `cache-and-network`
|
|
1128
|
+
await expect(stream).toEmitValue({
|
|
1129
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1130
|
+
dataState: "complete",
|
|
1131
|
+
loading: false,
|
|
1132
|
+
networkStatus: NetworkStatus.ready,
|
|
1133
|
+
partial: false,
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
await expect(stream).not.toEmitAnything();
|
|
1137
|
+
|
|
1138
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test("serves tool result data on no-cache query without calling execute tool and does not write to cache", async () => {
|
|
1142
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1143
|
+
using _ = spyOnConsole("debug");
|
|
1144
|
+
const query = gql`
|
|
1145
|
+
query Product($id: ID!)
|
|
1146
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1147
|
+
product(id: $id) {
|
|
1148
|
+
id
|
|
1149
|
+
title
|
|
1150
|
+
__typename
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
`;
|
|
1154
|
+
|
|
1155
|
+
const data = {
|
|
1156
|
+
product: { id: "1", title: "Pen", __typename: "Product" },
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
const { client, host } = await setup({ query });
|
|
1160
|
+
using _host = host;
|
|
1161
|
+
|
|
1162
|
+
const execute = vi.fn();
|
|
1163
|
+
host.mockToolCall("execute", execute);
|
|
1164
|
+
|
|
1165
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1166
|
+
host.sendToolResult({ structuredContent: { result: { data } } });
|
|
1167
|
+
|
|
1168
|
+
await client.connect();
|
|
1169
|
+
|
|
1170
|
+
await expect(
|
|
1171
|
+
client.query({ query, variables: { id: "1" }, fetchPolicy: "no-cache" })
|
|
1172
|
+
).resolves.toStrictEqual({ data });
|
|
1173
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1174
|
+
expect(client.extract()).toStrictEqual({});
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test("hydrates prefetch query with network-only fetch policy", async () => {
|
|
1178
|
+
stubOpenAiGlobals({ toolInput: {} });
|
|
1179
|
+
using _ = spyOnConsole("debug");
|
|
1180
|
+
|
|
1181
|
+
const query = gql`
|
|
1182
|
+
query TopProducts @tool(description: "Shows top products") @prefetch {
|
|
1183
|
+
topProducts {
|
|
1184
|
+
id
|
|
1185
|
+
title
|
|
1186
|
+
__typename
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
`;
|
|
1190
|
+
|
|
1191
|
+
const data = {
|
|
1192
|
+
topProducts: [{ id: "1", title: "iPhone", __typename: "Product" }],
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
const { client, host } = await setup({ query, toolName: "OtherTool" });
|
|
1196
|
+
using _host = host;
|
|
1197
|
+
|
|
1198
|
+
const execute = vi.fn();
|
|
1199
|
+
host.mockToolCall("execute", execute);
|
|
1200
|
+
|
|
1201
|
+
host.sendToolResult({
|
|
1202
|
+
structuredContent: {
|
|
1203
|
+
prefetch: { __anonymous: { data } },
|
|
1204
|
+
},
|
|
1205
|
+
});
|
|
1206
|
+
host.sendToolInput({ arguments: {} });
|
|
1207
|
+
|
|
1208
|
+
await client.connect();
|
|
1209
|
+
|
|
1210
|
+
await expect(
|
|
1211
|
+
client.query({ query, fetchPolicy: "network-only" })
|
|
1212
|
+
).resolves.toStrictEqual({ data });
|
|
1213
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
test("serves hydrated query from tool result while other network-only queries call execute", async () => {
|
|
1217
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1218
|
+
using _ = spyOnConsole("debug");
|
|
1219
|
+
|
|
1220
|
+
const productQuery = gql`
|
|
1221
|
+
query Product($id: ID!)
|
|
1222
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1223
|
+
product(id: $id) {
|
|
1224
|
+
id
|
|
1225
|
+
title
|
|
1226
|
+
__typename
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
`;
|
|
1230
|
+
|
|
1231
|
+
const cartQuery = gql`
|
|
1232
|
+
query Cart @tool(name: "GetCart", description: "Get the cart") {
|
|
1233
|
+
cart {
|
|
1234
|
+
id
|
|
1235
|
+
__typename
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
`;
|
|
1239
|
+
|
|
1240
|
+
const productOperation = parseManifestOperation(productQuery);
|
|
1241
|
+
const cartOperation = parseManifestOperation(cartQuery);
|
|
1242
|
+
|
|
1243
|
+
const client = new ApolloClient({
|
|
1244
|
+
cache: new InMemoryCache(),
|
|
1245
|
+
manifest: mockApplicationManifest({
|
|
1246
|
+
operations: [productOperation, cartOperation],
|
|
1247
|
+
}),
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
using host = await mockMcpHost({
|
|
1251
|
+
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
1252
|
+
});
|
|
1253
|
+
host.onCleanup(() => client.stop());
|
|
1254
|
+
|
|
1255
|
+
const execute = vi.fn(() => ({
|
|
1256
|
+
structuredContent: {
|
|
1257
|
+
data: { cart: { id: "1", __typename: "Cart" } },
|
|
1258
|
+
},
|
|
1259
|
+
}));
|
|
1260
|
+
host.mockToolCall("execute", execute);
|
|
1261
|
+
|
|
1262
|
+
host.sendToolResult({
|
|
1263
|
+
structuredContent: {
|
|
1264
|
+
result: {
|
|
1265
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
});
|
|
1269
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1270
|
+
|
|
1271
|
+
await client.connect();
|
|
1272
|
+
|
|
1273
|
+
const [productResult, cartResult] = await Promise.all([
|
|
1274
|
+
client.query({
|
|
1275
|
+
query: productQuery,
|
|
1276
|
+
variables: { id: "1" },
|
|
1277
|
+
fetchPolicy: "network-only",
|
|
1278
|
+
}),
|
|
1279
|
+
client.query({
|
|
1280
|
+
query: cartQuery,
|
|
1281
|
+
fetchPolicy: "network-only",
|
|
1282
|
+
}),
|
|
1283
|
+
]);
|
|
1284
|
+
|
|
1285
|
+
expect(productResult).toStrictEqual({
|
|
1286
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1287
|
+
});
|
|
1288
|
+
expect(cartResult).toStrictEqual({
|
|
1289
|
+
data: { cart: { id: "1", __typename: "Cart" } },
|
|
1290
|
+
});
|
|
1291
|
+
expect(execute).toHaveBeenCalledOnce();
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
test("serves hydrated query after tool result while earlier-queued non-matching query calls execute", async () => {
|
|
1295
|
+
stubOpenAiGlobals({ toolInput: { id: "1" } });
|
|
1296
|
+
using _ = spyOnConsole("debug");
|
|
1297
|
+
|
|
1298
|
+
const productQuery = gql`
|
|
1299
|
+
query Product($id: ID!)
|
|
1300
|
+
@tool(name: "GetProduct", description: "Get a product") {
|
|
1301
|
+
product(id: $id) {
|
|
1302
|
+
id
|
|
1303
|
+
title
|
|
1304
|
+
__typename
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
`;
|
|
1308
|
+
|
|
1309
|
+
const cartQuery = gql`
|
|
1310
|
+
query Cart @tool(name: "GetCart", description: "Get the cart") {
|
|
1311
|
+
cart {
|
|
1312
|
+
id
|
|
1313
|
+
__typename
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
`;
|
|
1317
|
+
|
|
1318
|
+
const productOperation = parseManifestOperation(productQuery);
|
|
1319
|
+
const cartOperation = parseManifestOperation(cartQuery);
|
|
1320
|
+
|
|
1321
|
+
const client = new ApolloClient({
|
|
1322
|
+
cache: new InMemoryCache(),
|
|
1323
|
+
manifest: mockApplicationManifest({
|
|
1324
|
+
operations: [productOperation, cartOperation],
|
|
1325
|
+
}),
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
using host = await mockMcpHost({
|
|
1329
|
+
hostContext: minimalHostContextWithToolName("GetProduct"),
|
|
1330
|
+
});
|
|
1331
|
+
host.onCleanup(() => client.stop());
|
|
1332
|
+
|
|
1333
|
+
const execute = vi.fn(() => ({
|
|
1334
|
+
structuredContent: {
|
|
1335
|
+
data: { cart: { id: "1", __typename: "Cart" } },
|
|
1336
|
+
},
|
|
1337
|
+
}));
|
|
1338
|
+
host.mockToolCall("execute", execute);
|
|
1339
|
+
|
|
1340
|
+
const connectPromise = client.connect();
|
|
1341
|
+
|
|
1342
|
+
const cartPromise = client.query({
|
|
1343
|
+
query: cartQuery,
|
|
1344
|
+
fetchPolicy: "network-only",
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
host.sendToolResult({
|
|
1348
|
+
structuredContent: {
|
|
1349
|
+
result: {
|
|
1350
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
});
|
|
1354
|
+
host.sendToolInput({ arguments: { id: "1" } });
|
|
1355
|
+
|
|
1356
|
+
await connectPromise;
|
|
1357
|
+
|
|
1358
|
+
await expect(cartPromise).resolves.toStrictEqual({
|
|
1359
|
+
data: { cart: { id: "1", __typename: "Cart" } },
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
await expect(
|
|
1363
|
+
client.query({
|
|
1364
|
+
query: productQuery,
|
|
1365
|
+
variables: { id: "1" },
|
|
1366
|
+
fetchPolicy: "network-only",
|
|
1367
|
+
})
|
|
1368
|
+
).resolves.toStrictEqual({
|
|
1369
|
+
data: { product: { id: "1", title: "Pen", __typename: "Product" } },
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
expect(execute).toHaveBeenCalledOnce();
|
|
1373
|
+
});
|
|
1374
|
+
|
|
903
1375
|
describe("watchQuery dev warnings", () => {
|
|
904
1376
|
const query = gql`
|
|
905
1377
|
query Products($category: String!, $page: Int!, $sortBy: String!)
|
|
@@ -1004,3 +1476,26 @@ describe("watchQuery dev warnings", () => {
|
|
|
1004
1476
|
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
1005
1477
|
});
|
|
1006
1478
|
});
|
|
1479
|
+
|
|
1480
|
+
async function setup({
|
|
1481
|
+
query,
|
|
1482
|
+
toolName,
|
|
1483
|
+
}: {
|
|
1484
|
+
query: DocumentNode;
|
|
1485
|
+
toolName?: string;
|
|
1486
|
+
}) {
|
|
1487
|
+
const operation = parseManifestOperation(query);
|
|
1488
|
+
const client = new ApolloClient({
|
|
1489
|
+
cache: new InMemoryCache(),
|
|
1490
|
+
manifest: mockApplicationManifest({ operations: [operation] }),
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
const host = await mockMcpHost({
|
|
1494
|
+
hostContext: minimalHostContextWithToolName(
|
|
1495
|
+
toolName ?? operation.tools[0].name
|
|
1496
|
+
),
|
|
1497
|
+
});
|
|
1498
|
+
host.onCleanup(() => client.stop());
|
|
1499
|
+
|
|
1500
|
+
return { client, host };
|
|
1501
|
+
}
|