@apollo/client-ai-apps 0.3.2 → 0.4.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.
Files changed (140) hide show
  1. package/.github/workflows/pr.yaml +52 -3
  2. package/.github/workflows/prep-release.yml +38 -0
  3. package/.github/workflows/release.yaml +8 -4
  4. package/.github/workflows/verify-changeset.yml +58 -0
  5. package/CHANGELOG.md +88 -0
  6. package/dist/core/ApolloClient.d.ts +3 -2
  7. package/dist/core/ApolloClient.d.ts.map +1 -0
  8. package/dist/core/ApolloClient.js +65 -0
  9. package/dist/core/ApolloClient.js.map +1 -0
  10. package/dist/index.d.ts +18 -18
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +16 -279
  13. package/dist/index.js.map +1 -0
  14. package/dist/link/ToolCallLink.d.ts +1 -0
  15. package/dist/link/ToolCallLink.d.ts.map +1 -0
  16. package/dist/link/ToolCallLink.js +39 -0
  17. package/dist/link/ToolCallLink.js.map +1 -0
  18. package/dist/react/ApolloProvider.d.ts +4 -3
  19. package/dist/react/ApolloProvider.d.ts.map +1 -0
  20. package/dist/react/ApolloProvider.js +30 -0
  21. package/dist/react/ApolloProvider.js.map +1 -0
  22. package/dist/react/context/ToolUseContext.d.ts +4 -3
  23. package/dist/react/context/ToolUseContext.d.ts.map +1 -0
  24. package/dist/react/context/ToolUseContext.js +11 -0
  25. package/dist/react/context/ToolUseContext.js.map +1 -0
  26. package/dist/react/hooks/useCallTool.d.ts +4 -0
  27. package/dist/react/hooks/useCallTool.d.ts.map +1 -0
  28. package/dist/react/hooks/useCallTool.js +5 -0
  29. package/dist/react/hooks/useCallTool.js.map +1 -0
  30. package/dist/react/hooks/useOpenAiGlobal.d.ts +2 -1
  31. package/dist/react/hooks/useOpenAiGlobal.d.ts.map +1 -0
  32. package/dist/react/hooks/useOpenAiGlobal.js +20 -0
  33. package/dist/react/hooks/useOpenAiGlobal.js.map +1 -0
  34. package/dist/react/hooks/useOpenExternal.d.ts +1 -0
  35. package/dist/react/hooks/useOpenExternal.d.ts.map +1 -0
  36. package/dist/react/hooks/useOpenExternal.js +5 -0
  37. package/dist/react/hooks/useOpenExternal.js.map +1 -0
  38. package/dist/react/hooks/useRequestDisplayMode.d.ts +2 -1
  39. package/dist/react/hooks/useRequestDisplayMode.d.ts.map +1 -0
  40. package/dist/react/hooks/useRequestDisplayMode.js +6 -0
  41. package/dist/react/hooks/useRequestDisplayMode.js.map +1 -0
  42. package/dist/react/hooks/useSendFollowUpMessage.d.ts +1 -0
  43. package/dist/react/hooks/useSendFollowUpMessage.d.ts.map +1 -0
  44. package/dist/react/hooks/useSendFollowUpMessage.js +8 -0
  45. package/dist/react/hooks/useSendFollowUpMessage.js.map +1 -0
  46. package/dist/react/hooks/useToolEffect.d.ts +1 -0
  47. package/dist/react/hooks/useToolEffect.d.ts.map +1 -0
  48. package/dist/react/hooks/useToolEffect.js +28 -0
  49. package/dist/react/hooks/useToolEffect.js.map +1 -0
  50. package/dist/react/hooks/useToolInput.d.ts +1 -0
  51. package/dist/react/hooks/useToolInput.d.ts.map +1 -0
  52. package/dist/react/hooks/useToolInput.js +6 -0
  53. package/dist/react/hooks/useToolInput.js.map +1 -0
  54. package/dist/react/hooks/useToolName.d.ts +1 -0
  55. package/dist/react/hooks/useToolName.d.ts.map +1 -0
  56. package/dist/react/hooks/useToolName.js +6 -0
  57. package/dist/react/hooks/useToolName.js.map +1 -0
  58. package/dist/react/hooks/useToolOutput.d.ts +2 -1
  59. package/dist/react/hooks/useToolOutput.d.ts.map +1 -0
  60. package/dist/react/hooks/useToolOutput.js +5 -0
  61. package/dist/react/hooks/useToolOutput.js.map +1 -0
  62. package/dist/react/hooks/useToolResponseMetadata.d.ts +2 -1
  63. package/dist/react/hooks/useToolResponseMetadata.d.ts.map +1 -0
  64. package/dist/react/hooks/useToolResponseMetadata.js +5 -0
  65. package/dist/react/hooks/useToolResponseMetadata.js.map +1 -0
  66. package/dist/react/hooks/useWidgetState.d.ts +3 -2
  67. package/dist/react/hooks/useWidgetState.d.ts.map +1 -0
  68. package/dist/react/hooks/useWidgetState.js +27 -0
  69. package/dist/react/hooks/useWidgetState.js.map +1 -0
  70. package/dist/types/application-manifest.d.ts +15 -0
  71. package/dist/types/application-manifest.d.ts.map +1 -0
  72. package/dist/types/application-manifest.js +2 -0
  73. package/dist/types/application-manifest.js.map +1 -0
  74. package/dist/types/openai.d.ts +1 -0
  75. package/dist/types/openai.d.ts.map +1 -0
  76. package/dist/types/openai.js +6 -0
  77. package/dist/types/openai.js.map +1 -0
  78. package/dist/vite/absolute_asset_imports_plugin.d.ts +1 -0
  79. package/dist/vite/absolute_asset_imports_plugin.d.ts.map +1 -0
  80. package/dist/vite/absolute_asset_imports_plugin.js +17 -0
  81. package/dist/vite/absolute_asset_imports_plugin.js.map +1 -0
  82. package/dist/vite/application_manifest_plugin.d.ts +2 -1
  83. package/dist/vite/application_manifest_plugin.d.ts.map +1 -0
  84. package/dist/vite/application_manifest_plugin.js +317 -0
  85. package/dist/vite/application_manifest_plugin.js.map +1 -0
  86. package/dist/vite/index.d.ts +3 -2
  87. package/dist/vite/index.d.ts.map +1 -0
  88. package/dist/vite/index.js +3 -307
  89. package/dist/vite/index.js.map +1 -0
  90. package/knope.toml +63 -0
  91. package/package.json +19 -11
  92. package/src/core/ApolloClient.ts +10 -5
  93. package/src/core/__tests__/ApolloClient.test.ts +33 -10
  94. package/src/index.ts +19 -18
  95. package/src/react/ApolloProvider.tsx +4 -3
  96. package/src/react/__tests__/ApolloProvider.test.tsx +3 -3
  97. package/src/react/context/ToolUseContext.tsx +2 -1
  98. package/src/react/hooks/__tests__/useCallTool.test.ts +1 -1
  99. package/src/react/hooks/__tests__/useOpenAiGlobal.test.ts +6 -6
  100. package/src/react/hooks/__tests__/useOpenExternal.test.tsx +2 -2
  101. package/src/react/hooks/__tests__/useRequestDisplayMode.test.ts +2 -2
  102. package/src/react/hooks/__tests__/useSendFollowUpMessage.test.ts +1 -1
  103. package/src/react/hooks/__tests__/useToolEffect.test.tsx +2 -2
  104. package/src/react/hooks/__tests__/useToolInput.test.ts +1 -1
  105. package/src/react/hooks/__tests__/useToolName.test.ts +1 -1
  106. package/src/react/hooks/__tests__/useToolOutput.test.tsx +2 -2
  107. package/src/react/hooks/__tests__/useToolResponseMetadata.test.tsx +2 -2
  108. package/src/react/hooks/__tests__/useWidgetState.test.tsx +2 -2
  109. package/src/react/hooks/useOpenAiGlobal.ts +2 -5
  110. package/src/react/hooks/useOpenExternal.ts +1 -1
  111. package/src/react/hooks/useRequestDisplayMode.ts +1 -1
  112. package/src/react/hooks/useToolEffect.tsx +3 -3
  113. package/src/react/hooks/useToolInput.ts +1 -1
  114. package/src/react/hooks/useToolName.ts +1 -1
  115. package/src/react/hooks/useToolOutput.ts +1 -1
  116. package/src/react/hooks/useToolResponseMetadata.ts +1 -1
  117. package/src/react/hooks/useWidgetState.ts +4 -3
  118. package/src/testing/internal/index.ts +2 -2
  119. package/src/testing/internal/matchers/index.ts +1 -1
  120. package/src/testing/internal/matchers/toRerender.ts +2 -2
  121. package/src/testing/internal/matchers/{index.d.ts → types.ts} +1 -1
  122. package/src/testing/internal/openai/dispatchStateChange.ts +1 -1
  123. package/src/testing/internal/openai/stubOpenAiGlobals.ts +6 -2
  124. package/src/types/application-manifest.ts +16 -0
  125. package/src/vite/__tests__/absolute_asset_imports_plugin.test.ts +2 -2
  126. package/src/vite/__tests__/application_manifest_plugin.test.ts +460 -240
  127. package/src/vite/application_manifest_plugin.ts +253 -93
  128. package/src/vite/index.ts +2 -2
  129. package/tsconfig.base.build.json +13 -0
  130. package/tsconfig.base.json +21 -0
  131. package/tsconfig.config.json +9 -0
  132. package/tsconfig.json +10 -0
  133. package/tsconfig.src.build.json +14 -0
  134. package/tsconfig.src.json +17 -0
  135. package/tsconfig.test.json +20 -0
  136. package/tsconfig.vite.build.json +6 -0
  137. package/tsconfig.vite.json +16 -0
  138. package/scripts/build-vite.mjs +0 -18
  139. package/scripts/build.mjs +0 -7
  140. package/scripts/shared.mjs +0 -9
