@zapier/zapier-sdk 0.8.3 → 0.10.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 (222) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +17 -40
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +14 -0
  5. package/dist/api/debug.d.ts +1 -0
  6. package/dist/api/debug.d.ts.map +1 -1
  7. package/dist/api/debug.js +42 -1
  8. package/dist/api/debug.test.d.ts +2 -0
  9. package/dist/api/debug.test.d.ts.map +1 -0
  10. package/dist/api/debug.test.js +59 -0
  11. package/dist/api/schemas.d.ts +451 -251
  12. package/dist/api/schemas.d.ts.map +1 -1
  13. package/dist/api/schemas.js +51 -29
  14. package/dist/index.cjs +1149 -751
  15. package/dist/index.d.mts +2359 -2161
  16. package/dist/index.d.ts +3 -5
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -4
  19. package/dist/index.mjs +1143 -743
  20. package/dist/plugins/apps/index.d.ts +4 -0
  21. package/dist/plugins/apps/index.d.ts.map +1 -1
  22. package/dist/plugins/findFirstAuthentication/index.d.ts +1 -1
  23. package/dist/plugins/findFirstAuthentication/index.d.ts.map +1 -1
  24. package/dist/plugins/findFirstAuthentication/index.js +9 -1
  25. package/dist/plugins/findFirstAuthentication/index.test.js +3 -4
  26. package/dist/plugins/findFirstAuthentication/schemas.d.ts +5 -3
  27. package/dist/plugins/findFirstAuthentication/schemas.d.ts.map +1 -1
  28. package/dist/plugins/findUniqueAuthentication/index.d.ts.map +1 -1
  29. package/dist/plugins/findUniqueAuthentication/index.js +4 -0
  30. package/dist/plugins/findUniqueAuthentication/schemas.d.ts +5 -3
  31. package/dist/plugins/findUniqueAuthentication/schemas.d.ts.map +1 -1
  32. package/dist/plugins/getAction/index.d.ts.map +1 -1
  33. package/dist/plugins/getAction/index.js +10 -0
  34. package/dist/plugins/getAction/schemas.d.ts +5 -3
  35. package/dist/plugins/getAction/schemas.d.ts.map +1 -1
  36. package/dist/plugins/getApp/index.d.ts +2 -7
  37. package/dist/plugins/getApp/index.d.ts.map +1 -1
  38. package/dist/plugins/getApp/index.js +17 -9
  39. package/dist/plugins/getApp/index.test.js +3 -3
  40. package/dist/plugins/getApp/schemas.d.ts +3 -1
  41. package/dist/plugins/getApp/schemas.d.ts.map +1 -1
  42. package/dist/plugins/getApp/schemas.js +2 -4
  43. package/dist/plugins/getAuthentication/index.d.ts.map +1 -1
  44. package/dist/plugins/getAuthentication/index.js +8 -0
  45. package/dist/plugins/getAuthentication/index.test.js +1 -1
  46. package/dist/plugins/getProfile/index.d.ts.map +1 -1
  47. package/dist/plugins/getProfile/index.js +4 -0
  48. package/dist/plugins/getProfile/schemas.d.ts.map +1 -1
  49. package/dist/plugins/getProfile/schemas.js +4 -3
  50. package/dist/plugins/listActions/index.d.ts +2 -4
  51. package/dist/plugins/listActions/index.d.ts.map +1 -1
  52. package/dist/plugins/listActions/index.js +10 -1
  53. package/dist/plugins/listActions/index.test.js +4 -4
  54. package/dist/plugins/listActions/schemas.d.ts +5 -3
  55. package/dist/plugins/listActions/schemas.d.ts.map +1 -1
  56. package/dist/plugins/listActions/schemas.js +2 -4
  57. package/dist/plugins/listApps/index.d.ts +4 -7
  58. package/dist/plugins/listApps/index.d.ts.map +1 -1
  59. package/dist/plugins/listApps/index.js +37 -17
  60. package/dist/plugins/listApps/index.test.js +23 -3
  61. package/dist/plugins/listApps/schemas.d.ts.map +1 -1
  62. package/dist/plugins/listApps/schemas.js +3 -9
  63. package/dist/plugins/listAuthentications/index.d.ts +2 -4
  64. package/dist/plugins/listAuthentications/index.d.ts.map +1 -1
  65. package/dist/plugins/listAuthentications/index.js +12 -0
  66. package/dist/plugins/listAuthentications/index.test.js +39 -13
  67. package/dist/plugins/listAuthentications/schemas.d.ts +8 -3
  68. package/dist/plugins/listAuthentications/schemas.d.ts.map +1 -1
  69. package/dist/plugins/listAuthentications/schemas.js +4 -0
  70. package/dist/plugins/listInputFieldChoices/index.d.ts.map +1 -1
  71. package/dist/plugins/listInputFieldChoices/index.js +14 -2
  72. package/dist/plugins/listInputFieldChoices/schemas.d.ts +5 -3
  73. package/dist/plugins/listInputFieldChoices/schemas.d.ts.map +1 -1
  74. package/dist/plugins/listInputFieldChoices/schemas.js +10 -19
  75. package/dist/plugins/listInputFields/index.d.ts.map +1 -1
  76. package/dist/plugins/listInputFields/index.js +14 -2
  77. package/dist/plugins/listInputFields/index.test.js +5 -9
  78. package/dist/plugins/listInputFields/schemas.d.ts +5 -3
  79. package/dist/plugins/listInputFields/schemas.d.ts.map +1 -1
  80. package/dist/plugins/manifest/index.d.ts +25 -9
  81. package/dist/plugins/manifest/index.d.ts.map +1 -1
  82. package/dist/plugins/manifest/index.js +239 -67
  83. package/dist/plugins/manifest/index.test.js +426 -171
  84. package/dist/plugins/manifest/schemas.d.ts +5 -1
  85. package/dist/plugins/manifest/schemas.d.ts.map +1 -1
  86. package/dist/plugins/manifest/schemas.js +1 -0
  87. package/dist/plugins/registry/index.d.ts.map +1 -1
  88. package/dist/plugins/registry/index.js +8 -2
  89. package/dist/plugins/request/index.d.ts.map +1 -1
  90. package/dist/plugins/request/index.js +1 -0
  91. package/dist/plugins/runAction/index.d.ts.map +1 -1
  92. package/dist/plugins/runAction/index.js +12 -0
  93. package/dist/plugins/runAction/schemas.d.ts +5 -3
  94. package/dist/plugins/runAction/schemas.d.ts.map +1 -1
  95. package/dist/resolvers/actionKey.d.ts +13 -7
  96. package/dist/resolvers/actionKey.d.ts.map +1 -1
  97. package/dist/resolvers/actionType.d.ts +8 -7
  98. package/dist/resolvers/actionType.d.ts.map +1 -1
  99. package/dist/resolvers/appKey.d.ts +2 -6
  100. package/dist/resolvers/appKey.d.ts.map +1 -1
  101. package/dist/resolvers/authenticationId.d.ts +7 -7
  102. package/dist/resolvers/authenticationId.d.ts.map +1 -1
  103. package/dist/resolvers/authenticationId.js +16 -7
  104. package/dist/resolvers/index.d.ts +3 -35
  105. package/dist/resolvers/index.d.ts.map +1 -1
  106. package/dist/resolvers/index.js +4 -87
  107. package/dist/resolvers/inputFieldKey.d.ts +11 -0
  108. package/dist/resolvers/inputFieldKey.d.ts.map +1 -0
  109. package/dist/resolvers/inputFieldKey.js +23 -0
  110. package/dist/resolvers/inputs.d.ts +11 -6
  111. package/dist/resolvers/inputs.d.ts.map +1 -1
  112. package/dist/resolvers/inputs.js +17 -0
  113. package/dist/schemas/Action.d.ts +8 -8
  114. package/dist/schemas/Action.d.ts.map +1 -1
  115. package/dist/schemas/Action.js +8 -3
  116. package/dist/schemas/App.d.ts +183 -11
  117. package/dist/schemas/App.d.ts.map +1 -1
  118. package/dist/schemas/App.js +7 -9
  119. package/dist/schemas/Auth.d.ts +12 -12
  120. package/dist/schemas/Auth.js +1 -1
  121. package/dist/schemas/Field.d.ts +5 -98
  122. package/dist/schemas/Field.d.ts.map +1 -1
  123. package/dist/schemas/Field.js +24 -52
  124. package/dist/schemas/Run.d.ts +3 -0
  125. package/dist/schemas/Run.d.ts.map +1 -0
  126. package/dist/schemas/Run.js +31 -0
  127. package/dist/schemas/UserProfile.d.ts +11 -11
  128. package/dist/schemas/UserProfile.d.ts.map +1 -1
  129. package/dist/schemas/UserProfile.js +21 -7
  130. package/dist/sdk.d.ts +15 -14
  131. package/dist/sdk.d.ts.map +1 -1
  132. package/dist/sdk.js +1 -4
  133. package/dist/types/plugin.d.ts +6 -0
  134. package/dist/types/plugin.d.ts.map +1 -1
  135. package/dist/types/properties.d.ts +3 -1
  136. package/dist/types/properties.d.ts.map +1 -1
  137. package/dist/types/sdk.d.ts +11 -3
  138. package/dist/types/sdk.d.ts.map +1 -1
  139. package/dist/utils/domain-utils.d.ts +17 -16
  140. package/dist/utils/domain-utils.d.ts.map +1 -1
  141. package/dist/utils/domain-utils.js +53 -78
  142. package/dist/utils/domain-utils.test.js +157 -3
  143. package/dist/utils/file-utils.d.ts +4 -0
  144. package/dist/utils/file-utils.d.ts.map +1 -0
  145. package/dist/utils/file-utils.js +74 -0
  146. package/dist/utils/file-utils.test.d.ts +2 -0
  147. package/dist/utils/file-utils.test.d.ts.map +1 -0
  148. package/dist/utils/file-utils.test.js +51 -0
  149. package/dist/utils/schema-utils.d.ts +44 -21
  150. package/dist/utils/schema-utils.d.ts.map +1 -1
  151. package/dist/utils/schema-utils.js +17 -11
  152. package/package.json +1 -1
  153. package/src/api/client.ts +12 -0
  154. package/src/api/debug.test.ts +76 -0
  155. package/src/api/debug.ts +46 -2
  156. package/src/api/schemas.ts +51 -29
  157. package/src/index.ts +5 -6
  158. package/src/plugins/apps/index.ts +9 -2
  159. package/src/plugins/findFirstAuthentication/index.test.ts +8 -5
  160. package/src/plugins/findFirstAuthentication/index.ts +14 -2
  161. package/src/plugins/findUniqueAuthentication/index.ts +4 -0
  162. package/src/plugins/getAction/index.ts +14 -0
  163. package/src/plugins/getApp/index.test.ts +3 -3
  164. package/src/plugins/getApp/index.ts +20 -14
  165. package/src/plugins/getApp/schemas.ts +7 -12
  166. package/src/plugins/getAuthentication/index.test.ts +1 -1
  167. package/src/plugins/getAuthentication/index.ts +8 -0
  168. package/src/plugins/getProfile/index.ts +4 -0
  169. package/src/plugins/getProfile/schemas.ts +4 -6
  170. package/src/plugins/listActions/index.test.ts +8 -7
  171. package/src/plugins/listActions/index.ts +12 -3
  172. package/src/plugins/listActions/schemas.ts +20 -25
  173. package/src/plugins/listApps/index.test.ts +24 -3
  174. package/src/plugins/listApps/index.ts +50 -25
  175. package/src/plugins/listApps/schemas.ts +17 -26
  176. package/src/plugins/listAuthentications/index.test.ts +52 -15
  177. package/src/plugins/listAuthentications/index.ts +15 -2
  178. package/src/plugins/listAuthentications/schemas.ts +4 -0
  179. package/src/plugins/listInputFieldChoices/index.ts +21 -1
  180. package/src/plugins/listInputFieldChoices/schemas.ts +61 -76
  181. package/src/plugins/listInputFields/index.test.ts +5 -9
  182. package/src/plugins/listInputFields/index.ts +20 -2
  183. package/src/plugins/manifest/index.test.ts +503 -197
  184. package/src/plugins/manifest/index.ts +338 -82
  185. package/src/plugins/manifest/schemas.ts +9 -2
  186. package/src/plugins/registry/index.ts +8 -2
  187. package/src/plugins/request/index.ts +1 -0
  188. package/src/plugins/runAction/index.ts +18 -0
  189. package/src/resolvers/actionKey.ts +15 -13
  190. package/src/resolvers/actionType.ts +10 -12
  191. package/src/resolvers/appKey.ts +2 -6
  192. package/src/resolvers/authenticationId.ts +25 -19
  193. package/src/resolvers/index.ts +7 -113
  194. package/src/resolvers/inputFieldKey.ts +38 -0
  195. package/src/resolvers/inputs.ts +28 -10
  196. package/src/schemas/Action.ts +8 -3
  197. package/src/schemas/App.ts +7 -9
  198. package/src/schemas/Auth.ts +1 -1
  199. package/src/schemas/Field.ts +24 -57
  200. package/src/schemas/Run.ts +40 -0
  201. package/src/schemas/UserProfile.ts +24 -7
  202. package/src/sdk.ts +18 -12
  203. package/src/types/plugin.ts +8 -0
  204. package/src/types/sdk.ts +31 -21
  205. package/src/utils/domain-utils.test.ts +196 -2
  206. package/src/utils/domain-utils.ts +77 -102
  207. package/src/utils/file-utils.test.ts +73 -0
  208. package/src/utils/file-utils.ts +94 -0
  209. package/src/utils/schema-utils.ts +96 -44
  210. package/tsconfig.tsbuildinfo +1 -1
  211. package/dist/plugins/lockVersion/index.d.ts +0 -24
  212. package/dist/plugins/lockVersion/index.d.ts.map +0 -1
  213. package/dist/plugins/lockVersion/index.js +0 -72
  214. package/dist/plugins/lockVersion/index.test.d.ts +0 -2
  215. package/dist/plugins/lockVersion/index.test.d.ts.map +0 -1
  216. package/dist/plugins/lockVersion/index.test.js +0 -129
  217. package/dist/plugins/lockVersion/schemas.d.ts +0 -10
  218. package/dist/plugins/lockVersion/schemas.d.ts.map +0 -1
  219. package/dist/plugins/lockVersion/schemas.js +0 -6
  220. package/src/plugins/lockVersion/index.test.ts +0 -176
  221. package/src/plugins/lockVersion/index.ts +0 -112
  222. package/src/plugins/lockVersion/schemas.ts +0 -9
@@ -1,23 +1,26 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { manifestPlugin, loadManifestFromFile } from "./index";
2
+ import {
3
+ manifestPlugin,
4
+ readManifestFromFile,
5
+ findManifestEntry,
6
+ getPreferredManifestEntryKey,
7
+ } from "./index";
3
8
  import { createSdk } from "../../sdk";
4
9
  import type { Manifest } from "./schemas";
5
10
 
6
- // Mock fs and path modules
7
- vi.mock("fs", () => ({
8
- readFileSync: vi.fn(),
11
+ // Mock file-utils module
12
+ vi.mock("../../utils/file-utils", () => ({
13
+ readFile: vi.fn(),
14
+ writeFile: vi.fn(),
15
+ resolve: vi.fn((path) => Promise.resolve(`/resolved/${path}`)),
9
16
  }));
10
17
 
11
- vi.mock("path", () => ({
12
- resolve: vi.fn((path) => `/resolved/${path}`),
13
- }));
14
-
15
- import { readFileSync } from "fs";
16
- import { resolve } from "path";
18
+ import { readFile, resolve } from "../../utils/file-utils";
17
19
  import type { ApiClient } from "../../api";
18
20
 
19
- const mockReadFileSync = vi.mocked(readFileSync);
21
+ const mockReadFile = vi.mocked(readFile);
20
22
  const mockResolve = vi.mocked(resolve);
23
+ const mockResolveAppKeys = vi.fn();
21
24
 
22
25
  describe("manifestPlugin", () => {
23
26
  const mockManifest = {
@@ -43,63 +46,100 @@ describe("manifestPlugin", () => {
43
46
  vi.spyOn(console, "warn").mockImplementation(() => {});
44
47
 
45
48
  mockApiClient = {
46
- get: vi.fn().mockResolvedValue({
47
- count: 1,
48
- next: null,
49
- previous: null,
50
- results: [
51
- {
52
- selected_api: "SlackCLIAPI@1.29.0",
53
- app_id: null,
54
- service_id: null,
55
- auth_type: "oauth",
56
- auth_fields: [
49
+ get: vi.fn().mockImplementation((url: string, options?: any) => {
50
+ if (url === "/api/v4/implementations/") {
51
+ // Mock for manifest entries (versioned)
52
+ return Promise.resolve({
53
+ count: 1,
54
+ next: null,
55
+ previous: null,
56
+ results: [
57
57
  {
58
- key: "access_token",
59
- required: true,
60
- type: "unicode",
61
- computed: true,
58
+ selected_api: "SlackCLIAPI@1.29.0",
59
+ app_id: null,
60
+ service_id: null,
61
+ auth_type: "oauth",
62
+ auth_fields: [
63
+ {
64
+ key: "access_token",
65
+ required: true,
66
+ type: "unicode",
67
+ computed: true,
68
+ },
69
+ ],
70
+ is_deprecated: false,
71
+ is_private_only: false,
72
+ is_invite_only: false,
73
+ is_beta: false,
74
+ is_premium: false,
75
+ is_hidden: false,
76
+ name: "Slack (1.29.0)",
77
+ name_clean: "Slack",
78
+ version: "1.29.0",
79
+ slug: null,
80
+ images: {
81
+ url_16x16:
82
+ "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=16&ixlib=python-3.0.0&q=50&w=16",
83
+ url_32x32:
84
+ "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=32&ixlib=python-3.0.0&q=50&w=32",
85
+ url_64x64:
86
+ "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=64&ixlib=python-3.0.0&q=50&w=64",
87
+ url_128x128:
88
+ "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=128&ixlib=python-3.0.0&q=50&w=128",
89
+ },
90
+ primary_color: null,
91
+ secondary_color: null,
92
+ classification: "third-party",
93
+ current_implementation: "SlackCLIAPI@1.30.0",
94
+ is_adoptable: true,
95
+ is_usable: true,
96
+ update_cta_level: "info",
97
+ update_cta_message:
98
+ "This version is legacy. Consider updating to the latest version for better support.",
99
+ badges: [
100
+ {
101
+ label: "Legacy",
102
+ color: "primary",
103
+ tooltip_markdown:
104
+ "This version is legacy. Consider updating to the latest version for better support.",
105
+ },
106
+ ],
62
107
  },
63
108
  ],
64
- is_deprecated: false,
65
- is_private_only: false,
66
- is_invite_only: false,
67
- is_beta: false,
68
- is_premium: false,
69
- is_hidden: false,
70
- name: "Slack (1.29.0)",
71
- name_clean: "Slack",
72
- version: "1.29.0",
73
- slug: null,
74
- images: {
75
- url_16x16:
76
- "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=16&ixlib=python-3.0.0&q=50&w=16",
77
- url_32x32:
78
- "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=32&ixlib=python-3.0.0&q=50&w=32",
79
- url_64x64:
80
- "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=64&ixlib=python-3.0.0&q=50&w=64",
81
- url_128x128:
82
- "https://zapier-images.imgix.net/storage/services/6cf3f5a461feadfba7abc93c4c395b33_2.png?auto=format%2Ccompress&fit=crop&h=128&ixlib=python-3.0.0&q=50&w=128",
83
- },
84
- primary_color: null,
85
- secondary_color: null,
86
- classification: "third-party",
87
- current_implementation: "SlackCLIAPI@1.30.0",
88
- is_adoptable: true,
89
- is_usable: true,
90
- update_cta_level: "info",
91
- update_cta_message:
92
- "This version is legacy. Consider updating to the latest version for better support.",
93
- badges: [
109
+ });
110
+ } else if (url === "/api/v4/implementations-meta/lookup/") {
111
+ // Mock for implementations-meta/lookup fallback
112
+ const searchParams = options?.searchParams || {};
113
+
114
+ // Check if we're looking for a nonexistent app
115
+ if (
116
+ searchParams.slugs?.includes("nonexistent") ||
117
+ searchParams.selected_apis?.includes("nonexistent")
118
+ ) {
119
+ return Promise.resolve({
120
+ count: 0,
121
+ next: null,
122
+ previous: null,
123
+ results: [],
124
+ });
125
+ }
126
+
127
+ // Return successful response for known apps (like slack)
128
+ return Promise.resolve({
129
+ count: 1,
130
+ next: null,
131
+ previous: null,
132
+ results: [
94
133
  {
95
- label: "Legacy",
96
- color: "primary",
97
- tooltip_markdown:
98
- "This version is legacy. Consider updating to the latest version for better support.",
134
+ id: "SlackCLIAPI@1.30.0",
135
+ name: "Slack",
136
+ slug: "slack",
137
+ images: {},
99
138
  },
100
139
  ],
101
- },
102
- ],
140
+ });
141
+ }
142
+ return Promise.resolve({ count: 0, results: [] });
103
143
  }),
104
144
  } as Partial<ApiClient> as ApiClient;
105
145
 
@@ -141,10 +181,7 @@ describe("manifestPlugin", () => {
141
181
  });
142
182
 
143
183
  function createTestSdk(options: any = {}) {
144
- return createSdk(options)
145
- .addPlugin(apiPlugin)
146
- .addPlugin(listAppsMockPlugin)
147
- .addPlugin(manifestPlugin);
184
+ return createSdk(options).addPlugin(apiPlugin).addPlugin(manifestPlugin);
148
185
  }
149
186
 
150
187
  describe("plugin initialization", () => {
@@ -153,28 +190,20 @@ describe("manifestPlugin", () => {
153
190
  const context = sdk.getContext();
154
191
 
155
192
  expect(context.getVersionedImplementationId).toBeInstanceOf(Function);
156
- expect(context.getImplementation).toBeInstanceOf(Function);
157
- expect(context.getManifestEntry).toBeInstanceOf(Function);
158
193
  });
159
194
 
160
- it("should provide helper functions with manifestPath", () => {
161
- mockReadFileSync.mockReturnValue(mockManifestContent);
195
+ it("should provide helper functions with manifestPath", async () => {
196
+ mockReadFile.mockResolvedValue(mockManifestContent);
162
197
 
163
198
  const sdk = createTestSdk({ manifestPath: "manifest.json" });
164
199
  const context = sdk.getContext();
165
200
 
166
201
  expect(context.getVersionedImplementationId).toBeInstanceOf(Function);
167
- expect(context.getImplementation).toBeInstanceOf(Function);
168
- expect(context.getManifestEntry).toBeInstanceOf(Function);
169
202
 
170
203
  // Verify file operations happen when functions are called
171
- const manifestEntry = context.getManifestEntry("slack");
204
+ await context.getVersionedImplementationId("slack");
172
205
  expect(mockResolve).toHaveBeenCalledWith("manifest.json");
173
- expect(mockReadFileSync).toHaveBeenCalledWith(
174
- "/resolved/manifest.json",
175
- "utf8",
176
- );
177
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
206
+ expect(mockReadFile).toHaveBeenCalledWith("/resolved/manifest.json");
178
207
  });
179
208
 
180
209
  it("should provide helper functions when no manifest or path provided", () => {
@@ -182,29 +211,22 @@ describe("manifestPlugin", () => {
182
211
  const context = sdk.getContext();
183
212
 
184
213
  expect(context.getVersionedImplementationId).toBeInstanceOf(Function);
185
- expect(context.getImplementation).toBeInstanceOf(Function);
186
- expect(context.getManifestEntry).toBeInstanceOf(Function);
187
214
 
188
- // Verify that no manifest entry is found
189
- const manifestEntry = context.getManifestEntry("slack");
190
- expect(manifestEntry).toBeNull();
215
+ // Manifest functions are available even when no manifest is provided
191
216
  });
192
217
 
193
218
  it("should prioritize direct manifest over manifestPath", () => {
194
- mockReadFileSync.mockReturnValue(
219
+ mockReadFile.mockResolvedValue(
195
220
  '{"apps": {"other": {"implementationName": "Other", "version": "1.0.0"}}}',
196
221
  );
197
222
 
198
- const sdk = createTestSdk({
223
+ createTestSdk({
199
224
  manifest: mockManifest,
200
225
  manifestPath: "manifest.json",
201
226
  });
202
- const context = sdk.getContext();
203
227
 
204
- // Verify that direct manifest is used (should find slack app)
205
- const manifestEntry = context.getManifestEntry("slack");
206
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
207
- expect(mockReadFileSync).not.toHaveBeenCalled();
228
+ // Verify that direct manifest is used and file loading is not needed
229
+ expect(mockReadFile).not.toHaveBeenCalled();
208
230
  });
209
231
  });
210
232
 
@@ -238,54 +260,11 @@ describe("manifestPlugin", () => {
238
260
  });
239
261
  });
240
262
 
241
- describe("getImplementation function", () => {
242
- it("should return app from manifest when available", async () => {
243
- const sdk = createTestSdk({ manifest: mockManifest });
244
- const context = sdk.getContext();
245
-
246
- const result = await context.getImplementation("slack");
247
- expect(result).toBeDefined();
248
- expect(mockApiClient.get).toHaveBeenCalledWith(
249
- "/api/v4/implementations/",
250
- {
251
- searchParams: {
252
- selected_apis: "SlackCLIAPI@1.21.1",
253
- },
254
- },
255
- );
256
- });
257
-
258
- it("should fetch app from API when not in manifest", async () => {
259
- const sdk = createTestSdk();
260
- const context = sdk.getContext();
261
-
262
- const result = await context.getImplementation("slack");
263
- expect(result).toBeDefined();
264
- expect(result?.title).toBe("Slack");
265
- expect(result?.current_implementation_id).toBe("SlackCLIAPI@1.30.0");
266
- });
267
-
268
- it("should return null when app cannot be fetched", async () => {
269
- mockSdk.listApps = vi.fn().mockReturnValue({
270
- items: vi.fn().mockReturnValue([][Symbol.iterator]()),
271
- });
272
-
273
- const sdk = createTestSdk();
274
- const context = sdk.getContext();
275
-
276
- const result = await context.getImplementation("nonexistent");
277
- expect(result).toBeNull();
278
- });
279
- });
280
-
281
263
  describe("manifest parsing", () => {
282
264
  it("should parse valid manifest content", () => {
283
- const sdk = createTestSdk({ manifest: mockManifest });
284
- const context = sdk.getContext();
265
+ createTestSdk({ manifest: mockManifest });
285
266
 
286
- // Verify manifest content is accessible through helper functions
287
- const manifestEntry = context.getManifestEntry("slack");
288
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
267
+ // Verify manifest is parsed correctly for other functions to use
289
268
  });
290
269
 
291
270
  it("should handle manifest with missing version", () => {
@@ -297,68 +276,58 @@ describe("manifestPlugin", () => {
297
276
  },
298
277
  };
299
278
 
300
- const sdk = createTestSdk({ manifest: manifestWithoutVersion });
301
- const context = sdk.getContext();
279
+ createTestSdk({ manifest: manifestWithoutVersion });
302
280
 
303
- // Verify manifest entry is accessible and matches expected structure
304
- const manifestEntry = context.getManifestEntry("slack");
305
- expect(manifestEntry).toEqual(manifestWithoutVersion.apps.slack);
281
+ // Verify manifest with missing version is handled correctly
306
282
  });
307
283
 
308
284
  it("should handle empty manifest", () => {
309
- const sdk = createTestSdk({ manifest: {} });
310
- const context = sdk.getContext();
285
+ createTestSdk({ manifest: {} });
311
286
 
312
- // Verify that no manifest entry is found for any app
313
- const manifestEntry = context.getManifestEntry("slack");
314
- expect(manifestEntry).toBeNull();
287
+ // Verify empty manifest is handled correctly
315
288
  });
316
289
  });
317
290
 
318
291
  describe("file loading", () => {
319
- it("should load manifest from file successfully", () => {
320
- mockReadFileSync.mockReturnValue(mockManifestContent);
292
+ it("should load manifest from file successfully", async () => {
293
+ mockReadFile.mockResolvedValue(mockManifestContent);
321
294
 
322
295
  const sdk = createTestSdk({ manifestPath: "manifest.json" });
323
296
  const context = sdk.getContext();
324
297
 
325
298
  // Verify that file operations happen when functions are called
326
- const manifestEntry = context.getManifestEntry("slack");
299
+ await context.getVersionedImplementationId("slack");
327
300
  expect(mockResolve).toHaveBeenCalledWith("manifest.json");
328
- expect(mockReadFileSync).toHaveBeenCalledWith(
329
- "/resolved/manifest.json",
330
- "utf8",
331
- );
332
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
301
+ expect(mockReadFile).toHaveBeenCalledWith("/resolved/manifest.json");
333
302
  });
334
303
 
335
- it("should handle file read errors gracefully", () => {
336
- mockReadFileSync.mockImplementation(() => {
337
- throw new Error("File not found");
338
- });
304
+ it("should handle file read errors gracefully", async () => {
305
+ mockReadFile.mockRejectedValue(new Error("File not found"));
339
306
 
340
307
  const sdk = createTestSdk({ manifestPath: "nonexistent.json" });
341
308
  const context = sdk.getContext();
342
309
 
343
- // Verify that no manifest entry is found when file loading fails
344
- const manifestEntry = context.getManifestEntry("slack");
345
- expect(manifestEntry).toBeNull();
310
+ // Trigger file loading by calling a function
311
+ await context.getVersionedImplementationId("slack");
312
+
313
+ // Verify file loading errors are handled gracefully
346
314
  expect(console.warn).toHaveBeenCalledWith(
347
315
  expect.stringContaining(
348
- "Failed to load manifest from nonexistent.json",
316
+ "Failed to read manifest from nonexistent.json",
349
317
  ),
350
318
  );
351
319
  });
352
320
 
353
- it("should handle invalid JSON gracefully", () => {
354
- mockReadFileSync.mockReturnValue("invalid json content");
321
+ it("should handle invalid JSON gracefully", async () => {
322
+ mockReadFile.mockResolvedValue("invalid json content");
355
323
 
356
324
  const sdk = createTestSdk({ manifestPath: "invalid.json" });
357
325
  const context = sdk.getContext();
358
326
 
359
- // Verify that no manifest entry is found when JSON parsing fails
360
- const manifestEntry = context.getManifestEntry("slack");
361
- expect(manifestEntry).toBeNull();
327
+ // Trigger file loading by calling a function
328
+ await context.getVersionedImplementationId("slack");
329
+
330
+ // Verify JSON parsing errors are handled gracefully
362
331
  expect(console.warn).toHaveBeenCalledWith(
363
332
  expect.stringContaining(
364
333
  "Failed to parse manifest from /resolved/invalid.json",
@@ -368,25 +337,19 @@ describe("manifestPlugin", () => {
368
337
  });
369
338
 
370
339
  it("should handle JSON without apps property", () => {
371
- mockReadFileSync.mockReturnValue('{"other": "data"}');
340
+ mockReadFile.mockResolvedValue('{"other": "data"}');
372
341
 
373
- const sdk = createTestSdk({ manifestPath: "no-apps.json" });
374
- const context = sdk.getContext();
342
+ createTestSdk({ manifestPath: "no-apps.json" });
375
343
 
376
- // Verify that no manifest entry is found when apps property is missing
377
- const manifestEntry = context.getManifestEntry("slack");
378
- expect(manifestEntry).toBeNull();
344
+ // Verify missing apps property is handled correctly
379
345
  });
380
346
 
381
347
  it("should handle JSON with invalid apps format", () => {
382
- mockReadFileSync.mockReturnValue('{"apps": "not an object"}');
348
+ mockReadFile.mockResolvedValue('{"apps": "not an object"}');
383
349
 
384
- const sdk = createTestSdk({ manifestPath: "invalid-apps.json" });
385
- const context = sdk.getContext();
350
+ createTestSdk({ manifestPath: "invalid-apps.json" });
386
351
 
387
- // Verify that no manifest entry is found when apps format is invalid
388
- const manifestEntry = context.getManifestEntry("slack");
389
- expect(manifestEntry).toBeNull();
352
+ // Verify invalid apps format is handled correctly
390
353
  });
391
354
  });
392
355
 
@@ -398,17 +361,294 @@ describe("manifestPlugin", () => {
398
361
  .addPlugin(manifestPlugin);
399
362
  const context = sdk.getContext();
400
363
 
401
- // Verify that the manifest data is used correctly
402
- const manifestEntry = context.getManifestEntry("slack");
403
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
364
+ // Verify that the manifest data is used correctly by other functions
404
365
 
405
366
  const versionedId = await context.getVersionedImplementationId("slack");
406
367
  expect(versionedId).toBe("SlackCLIAPI@1.21.1");
407
368
  });
408
369
  });
370
+
371
+ describe("findManifestEntry function", () => {
372
+ it("should find entry by direct key match", () => {
373
+ const manifest = {
374
+ apps: {
375
+ slack: { implementationName: "SlackCLIAPI", version: "1.21.1" },
376
+ gmail: { implementationName: "GmailCLIAPI", version: "2.0.0" },
377
+ },
378
+ };
379
+
380
+ const result = findManifestEntry({ appKey: "slack", manifest });
381
+
382
+ expect(result).toEqual([
383
+ "slack",
384
+ { implementationName: "SlackCLIAPI", version: "1.21.1" },
385
+ ]);
386
+ });
387
+
388
+ it("should find entry by implementation name match", () => {
389
+ const manifest = {
390
+ apps: {
391
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
392
+ GmailCLIAPI: { implementationName: "GmailCLIAPI", version: "2.0.0" },
393
+ },
394
+ };
395
+
396
+ const result = findManifestEntry({ appKey: "SlackCLIAPI", manifest });
397
+
398
+ expect(result).toEqual([
399
+ "SlackCLIAPI",
400
+ { implementationName: "SlackCLIAPI", version: "1.21.1" },
401
+ ]);
402
+ });
403
+
404
+ it("should find existing key when searching by implementation name", () => {
405
+ const manifest = {
406
+ apps: {
407
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
408
+ },
409
+ };
410
+
411
+ // This is the key test: searching by implementation name should find existing entry
412
+ const result = findManifestEntry({ appKey: "SlackCLIAPI", manifest });
413
+
414
+ expect(result).toEqual([
415
+ "SlackCLIAPI",
416
+ { implementationName: "SlackCLIAPI", version: "1.21.1" },
417
+ ]);
418
+ });
419
+
420
+ it("should return null when no match found", () => {
421
+ const manifest = {
422
+ apps: {
423
+ gmail: { implementationName: "GmailCLIAPI", version: "2.0.0" },
424
+ },
425
+ };
426
+
427
+ const result = findManifestEntry({ appKey: "slack", manifest });
428
+
429
+ expect(result).toBeNull();
430
+ });
431
+
432
+ it("should handle snake-cased slugs", () => {
433
+ const manifest = {
434
+ apps: {
435
+ "google-sheets": {
436
+ implementationName: "GoogleSheetsCLIAPI",
437
+ version: "1.0.0",
438
+ },
439
+ },
440
+ };
441
+
442
+ const result = findManifestEntry({ appKey: "google_sheets", manifest });
443
+
444
+ expect(result).toEqual([
445
+ "google-sheets",
446
+ { implementationName: "GoogleSheetsCLIAPI", version: "1.0.0" },
447
+ ]);
448
+ });
449
+
450
+ it("should NOT find entry when searching slug against implementation name key (current limitation)", () => {
451
+ const manifest = {
452
+ apps: {
453
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
454
+ },
455
+ };
456
+
457
+ // This will fail - we search by "slack" but can't find "SlackCLIAPI" entry
458
+ // This is the core issue we need to solve
459
+ const result = findManifestEntry({ appKey: "slack", manifest });
460
+
461
+ expect(result).toBeNull(); // Current behavior - doesn't find it
462
+ // We need to fix this so it CAN find existing implementation name entries
463
+ });
464
+ });
465
+
466
+ describe("updateManifestEntry with slug-to-implementation matching", () => {
467
+ it("should find existing implementation name entry when input is slug", async () => {
468
+ // Mock existing manifest with implementation name as key
469
+ mockReadFile.mockResolvedValue(
470
+ JSON.stringify({
471
+ apps: {
472
+ SlackCLIAPI: {
473
+ implementationName: "SlackCLIAPI",
474
+ version: "1.21.1",
475
+ },
476
+ },
477
+ }),
478
+ );
479
+
480
+ // Mock resolveAppKeys to resolve "slack" to "SlackCLIAPI"
481
+ mockResolveAppKeys.mockResolvedValue([
482
+ {
483
+ slug: "slack",
484
+ implementationName: "SlackCLIAPI",
485
+ version: "1.30.0",
486
+ lookupAppKey: "slack",
487
+ },
488
+ ]);
489
+
490
+ const sdk = createTestSdk();
491
+ const context = sdk.getContext();
492
+
493
+ // This should now find the existing "SlackCLIAPI" entry
494
+ const [manifestKey] = await context.updateManifestEntry(
495
+ "slack", // Input is slug
496
+ { implementationName: "SlackCLIAPI", version: "1.30.0" },
497
+ );
498
+
499
+ // Should return the existing key, not create a new one
500
+ expect(manifestKey).toBe("SlackCLIAPI");
501
+ });
502
+ });
503
+
504
+ describe("resolveAppKeys function", () => {
505
+ it("should resolve slug to manifest entry even when manifest uses implementation name as key", async () => {
506
+ const manifest = {
507
+ apps: {
508
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
509
+ },
510
+ };
511
+
512
+ // Mock API to return slack app data when looking up "slack"
513
+ mockApiClient.get = vi.fn().mockResolvedValue({
514
+ count: 1,
515
+ next: null,
516
+ previous: null,
517
+ results: [
518
+ {
519
+ id: "SlackCLIAPI@1.30.0",
520
+ name: "Slack",
521
+ slug: "slack",
522
+ key: "SlackCLIAPI", // This is the implementation name
523
+ version: "1.30.0",
524
+ },
525
+ ],
526
+ });
527
+
528
+ // Test resolveAppKeys directly using the manifest plugin
529
+ const sdk = createSdk({ manifest })
530
+ .addPlugin(apiPlugin)
531
+ .addPlugin(manifestPlugin);
532
+ const context = sdk.getContext();
533
+
534
+ const resolved = await context.resolveAppKeys({ appKeys: ["slack"] });
535
+
536
+ // Should find the manifest entry and use its version (1.21.1) instead of latest (1.30.0)
537
+ expect(resolved).toHaveLength(1);
538
+ expect(resolved[0]).toEqual({
539
+ slug: "slack",
540
+ implementationName: "SlackCLIAPI",
541
+ version: "1.21.1", // Should use manifest version, not API version
542
+ lookupAppKey: "slack",
543
+ });
544
+ });
545
+
546
+ it("should fall back to API version when no manifest entry found", async () => {
547
+ const manifest = { apps: {} }; // Empty manifest
548
+
549
+ // Mock API to return slack app data
550
+ mockApiClient.get = vi.fn().mockResolvedValue({
551
+ count: 1,
552
+ next: null,
553
+ previous: null,
554
+ results: [
555
+ {
556
+ id: "SlackCLIAPI@1.30.0",
557
+ name: "Slack",
558
+ slug: "slack",
559
+ key: "SlackCLIAPI",
560
+ version: "1.30.0",
561
+ },
562
+ ],
563
+ });
564
+
565
+ const sdk = createSdk({ manifest })
566
+ .addPlugin(apiPlugin)
567
+ .addPlugin(manifestPlugin);
568
+ const context = sdk.getContext();
569
+
570
+ const resolved = await context.resolveAppKeys({ appKeys: ["slack"] });
571
+
572
+ // Should use API version since no manifest entry found
573
+ expect(resolved).toHaveLength(1);
574
+ expect(resolved[0]).toEqual({
575
+ slug: "slack",
576
+ implementationName: "SlackCLIAPI",
577
+ version: "1.30.0", // Should use API version
578
+ lookupAppKey: "slack",
579
+ });
580
+ });
581
+
582
+ it("should always use version from input when specified (slug@version)", async () => {
583
+ const manifest = {
584
+ apps: {
585
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
586
+ },
587
+ };
588
+
589
+ // Mock API to return slack app data
590
+ mockApiClient.get = vi.fn().mockResolvedValue({
591
+ count: 1,
592
+ next: null,
593
+ previous: null,
594
+ results: [
595
+ {
596
+ id: "SlackCLIAPI@1.30.0",
597
+ name: "Slack",
598
+ slug: "slack",
599
+ key: "SlackCLIAPI",
600
+ version: "1.30.0",
601
+ },
602
+ ],
603
+ });
604
+
605
+ const sdk = createSdk({ manifest })
606
+ .addPlugin(apiPlugin)
607
+ .addPlugin(manifestPlugin);
608
+ const context = sdk.getContext();
609
+
610
+ // Input version should take precedence over both manifest (1.21.1) and API (1.30.0)
611
+ const resolved = await context.resolveAppKeys({
612
+ appKeys: ["slack@1.2.3"],
613
+ });
614
+
615
+ expect(resolved).toHaveLength(1);
616
+ expect(resolved[0]).toEqual({
617
+ slug: "slack",
618
+ implementationName: "SlackCLIAPI",
619
+ version: "1.2.3", // Should use input version, not manifest or API
620
+ lookupAppKey: "slack",
621
+ });
622
+ });
623
+
624
+ it("should always use version from input when specified (implementationName@version)", async () => {
625
+ const manifest = {
626
+ apps: {
627
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
628
+ },
629
+ };
630
+
631
+ const sdk = createSdk({ manifest })
632
+ .addPlugin(apiPlugin)
633
+ .addPlugin(manifestPlugin);
634
+ const context = sdk.getContext();
635
+
636
+ // Input version should take precedence over manifest version
637
+ const resolved = await context.resolveAppKeys({
638
+ appKeys: ["SlackCLIAPI@1.2.3"],
639
+ });
640
+
641
+ expect(resolved).toHaveLength(1);
642
+ expect(resolved[0]).toEqual({
643
+ implementationName: "SlackCLIAPI",
644
+ version: "1.2.3", // Should use input version, not manifest version
645
+ lookupAppKey: "SlackCLIAPI",
646
+ });
647
+ });
648
+ });
409
649
  });
410
650
 
411
- describe("loadManifestFromFile", () => {
651
+ describe("readManifestFromFile", () => {
412
652
  const mockManifestContent = JSON.stringify({
413
653
  apps: {
414
654
  slack: {
@@ -426,16 +666,13 @@ describe("loadManifestFromFile", () => {
426
666
  vi.clearAllMocks();
427
667
  });
428
668
 
429
- it("should load and parse manifest from file", () => {
430
- mockReadFileSync.mockReturnValue(mockManifestContent);
669
+ it("should read and parse manifest from file", async () => {
670
+ mockReadFile.mockResolvedValue(mockManifestContent);
431
671
 
432
- const result = loadManifestFromFile("manifest.json");
672
+ const result = await readManifestFromFile("manifest.json");
433
673
 
434
674
  expect(mockResolve).toHaveBeenCalledWith("manifest.json");
435
- expect(mockReadFileSync).toHaveBeenCalledWith(
436
- "/resolved/manifest.json",
437
- "utf8",
438
- );
675
+ expect(mockReadFile).toHaveBeenCalledWith("/resolved/manifest.json");
439
676
  expect(result).toEqual({
440
677
  apps: {
441
678
  slack: {
@@ -450,21 +687,90 @@ describe("loadManifestFromFile", () => {
450
687
  });
451
688
  });
452
689
 
453
- it("should handle file read errors", () => {
454
- mockReadFileSync.mockImplementation(() => {
455
- throw new Error("File not found");
456
- });
690
+ it("should handle file read errors", async () => {
691
+ mockReadFile.mockRejectedValue(new Error("File not found"));
457
692
 
458
- const result = loadManifestFromFile("nonexistent.json");
693
+ const result = await readManifestFromFile("nonexistent.json");
459
694
 
460
695
  expect(result).toBeNull();
461
696
  });
462
697
 
463
- it("should handle invalid JSON content", () => {
464
- mockReadFileSync.mockReturnValue("invalid json");
698
+ it("should handle invalid JSON content", async () => {
699
+ mockReadFile.mockResolvedValue("invalid json");
465
700
 
466
- const result = loadManifestFromFile("invalid.json");
701
+ const result = await readManifestFromFile("invalid.json");
467
702
 
468
703
  expect(result).toBeNull();
469
704
  });
705
+
706
+ describe("getPreferredManifestEntryKey", () => {
707
+ let mockApi: ApiClient;
708
+
709
+ beforeEach(() => {
710
+ mockApi = {
711
+ get: vi.fn(),
712
+ } as any;
713
+ });
714
+
715
+ it("should prefer slug when available", async () => {
716
+ const result = await getPreferredManifestEntryKey({
717
+ appKey: "slack",
718
+ api: mockApi,
719
+ });
720
+
721
+ expect(result).toBe("slack");
722
+ });
723
+
724
+ it("should look up slug for implementation name", async () => {
725
+ // Mock API response for implementation name lookup
726
+ mockApi.get = vi.fn().mockResolvedValueOnce({
727
+ results: [
728
+ {
729
+ key: "SlackCLIAPI",
730
+ slug: "slack",
731
+ version: "1.0.0",
732
+ current_implementation_id: "SlackCLIAPI@1.0.0",
733
+ },
734
+ ],
735
+ next: null,
736
+ });
737
+
738
+ const result = await getPreferredManifestEntryKey({
739
+ appKey: "SlackCLIAPI",
740
+ api: mockApi,
741
+ });
742
+
743
+ expect(result).toBe("slack");
744
+ expect(mockApi.get).toHaveBeenCalledWith(
745
+ "/api/v4/implementations-meta/lookup/",
746
+ {
747
+ searchParams: { selected_apis: "SlackCLIAPI" },
748
+ },
749
+ );
750
+ });
751
+
752
+ it("should fall back to implementation name if slug lookup fails", async () => {
753
+ // Mock API to return empty results
754
+ mockApi.get = vi.fn().mockResolvedValueOnce({
755
+ results: [],
756
+ next: null,
757
+ });
758
+
759
+ const result = await getPreferredManifestEntryKey({
760
+ appKey: "SlackCLIAPI",
761
+ api: mockApi,
762
+ });
763
+
764
+ expect(result).toBe("SlackCLIAPI");
765
+ });
766
+
767
+ it("should fall back to original key if no slug or implementation name", async () => {
768
+ const result = await getPreferredManifestEntryKey({
769
+ appKey: "some-random-key",
770
+ api: mockApi,
771
+ });
772
+
773
+ expect(result).toBe("some-random-key");
774
+ });
775
+ });
470
776
  });