@@ -1,8 +1,10 @@
1
- import { expect, test, vi, describe, beforeEach, Mock } from "vitest";
2
- import { ApplicationManifestPlugin } from "../application_manifest_plugin";
1
+ import type { Mock } from "vitest";
2
+ import { expect, test, vi, describe, beforeEach } from "vitest";
3
+ import { ApplicationManifestPlugin } from "../application_manifest_plugin.js";
3
4
  import fs from "fs";
4
5
  import * as glob from "glob";
5
6
  import path from "path";
7
+ import type { ManifestWidgetSettings } from "../../types/application-manifest.js";
6
8
 
7
9
  const root = process.cwd();
8
10
 
@@ -31,12 +33,7 @@ vi.mock(import("path"), async (importOriginal) => {
31
33
  };
32
34
  });
33
35
 
34
- vi.mock(import("glob"), async (importOriginal) => {
35
- const actual = await importOriginal();
36
- return {
37
- glob: vi.fn(),
38
- };
39
- });
36
+ vi.mock(import("glob"));
40
37
 
41
38
  beforeEach(() => {
42
39
  vi.clearAllMocks();
@@ -44,19 +41,39 @@ beforeEach(() => {
44
41
 
45
42
  describe("buildStart", () => {
46
43
  test("Should write to dev application manifest file when using a serve command", async () => {
47
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
48
- if (path === "package.json") {
49
- return JSON.stringify({});
50
- } else if (path === root + "/my-component.tsx") {
51
- return `
52
- const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
53
- name: "doStuff",
54
- type: "boolean",
55
- description: "Should we do stuff?"
56
- }]) { helloWorld(name: $name) }\`;
57
- `;
58
- }
44
+ mockReadFile({
45
+ "package.json": JSON.stringify({
46
+ labels: {
47
+ toolInvocation: {
48
+ invoking: "Testing global...",
49
+ invoked: "Tested global!",
50
+ },
51
+ },
52
+ widgetSettings: {
53
+ description: "Test",
54
+ domain: "https://example.com",
55
+ prefersBorder: true,
56
+ } satisfies ManifestWidgetSettings,
57
+ }),
58
+ [`${root}/my-component.tsx`]: `
59
+ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(
60
+ name: "hello-world",
61
+ description: "This is an awesome tool!",
62
+ extraInputs: [{
63
+ name: "doStuff",
64
+ type: "boolean",
65
+ description: "Should we do stuff?"
66
+ }],
67
+ labels: {
68
+ toolInvocation: {
69
+ invoking: "Testing tool...",
70
+ invoked: "Tested tool!"
71
+ }
72
+ }
73
+ ) { helloWorld(name: $name) }\`;
74
+ `,
59
75
  });
76
+
60
77
  vi.spyOn(glob, "glob").mockImplementation(() =>
61
78
  Promise.resolve(["my-component.tsx"])
62
79
  );
@@ -81,10 +98,16 @@ describe("buildStart", () => {
81
98
  {
82
99
  "csp": {
83
100
  "connectDomains": [],
101
+ "frameDomains": [],
102
+ "redirectDomains": [],
84
103
  "resourceDomains": [],
85
104
  },
86
105
  "format": "apollo-ai-app-manifest",
87
106
  "hash": "abc",
107
+ "labels": {
108
+ "toolInvocation/invoked": "Tested global!",
109
+ "toolInvocation/invoking": "Testing global...",
110
+ },
88
111
  "operations": [
89
112
  {
90
113
  "body": "query HelloWorldQuery($name: string!) {
@@ -103,6 +126,10 @@ describe("buildStart", () => {
103
126
  "type": "boolean",
104
127
  },
105
128
  ],
129
+ "labels": {
130
+ "toolInvocation/invoked": "Tested tool!",
131
+ "toolInvocation/invoking": "Testing tool...",
132
+ },
106
133
  "name": "hello-world",
107
134
  },
108
135
  ],
@@ -114,19 +141,21 @@ describe("buildStart", () => {
114
141
  ],
115
142
  "resource": "http://localhost:undefined",
116
143
  "version": "1",
144
+ "widgetSettings": {
145
+ "description": "Test",
146
+ "domain": "https://example.com",
147
+ "prefersBorder": true,
148
+ },
117
149
  }
118
150
  `);
119
151
  });
120
152
 
121
153
  test("Should NOT write to dev application manifest file when using a build command", async () => {
122
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
123
- if (path === "package.json") {
124
- return JSON.stringify({});
125
- } else if (path === "my-component.tsx") {
126
- return `
127
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
128
- `;
129
- }
154
+ mockReadFile({
155
+ "package.json": JSON.stringify({}),
156
+ "my-component.tsx": `
157
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
158
+ `,
130
159
  });
131
160
  vi.spyOn(glob, "glob").mockImplementation(() =>
132
161
  Promise.resolve(["my-component.tsx"])
@@ -142,14 +171,11 @@ describe("buildStart", () => {
142
171
  });
143
172
 
144
173
  test("Should not process files that do not contain gql tags", async () => {
145
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
146
- if (path === "package.json") {
147
- return JSON.stringify({});
148
- } else if (path === root + "/my-component.tsx") {
149
- return `
150
- const MY_QUERY = \`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
151
- `;
152
- }
174
+ mockReadFile({
175
+ "package.json": JSON.stringify({}),
176
+ [`${root}/my-component.tsx`]: `
177
+ const MY_QUERY = \`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
178
+ `,
153
179
  });
154
180
  vi.spyOn(glob, "glob").mockImplementation(() =>
155
181
  Promise.resolve(["my-component.tsx"])
@@ -175,6 +201,8 @@ describe("buildStart", () => {
175
201
  {
176
202
  "csp": {
177
203
  "connectDomains": [],
204
+ "frameDomains": [],
205
+ "redirectDomains": [],
178
206
  "resourceDomains": [],
179
207
  },
180
208
  "format": "apollo-ai-app-manifest",
@@ -187,14 +215,11 @@ describe("buildStart", () => {
187
215
  });
188
216
 
189
217
  test("Should capture queries when writing to manifest file", async () => {
190
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
191
- if (path === "package.json") {
192
- return JSON.stringify({});
193
- } else if (path === root + "/my-component.tsx") {
194
- return `
195
- const MY_QUERY = gql\`query HelloWorldQuery { helloWorld }\`;
196
- `;
197
- }
218
+ mockReadFile({
219
+ "package.json": JSON.stringify({}),
220
+ [`${root}/my-component.tsx`]: `
221
+ const MY_QUERY = gql\`query HelloWorldQuery { helloWorld }\`;
222
+ `,
198
223
  });
199
224
  vi.spyOn(glob, "glob").mockImplementation(() =>
200
225
  Promise.resolve(["my-component.tsx"])
@@ -220,6 +245,8 @@ describe("buildStart", () => {
220
245
  {
221
246
  "csp": {
222
247
  "connectDomains": [],
248
+ "frameDomains": [],
249
+ "redirectDomains": [],
223
250
  "resourceDomains": [],
224
251
  },
225
252
  "format": "apollo-ai-app-manifest",
@@ -244,14 +271,11 @@ describe("buildStart", () => {
244
271
  });
245
272
 
246
273
  test("Should capture queries as prefetch when query is marked with @prefetch directive", async () => {
247
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
248
- if (path === "package.json") {
249
- return JSON.stringify({});
250
- } else if (path === root + "/my-component.tsx") {
251
- return `
252
- const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
253
- `;
254
- }
274
+ mockReadFile({
275
+ "package.json": JSON.stringify({}),
276
+ [`${root}/my-component.tsx`]: `
277
+ const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
278
+ `,
255
279
  });
256
280
  vi.spyOn(glob, "glob").mockImplementation(() =>
257
281
  Promise.resolve(["my-component.tsx"])
@@ -277,6 +301,8 @@ describe("buildStart", () => {
277
301
  {
278
302
  "csp": {
279
303
  "connectDomains": [],
304
+ "frameDomains": [],
305
+ "redirectDomains": [],
280
306
  "resourceDomains": [],
281
307
  },
282
308
  "format": "apollo-ai-app-manifest",
@@ -302,15 +328,12 @@ describe("buildStart", () => {
302
328
  });
303
329
 
304
330
  test("Should error when multiple operations are marked with @prefetch", async () => {
305
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
306
- if (path === "package.json") {
307
- return JSON.stringify({});
308
- } else if (path === "my-component.tsx") {
309
- return `
310
- const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
311
- const MY_QUERY2 = gql\`query HelloWorldQuery2 @prefetch { helloWorld }\`;
312
- `;
313
- }
331
+ mockReadFile({
332
+ "package.json": JSON.stringify({}),
333
+ "my-component.tsx": `
334
+ const MY_QUERY = gql\`query HelloWorldQuery @prefetch { helloWorld }\`;
335
+ const MY_QUERY2 = gql\`query HelloWorldQuery2 @prefetch { helloWorld }\`;
336
+ `,
314
337
  });
315
338
  vi.spyOn(glob, "glob").mockImplementation(() =>
316
339
  Promise.resolve(["my-component.tsx"])
@@ -328,14 +351,11 @@ describe("buildStart", () => {
328
351
  });
329
352
 
330
353
  test("Should capture mutations when writing to manifest file", async () => {
331
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
332
- if (path === "package.json") {
333
- return JSON.stringify({});
334
- } else if (path === root + "/my-component.tsx") {
335
- return `
336
- const MY_QUERY = gql\`mutation HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
337
- `;
338
- }
354
+ mockReadFile({
355
+ "package.json": JSON.stringify({}),
356
+ [`${root}/my-component.tsx`]: `
357
+ const MY_QUERY = gql\`mutation HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
358
+ `,
339
359
  });
340
360
  vi.spyOn(glob, "glob").mockImplementation(() =>
341
361
  Promise.resolve(["my-component.tsx"])
@@ -361,6 +381,8 @@ describe("buildStart", () => {
361
381
  {
362
382
  "csp": {
363
383
  "connectDomains": [],
384
+ "frameDomains": [],
385
+ "redirectDomains": [],
364
386
  "resourceDomains": [],
365
387
  },
366
388
  "format": "apollo-ai-app-manifest",
@@ -390,14 +412,11 @@ describe("buildStart", () => {
390
412
  });
391
413
 
392
414
  test("Should throw error when a subscription operation type is discovered", async () => {
393
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
394
- if (path === "package.json") {
395
- return JSON.stringify({});
396
- } else if (path === "my-component.tsx") {
397
- return `
398
- const MY_QUERY = gql\`subscription HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
399
- `;
400
- }
415
+ mockReadFile({
416
+ "package.json": JSON.stringify({}),
417
+ "my-component.tsx": `
418
+ const MY_QUERY = gql\`subscription HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
419
+ `,
401
420
  });
402
421
  vi.spyOn(glob, "glob").mockImplementation(() =>
403
422
  Promise.resolve(["my-component.tsx"])
@@ -416,18 +435,15 @@ describe("buildStart", () => {
416
435
  });
417
436
 
418
437
  test("Should use custom entry point when in serve mode and provided in package.json", async () => {
419
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
420
- if (path === "package.json") {
421
- return JSON.stringify({
422
- entry: {
423
- staging: "http://staging.awesome.com",
424
- },
425
- });
426
- } else if (path === "my-component.tsx") {
427
- return `
428
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
429
- `;
430
- }
438
+ mockReadFile({
439
+ "package.json": JSON.stringify({
440
+ entry: {
441
+ staging: "http://staging.awesome.com",
442
+ },
443
+ }),
444
+ "my-component.tsx": `
445
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
446
+ `,
431
447
  });
432
448
  vi.spyOn(glob, "glob").mockImplementation(() =>
433
449
  Promise.resolve(["my-component.tsx"])
@@ -444,21 +460,18 @@ describe("buildStart", () => {
444
460
  });
445
461
  await plugin.buildStart();
446
462
 
447
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
463
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
448
464
  let contentObj = JSON.parse(content);
449
465
 
450
466
  expect(contentObj.resource).toBe("http://staging.awesome.com");
451
467
  });
452
468
 
453
469
  test("Should use https when enabled in server config", async () => {
454
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
455
- if (path === "package.json") {
456
- return JSON.stringify({});
457
- } else if (path === "my-component.tsx") {
458
- return `
459
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
460
- `;
461
- }
470
+ mockReadFile({
471
+ "package.json": JSON.stringify({}),
472
+ "my-component.tsx": `
473
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
474
+ `,
462
475
  });
463
476
  vi.spyOn(glob, "glob").mockImplementation(() =>
464
477
  Promise.resolve(["my-component.tsx"])
@@ -474,21 +487,18 @@ describe("buildStart", () => {
474
487
  });
475
488
  await plugin.buildStart();
476
489
 
477
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
490
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
478
491
  let contentObj = JSON.parse(content);
479
492
 
480
493
  expect(contentObj.resource).toBe("https://localhost:5678");
481
494
  });
482
495
 
483
496
  test("Should use custom host when specified in server config", async () => {
484
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
485
- if (path === "package.json") {
486
- return JSON.stringify({});
487
- } else if (path === "my-component.tsx") {
488
- return `
489
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
490
- `;
491
- }
497
+ mockReadFile({
498
+ "package.json": JSON.stringify({}),
499
+ "my-component.tsx": `
500
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
501
+ `,
492
502
  });
493
503
  vi.spyOn(glob, "glob").mockImplementation(() =>
494
504
  Promise.resolve(["my-component.tsx"])
@@ -504,21 +514,18 @@ describe("buildStart", () => {
504
514
  });
505
515
  await plugin.buildStart();
506
516
 
507
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
517
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
508
518
  let contentObj = JSON.parse(content);
509
519
 
510
520
  expect(contentObj.resource).toBe("http://awesome.com:5678");
511
521
  });
512
522
 
513
523
  test("Should error when tool name is not provided", async () => {
514
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
515
- if (path === "package.json") {
516
- return JSON.stringify({});
517
- } else if (path === "my-component.tsx") {
518
- return `
519
- const MY_QUERY = gql\`query HelloWorldQuery @tool { helloWorld }\`;
520
- `;
521
- }
524
+ mockReadFile({
525
+ "package.json": JSON.stringify({}),
526
+ "my-component.tsx": `
527
+ const MY_QUERY = gql\`query HelloWorldQuery @tool { helloWorld }\`;
528
+ `,
522
529
  });
523
530
  vi.spyOn(glob, "glob").mockImplementation(() =>
524
531
  Promise.resolve(["my-component.tsx"])
@@ -537,14 +544,11 @@ describe("buildStart", () => {
537
544
  });
538
545
 
539
546
  test("Should error when tool description is not provided", async () => {
540
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
541
- if (path === "package.json") {
542
- return JSON.stringify({});
543
- } else if (path === "my-component.tsx") {
544
- return `
545
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world") { helloWorld }\`;
546
- `;
547
- }
547
+ mockReadFile({
548
+ "package.json": JSON.stringify({}),
549
+ "my-component.tsx": `
550
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world") { helloWorld }\`;
551
+ `,
548
552
  });
549
553
  vi.spyOn(glob, "glob").mockImplementation(() =>
550
554
  Promise.resolve(["my-component.tsx"])
@@ -563,14 +567,11 @@ describe("buildStart", () => {
563
567
  });
564
568
 
565
569
  test("Should error when tool name contains spaces", async () => {
566
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
567
- if (path === "package.json") {
568
- return JSON.stringify({});
569
- } else if (path === "my-component.tsx") {
570
- return `
571
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello world", description: "A tool") { helloWorld }\`;
572
- `;
573
- }
570
+ mockReadFile({
571
+ "package.json": JSON.stringify({}),
572
+ "my-component.tsx": `
573
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello world", description: "A tool") { helloWorld }\`;
574
+ `,
574
575
  });
575
576
  vi.spyOn(glob, "glob").mockImplementation(() =>
576
577
  Promise.resolve(["my-component.tsx"])
@@ -589,14 +590,11 @@ describe("buildStart", () => {
589
590
  });
590
591
 
591
592
  test("Should error when tool name is not a string", async () => {
592
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
593
- if (path === "package.json") {
594
- return JSON.stringify({});
595
- } else if (path === "my-component.tsx") {
596
- return `
597
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: true) { helloWorld }\`;
598
- `;
599
- }
593
+ mockReadFile({
594
+ "package.json": JSON.stringify({}),
595
+ "my-component.tsx": `
596
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: true) { helloWorld }\`;
597
+ `,
600
598
  });
601
599
  vi.spyOn(glob, "glob").mockImplementation(() =>
602
600
  Promise.resolve(["my-component.tsx"])
@@ -615,14 +613,11 @@ describe("buildStart", () => {
615
613
  });
616
614
 
617
615
  test("Should error when tool description is not a string", async () => {
618
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
619
- if (path === "package.json") {
620
- return JSON.stringify({});
621
- } else if (path === "my-component.tsx") {
622
- return `
623
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: false) { helloWorld }\`;
624
- `;
625
- }
616
+ mockReadFile({
617
+ "package.json": JSON.stringify({}),
618
+ "my-component.tsx": `
619
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: false) { helloWorld }\`;
620
+ `,
626
621
  });
627
622
  vi.spyOn(glob, "glob").mockImplementation(() =>
628
623
  Promise.resolve(["my-component.tsx"])
@@ -641,14 +636,11 @@ describe("buildStart", () => {
641
636
  });
642
637
 
643
638
  test("Should error when extraInputs is not an array", async () => {
644
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
645
- if (path === "package.json") {
646
- return JSON.stringify({});
647
- } else if (path === "my-component.tsx") {
648
- return `
649
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: false ) { helloWorld }\`;
650
- `;
651
- }
639
+ mockReadFile({
640
+ "package.json": JSON.stringify({}),
641
+ "my-component.tsx": `
642
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: false ) { helloWorld }\`;
643
+ `,
652
644
  });
653
645
  vi.spyOn(glob, "glob").mockImplementation(() =>
654
646
  Promise.resolve(["my-component.tsx"])
@@ -666,17 +658,241 @@ describe("buildStart", () => {
666
658
  );
667
659
  });
668
660
 
661
+ test("Should error when widgetSettings.prefersBorder is not a boolean", async () => {
662
+ mockReadFile({
663
+ "package.json": JSON.stringify({
664
+ widgetSettings: {
665
+ prefersBorder: "test",
666
+ },
667
+ }),
668
+ "my-component.tsx": `
669
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
670
+ `,
671
+ });
672
+ vi.spyOn(glob, "glob").mockImplementation(() =>
673
+ Promise.resolve(["my-component.tsx"])
674
+ );
675
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
676
+ vi.spyOn(fs, "writeFileSync");
677
+
678
+ const plugin = ApplicationManifestPlugin();
679
+ plugin.configResolved({ command: "serve", server: {} });
680
+
681
+ await expect(
682
+ async () => await plugin.buildStart()
683
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
684
+ `[Error: Expected 'widgetSettings.prefersBorder' to be of type 'boolean' but found 'string' instead.]`
685
+ );
686
+ });
687
+
688
+ test("Should error when widgetSettings.description is not a string", async () => {
689
+ mockReadFile({
690
+ "package.json": JSON.stringify({
691
+ widgetSettings: {
692
+ description: true,
693
+ },
694
+ }),
695
+ "my-component.tsx": `
696
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
697
+ `,
698
+ });
699
+ vi.spyOn(glob, "glob").mockImplementation(() =>
700
+ Promise.resolve(["my-component.tsx"])
701
+ );
702
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
703
+ vi.spyOn(fs, "writeFileSync");
704
+
705
+ const plugin = ApplicationManifestPlugin();
706
+ plugin.configResolved({ command: "serve", server: {} });
707
+
708
+ await expect(
709
+ async () => await plugin.buildStart()
710
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
711
+ `[Error: Expected 'widgetSettings.description' to be of type 'string' but found 'boolean' instead.]`
712
+ );
713
+ });
714
+
715
+ test("Should error when widgetSettings.domain is not a string", async () => {
716
+ mockReadFile({
717
+ "package.json": JSON.stringify({
718
+ widgetSettings: {
719
+ domain: true,
720
+ },
721
+ }),
722
+ "my-component.tsx": `
723
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
724
+ `,
725
+ });
726
+ vi.spyOn(glob, "glob").mockImplementation(() =>
727
+ Promise.resolve(["my-component.tsx"])
728
+ );
729
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
730
+ vi.spyOn(fs, "writeFileSync");
731
+
732
+ const plugin = ApplicationManifestPlugin();
733
+ plugin.configResolved({ command: "serve", server: {} });
734
+
735
+ await expect(
736
+ async () => await plugin.buildStart()
737
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
738
+ `[Error: Expected 'widgetSettings.domain' to be of type 'string' but found 'boolean' instead.]`
739
+ );
740
+ });
741
+
742
+ test("Should allow empty widgetSettings value", async () => {
743
+ mockReadFile({
744
+ "package.json": JSON.stringify({
745
+ widgetSettings: {},
746
+ }),
747
+ "my-component.tsx": `
748
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
749
+ `,
750
+ });
751
+ vi.spyOn(glob, "glob").mockImplementation(() =>
752
+ Promise.resolve(["my-component.tsx"])
753
+ );
754
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
755
+ vi.spyOn(fs, "writeFileSync");
756
+
757
+ const plugin = ApplicationManifestPlugin();
758
+ plugin.configResolved({ command: "serve", server: {}, build: {} });
759
+
760
+ await expect(plugin.buildStart()).resolves.toBeUndefined();
761
+ });
762
+
763
+ test("Should error when labels.toolInvocation.invoking in package.json is not a string", async () => {
764
+ mockReadFile({
765
+ "package.json": JSON.stringify({
766
+ labels: {
767
+ toolInvocation: {
768
+ invoking: true,
769
+ },
770
+ },
771
+ }),
772
+ "my-component.tsx": `
773
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
774
+ `,
775
+ });
776
+ vi.spyOn(glob, "glob").mockImplementation(() =>
777
+ Promise.resolve(["my-component.tsx"])
778
+ );
779
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
780
+ vi.spyOn(fs, "writeFileSync");
781
+
782
+ const plugin = ApplicationManifestPlugin();
783
+ plugin.configResolved({ command: "serve", server: {} });
784
+
785
+ await expect(
786
+ async () => await plugin.buildStart()
787
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
788
+ `[Error: Expected 'labels.toolInvocation.invoking' to be of type 'string' but found 'boolean' instead.]`
789
+ );
790
+ });
791
+
792
+ test("Should error when labels.toolInvocation.invoking in @tool is not a string", async () => {
793
+ mockReadFile({
794
+ "package.json": JSON.stringify({}),
795
+ "my-component.tsx": `
796
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test", labels: { toolInvocation: { invoking: true } }) { helloWorld }\`;
797
+ `,
798
+ });
799
+ vi.spyOn(glob, "glob").mockImplementation(() =>
800
+ Promise.resolve(["my-component.tsx"])
801
+ );
802
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
803
+ vi.spyOn(fs, "writeFileSync");
804
+
805
+ const plugin = ApplicationManifestPlugin();
806
+ plugin.configResolved({ command: "serve", server: {} });
807
+
808
+ await expect(
809
+ async () => await plugin.buildStart()
810
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
811
+ `[Error: Expected 'labels.toolInvocation.invoking' to be of type 'string' but found 'boolean' instead.]`
812
+ );
813
+ });
814
+
815
+ test("Should error when labels.toolInvocation.invoked in package.json is not a string", async () => {
816
+ mockReadFile({
817
+ "package.json": JSON.stringify({
818
+ labels: {
819
+ toolInvocation: {
820
+ invoked: true,
821
+ },
822
+ },
823
+ }),
824
+ "my-component.tsx": `
825
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test") { helloWorld }\`;
826
+ `,
827
+ });
828
+ vi.spyOn(glob, "glob").mockImplementation(() =>
829
+ Promise.resolve(["my-component.tsx"])
830
+ );
831
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
832
+ vi.spyOn(fs, "writeFileSync");
833
+
834
+ const plugin = ApplicationManifestPlugin();
835
+ plugin.configResolved({ command: "serve", server: {} });
836
+
837
+ await expect(
838
+ async () => await plugin.buildStart()
839
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
840
+ `[Error: Expected 'labels.toolInvocation.invoked' to be of type 'string' but found 'boolean' instead.]`
841
+ );
842
+ });
843
+
844
+ test("Should error when labels.toolInvocation.invoked in @tool is not a string", async () => {
845
+ mockReadFile({
846
+ "package.json": JSON.stringify({}),
847
+ "my-component.tsx": `
848
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test", labels: { toolInvocation: { invoked: true } }) { helloWorld }\`;
849
+ `,
850
+ });
851
+ vi.spyOn(glob, "glob").mockImplementation(() =>
852
+ Promise.resolve(["my-component.tsx"])
853
+ );
854
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
855
+ vi.spyOn(fs, "writeFileSync");
856
+
857
+ const plugin = ApplicationManifestPlugin();
858
+ plugin.configResolved({ command: "serve", server: {} });
859
+
860
+ await expect(
861
+ async () => await plugin.buildStart()
862
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
863
+ `[Error: Expected 'labels.toolInvocation.invoked' to be of type 'string' but found 'boolean' instead.]`
864
+ );
865
+ });
866
+
867
+ test("Should allow empty labels value", async () => {
868
+ mockReadFile({
869
+ "package.json": JSON.stringify({
870
+ labels: {},
871
+ }),
872
+ "my-component.tsx": `
873
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "test", description: "Test", labels: {}) { helloWorld }\`;
874
+ `,
875
+ });
876
+ vi.spyOn(glob, "glob").mockImplementation(() =>
877
+ Promise.resolve(["my-component.tsx"])
878
+ );
879
+ vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
880
+ vi.spyOn(fs, "writeFileSync");
881
+
882
+ const plugin = ApplicationManifestPlugin();
883
+ plugin.configResolved({ command: "serve", server: {}, build: {} });
884
+
885
+ await expect(plugin.buildStart()).resolves.toBeUndefined();
886
+ });
887
+
669
888
  test("Should error when an unknown type is discovered", async () => {
670
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
671
- if (path === "package.json") {
672
- return JSON.stringify({});
673
- } else if (path === "my-component.tsx") {
674
- return `
675
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: [{
676
- name: 3.1
677
- }] ) { helloWorld }\`;
678
- `;
679
- }
889
+ mockReadFile({
890
+ "package.json": JSON.stringify({}),
891
+ "my-component.tsx": `
892
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "hello", extraInputs: [{
893
+ name: 3.1
894
+ }] ) { helloWorld }\`;
895
+ `,
680
896
  });
681
897
  vi.spyOn(glob, "glob").mockImplementation(() =>
682
898
  Promise.resolve(["my-component.tsx"])
@@ -695,25 +911,22 @@ describe("buildStart", () => {
695
911
  });
696
912
 
697
913
  test("Should order operations and fragments when generating normalized operation", async () => {
698
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
699
- if (path === "package.json") {
700
- return JSON.stringify({});
701
- } else if (path === root + "/my-component.tsx") {
702
- return `
703
- const MY_QUERY = gql\`
704
- fragment A on User { firstName }
705
- fragment B on User { lastName }
706
- query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") {
707
- helloWorld {
708
- ...B
709
- ...A
710
- ...C
711
- }
712
-
713
- fragment C on User { middleName }
714
- }\`;
715
- `;
716
- }
914
+ mockReadFile({
915
+ "package.json": JSON.stringify({}),
916
+ [`${root}/my-component.tsx`]: `
917
+ const MY_QUERY = gql\`
918
+ fragment A on User { firstName }
919
+ fragment B on User { lastName }
920
+ query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") {
921
+ helloWorld {
922
+ ...B
923
+ ...A
924
+ ...C
925
+ }
926
+
927
+ fragment C on User { middleName }
928
+ }\`;
929
+ `,
717
930
  });
718
931
  vi.spyOn(glob, "glob").mockImplementation(() =>
719
932
  Promise.resolve(["my-component.tsx"])
@@ -739,6 +952,8 @@ describe("buildStart", () => {
739
952
  {
740
953
  "csp": {
741
954
  "connectDomains": [],
955
+ "frameDomains": [],
956
+ "redirectDomains": [],
742
957
  "resourceDomains": [],
743
958
  },
744
959
  "format": "apollo-ai-app-manifest",
@@ -792,18 +1007,15 @@ describe("buildStart", () => {
792
1007
 
793
1008
  describe("writeBundle", () => {
794
1009
  test("Should use custom entry point when in build mode and provided in package.json", async () => {
795
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
796
- if (path === "package.json") {
797
- return JSON.stringify({
798
- entry: {
799
- staging: "http://staging.awesome.com",
800
- },
801
- });
802
- } else if (path === "my-component.tsx") {
803
- return `
804
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
805
- `;
806
- }
1010
+ mockReadFile({
1011
+ "package.json": JSON.stringify({
1012
+ entry: {
1013
+ staging: "http://staging.awesome.com",
1014
+ },
1015
+ }),
1016
+ "my-component.tsx": `
1017
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
1018
+ `,
807
1019
  });
808
1020
  vi.spyOn(glob, "glob").mockImplementation(() =>
809
1021
  Promise.resolve(["my-component.tsx"])
@@ -822,21 +1034,18 @@ describe("writeBundle", () => {
822
1034
  await plugin.buildStart();
823
1035
  await plugin.writeBundle();
824
1036
 
825
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
1037
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
826
1038
  let contentObj = JSON.parse(content);
827
1039
 
828
1040
  expect(contentObj.resource).toBe("http://staging.awesome.com");
829
1041
  });
830
1042
 
831
1043
  test("Should use index.html when in build production and not provided in package.json", async () => {
832
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
833
- if (path === "package.json") {
834
- return JSON.stringify({});
835
- } else if (path === "my-component.tsx") {
836
- return `
837
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
838
- `;
839
- }
1044
+ mockReadFile({
1045
+ "package.json": JSON.stringify({}),
1046
+ "my-component.tsx": `
1047
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
1048
+ `,
840
1049
  });
841
1050
  vi.spyOn(glob, "glob").mockImplementation(() =>
842
1051
  Promise.resolve(["my-component.tsx"])
@@ -855,21 +1064,18 @@ describe("writeBundle", () => {
855
1064
  await plugin.buildStart();
856
1065
  await plugin.writeBundle();
857
1066
 
858
- let [file, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
1067
+ let [, content] = (fs.writeFileSync as unknown as Mock).mock.calls[0];
859
1068
  let contentObj = JSON.parse(content);
860
1069
 
861
1070
  expect(contentObj.resource).toBe("index.html");
862
1071
  });
863
1072
 
864
1073
  test("Should throw an error when in build mode and using a mode that is not production and not provided in package.json", async () => {
865
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
866
- if (path === "package.json") {
867
- return JSON.stringify({});
868
- } else if (path === "my-component.tsx") {
869
- return `
870
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
871
- `;
872
- }
1074
+ mockReadFile({
1075
+ "package.json": JSON.stringify({}),
1076
+ "my-component.tsx": `
1077
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
1078
+ `,
873
1079
  });
874
1080
  vi.spyOn(glob, "glob").mockImplementation(() =>
875
1081
  Promise.resolve(["my-component.tsx"])
@@ -895,14 +1101,11 @@ describe("writeBundle", () => {
895
1101
  });
896
1102
 
897
1103
  test("Should always write to both locations when running in build mode", async () => {
898
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
899
- if (path === "package.json") {
900
- return JSON.stringify({});
901
- } else if (path === "my-component.tsx") {
902
- return `
903
- const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
904
- `;
905
- }
1104
+ mockReadFile({
1105
+ "package.json": JSON.stringify({}),
1106
+ "my-component.tsx": `
1107
+ const MY_QUERY = gql\`query HelloWorldQuery @tool(name: "hello-world", description: "This is an awesome tool!") { helloWorld }\`;
1108
+ `,
906
1109
  });
907
1110
  vi.spyOn(glob, "glob").mockImplementation(() =>
908
1111
  Promise.resolve(["my-component.tsx"])
@@ -927,18 +1130,15 @@ describe("writeBundle", () => {
927
1130
 
928
1131
  describe("configureServer", () => {
929
1132
  test("Should write to manifest file when package.json or file is updated", async () => {
930
- vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
931
- if (path === "package.json") {
932
- return JSON.stringify({});
933
- } else if (path === "my-component.tsx") {
934
- return `
935
- const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
936
- name: "doStuff",
937
- type: "boolean",
938
- description: "Should we do stuff?"
939
- }]) { helloWorld(name: $name) }\`;
940
- `;
941
- }
1133
+ mockReadFile({
1134
+ "package.json": JSON.stringify({}),
1135
+ "my-component.tsx": `
1136
+ const MY_QUERY = gql\`query HelloWorldQuery($name: string!) @tool(name: "hello-world", description: "This is an awesome tool!", extraInputs: [{
1137
+ name: "doStuff",
1138
+ type: "boolean",
1139
+ description: "Should we do stuff?"
1140
+ }]) { helloWorld(name: $name) }\`;
1141
+ `,
942
1142
  });
943
1143
  vi.spyOn(glob, "glob").mockImplementation(() =>
944
1144
  Promise.resolve(["my-component.tsx"])
@@ -946,16 +1146,18 @@ describe("configureServer", () => {
946
1146
  vi.spyOn(path, "resolve").mockImplementation((_, file) => file);
947
1147
  vi.spyOn(fs, "writeFileSync");
948
1148
 
1149
+ let _callbacks: Function[] = [];
1150
+
949
1151
  const server = {
950
1152
  watcher: {
951
1153
  init: () => {
952
- this._callbacks = [];
1154
+ _callbacks = [];
953
1155
  },
954
1156
  on: (_event: string, callback: Function) => {
955
- this._callbacks.push(callback);
1157
+ _callbacks.push(callback);
956
1158
  },
957
- trigger: async (file) => {
958
- for (const callback of this._callbacks) {
1159
+ trigger: async (file: string) => {
1160
+ for (const callback of _callbacks) {
959
1161
  await callback(file);
960
1162
  }
961
1163
  },
@@ -970,10 +1172,28 @@ describe("configureServer", () => {
970
1172
  build: { outDir: "/dist" },
971
1173
  });
972
1174
  await plugin.buildStart();
973
- await plugin.configureServer(server);
1175
+ plugin.configureServer(server);
974
1176
  await server.watcher.trigger("package.json");
975
1177
  await server.watcher.trigger("my-component.tsx");
976
1178
 
977
1179
  expect(fs.writeFileSync).toBeCalledTimes(6);
978
1180
  });
979
1181
  });
1182
+
1183
+ type FilePath = string;
1184
+
1185
+ function mockReadFile(mocks: Record<FilePath, string | (() => string)>) {
1186
+ vi.spyOn(fs, "readFileSync").mockImplementation((path) => {
1187
+ const mock = mocks[path.toString()];
1188
+
1189
+ if (!mock) {
1190
+ throw new Error(`No matched mock for path '${path}'`);
1191
+ }
1192
+
1193
+ if (typeof mock === "function") {
1194
+ return mock();
1195
+ }
1196
+
1197
+ return mock;
1198
+ });
1199
+ }