@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,17 +1,16 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { manifestPlugin, loadManifestFromFile } from "./index";
2
+ import { manifestPlugin, readManifestFromFile, findManifestEntry, getPreferredManifestEntryKey, } from "./index";
3
3
  import { createSdk } from "../../sdk";
4
- // Mock fs and path modules
5
- vi.mock("fs", () => ({
6
- readFileSync: vi.fn(),
4
+ // Mock file-utils module
5
+ vi.mock("../../utils/file-utils", () => ({
6
+ readFile: vi.fn(),
7
+ writeFile: vi.fn(),
8
+ resolve: vi.fn((path) => Promise.resolve(`/resolved/${path}`)),
7
9
  }));
8
- vi.mock("path", () => ({
9
- resolve: vi.fn((path) => `/resolved/${path}`),
10
- }));
11
- import { readFileSync } from "fs";
12
- import { resolve } from "path";
13
- const mockReadFileSync = vi.mocked(readFileSync);
10
+ import { readFile, resolve } from "../../utils/file-utils";
11
+ const mockReadFile = vi.mocked(readFile);
14
12
  const mockResolve = vi.mocked(resolve);
13
+ const mockResolveAppKeys = vi.fn();
15
14
  describe("manifestPlugin", () => {
16
15
  const mockManifest = {
17
16
  apps: {
@@ -32,57 +31,91 @@ describe("manifestPlugin", () => {
32
31
  vi.clearAllMocks();
33
32
  vi.spyOn(console, "warn").mockImplementation(() => { });
34
33
  mockApiClient = {
35
- get: vi.fn().mockResolvedValue({
36
- count: 1,
37
- next: null,
38
- previous: null,
39
- results: [
40
- {
41
- selected_api: "SlackCLIAPI@1.29.0",
42
- app_id: null,
43
- service_id: null,
44
- auth_type: "oauth",
45
- auth_fields: [
34
+ get: vi.fn().mockImplementation((url, options) => {
35
+ if (url === "/api/v4/implementations/") {
36
+ // Mock for manifest entries (versioned)
37
+ return Promise.resolve({
38
+ count: 1,
39
+ next: null,
40
+ previous: null,
41
+ results: [
46
42
  {
47
- key: "access_token",
48
- required: true,
49
- type: "unicode",
50
- computed: true,
43
+ selected_api: "SlackCLIAPI@1.29.0",
44
+ app_id: null,
45
+ service_id: null,
46
+ auth_type: "oauth",
47
+ auth_fields: [
48
+ {
49
+ key: "access_token",
50
+ required: true,
51
+ type: "unicode",
52
+ computed: true,
53
+ },
54
+ ],
55
+ is_deprecated: false,
56
+ is_private_only: false,
57
+ is_invite_only: false,
58
+ is_beta: false,
59
+ is_premium: false,
60
+ is_hidden: false,
61
+ name: "Slack (1.29.0)",
62
+ name_clean: "Slack",
63
+ version: "1.29.0",
64
+ slug: null,
65
+ images: {
66
+ url_16x16: "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",
67
+ url_32x32: "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",
68
+ url_64x64: "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",
69
+ url_128x128: "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",
70
+ },
71
+ primary_color: null,
72
+ secondary_color: null,
73
+ classification: "third-party",
74
+ current_implementation: "SlackCLIAPI@1.30.0",
75
+ is_adoptable: true,
76
+ is_usable: true,
77
+ update_cta_level: "info",
78
+ update_cta_message: "This version is legacy. Consider updating to the latest version for better support.",
79
+ badges: [
80
+ {
81
+ label: "Legacy",
82
+ color: "primary",
83
+ tooltip_markdown: "This version is legacy. Consider updating to the latest version for better support.",
84
+ },
85
+ ],
51
86
  },
52
87
  ],
53
- is_deprecated: false,
54
- is_private_only: false,
55
- is_invite_only: false,
56
- is_beta: false,
57
- is_premium: false,
58
- is_hidden: false,
59
- name: "Slack (1.29.0)",
60
- name_clean: "Slack",
61
- version: "1.29.0",
62
- slug: null,
63
- images: {
64
- url_16x16: "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",
65
- url_32x32: "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",
66
- url_64x64: "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",
67
- url_128x128: "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",
68
- },
69
- primary_color: null,
70
- secondary_color: null,
71
- classification: "third-party",
72
- current_implementation: "SlackCLIAPI@1.30.0",
73
- is_adoptable: true,
74
- is_usable: true,
75
- update_cta_level: "info",
76
- update_cta_message: "This version is legacy. Consider updating to the latest version for better support.",
77
- badges: [
88
+ });
89
+ }
90
+ else if (url === "/api/v4/implementations-meta/lookup/") {
91
+ // Mock for implementations-meta/lookup fallback
92
+ const searchParams = options?.searchParams || {};
93
+ // Check if we're looking for a nonexistent app
94
+ if (searchParams.slugs?.includes("nonexistent") ||
95
+ searchParams.selected_apis?.includes("nonexistent")) {
96
+ return Promise.resolve({
97
+ count: 0,
98
+ next: null,
99
+ previous: null,
100
+ results: [],
101
+ });
102
+ }
103
+ // Return successful response for known apps (like slack)
104
+ return Promise.resolve({
105
+ count: 1,
106
+ next: null,
107
+ previous: null,
108
+ results: [
78
109
  {
79
- label: "Legacy",
80
- color: "primary",
81
- tooltip_markdown: "This version is legacy. Consider updating to the latest version for better support.",
110
+ id: "SlackCLIAPI@1.30.0",
111
+ name: "Slack",
112
+ slug: "slack",
113
+ images: {},
82
114
  },
83
115
  ],
84
- },
85
- ],
116
+ });
117
+ }
118
+ return Promise.resolve({ count: 0, results: [] });
86
119
  }),
87
120
  };
88
121
  mockSdk = {
@@ -117,53 +150,38 @@ describe("manifestPlugin", () => {
117
150
  },
118
151
  });
119
152
  function createTestSdk(options = {}) {
120
- return createSdk(options)
121
- .addPlugin(apiPlugin)
122
- .addPlugin(listAppsMockPlugin)
123
- .addPlugin(manifestPlugin);
153
+ return createSdk(options).addPlugin(apiPlugin).addPlugin(manifestPlugin);
124
154
  }
125
155
  describe("plugin initialization", () => {
126
156
  it("should provide helper functions with direct manifest", () => {
127
157
  const sdk = createTestSdk({ manifest: mockManifest });
128
158
  const context = sdk.getContext();
129
159
  expect(context.getVersionedImplementationId).toBeInstanceOf(Function);
130
- expect(context.getImplementation).toBeInstanceOf(Function);
131
- expect(context.getManifestEntry).toBeInstanceOf(Function);
132
160
  });
133
- it("should provide helper functions with manifestPath", () => {
134
- mockReadFileSync.mockReturnValue(mockManifestContent);
161
+ it("should provide helper functions with manifestPath", async () => {
162
+ mockReadFile.mockResolvedValue(mockManifestContent);
135
163
  const sdk = createTestSdk({ manifestPath: "manifest.json" });
136
164
  const context = sdk.getContext();
137
165
  expect(context.getVersionedImplementationId).toBeInstanceOf(Function);
138
- expect(context.getImplementation).toBeInstanceOf(Function);
139
- expect(context.getManifestEntry).toBeInstanceOf(Function);
140
166
  // Verify file operations happen when functions are called
141
- const manifestEntry = context.getManifestEntry("slack");
167
+ await context.getVersionedImplementationId("slack");
142
168
  expect(mockResolve).toHaveBeenCalledWith("manifest.json");
143
- expect(mockReadFileSync).toHaveBeenCalledWith("/resolved/manifest.json", "utf8");
144
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
169
+ expect(mockReadFile).toHaveBeenCalledWith("/resolved/manifest.json");
145
170
  });
146
171
  it("should provide helper functions when no manifest or path provided", () => {
147
172
  const sdk = createTestSdk();
148
173
  const context = sdk.getContext();
149
174
  expect(context.getVersionedImplementationId).toBeInstanceOf(Function);
150
- expect(context.getImplementation).toBeInstanceOf(Function);
151
- expect(context.getManifestEntry).toBeInstanceOf(Function);
152
- // Verify that no manifest entry is found
153
- const manifestEntry = context.getManifestEntry("slack");
154
- expect(manifestEntry).toBeNull();
175
+ // Manifest functions are available even when no manifest is provided
155
176
  });
156
177
  it("should prioritize direct manifest over manifestPath", () => {
157
- mockReadFileSync.mockReturnValue('{"apps": {"other": {"implementationName": "Other", "version": "1.0.0"}}}');
158
- const sdk = createTestSdk({
178
+ mockReadFile.mockResolvedValue('{"apps": {"other": {"implementationName": "Other", "version": "1.0.0"}}}');
179
+ createTestSdk({
159
180
  manifest: mockManifest,
160
181
  manifestPath: "manifest.json",
161
182
  });
162
- const context = sdk.getContext();
163
- // Verify that direct manifest is used (should find slack app)
164
- const manifestEntry = context.getManifestEntry("slack");
165
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
166
- expect(mockReadFileSync).not.toHaveBeenCalled();
183
+ // Verify that direct manifest is used and file loading is not needed
184
+ expect(mockReadFile).not.toHaveBeenCalled();
167
185
  });
168
186
  });
169
187
  describe("getVersionedImplementationId function", () => {
@@ -189,43 +207,10 @@ describe("manifestPlugin", () => {
189
207
  expect(result).toBeNull();
190
208
  });
191
209
  });
192
- describe("getImplementation function", () => {
193
- it("should return app from manifest when available", async () => {
194
- const sdk = createTestSdk({ manifest: mockManifest });
195
- const context = sdk.getContext();
196
- const result = await context.getImplementation("slack");
197
- expect(result).toBeDefined();
198
- expect(mockApiClient.get).toHaveBeenCalledWith("/api/v4/implementations/", {
199
- searchParams: {
200
- selected_apis: "SlackCLIAPI@1.21.1",
201
- },
202
- });
203
- });
204
- it("should fetch app from API when not in manifest", async () => {
205
- const sdk = createTestSdk();
206
- const context = sdk.getContext();
207
- const result = await context.getImplementation("slack");
208
- expect(result).toBeDefined();
209
- expect(result?.title).toBe("Slack");
210
- expect(result?.current_implementation_id).toBe("SlackCLIAPI@1.30.0");
211
- });
212
- it("should return null when app cannot be fetched", async () => {
213
- mockSdk.listApps = vi.fn().mockReturnValue({
214
- items: vi.fn().mockReturnValue([][Symbol.iterator]()),
215
- });
216
- const sdk = createTestSdk();
217
- const context = sdk.getContext();
218
- const result = await context.getImplementation("nonexistent");
219
- expect(result).toBeNull();
220
- });
221
- });
222
210
  describe("manifest parsing", () => {
223
211
  it("should parse valid manifest content", () => {
224
- const sdk = createTestSdk({ manifest: mockManifest });
225
- const context = sdk.getContext();
226
- // Verify manifest content is accessible through helper functions
227
- const manifestEntry = context.getManifestEntry("slack");
228
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
212
+ createTestSdk({ manifest: mockManifest });
213
+ // Verify manifest is parsed correctly for other functions to use
229
214
  });
230
215
  it("should handle manifest with missing version", () => {
231
216
  const manifestWithoutVersion = {
@@ -235,66 +220,51 @@ describe("manifestPlugin", () => {
235
220
  },
236
221
  },
237
222
  };
238
- const sdk = createTestSdk({ manifest: manifestWithoutVersion });
239
- const context = sdk.getContext();
240
- // Verify manifest entry is accessible and matches expected structure
241
- const manifestEntry = context.getManifestEntry("slack");
242
- expect(manifestEntry).toEqual(manifestWithoutVersion.apps.slack);
223
+ createTestSdk({ manifest: manifestWithoutVersion });
224
+ // Verify manifest with missing version is handled correctly
243
225
  });
244
226
  it("should handle empty manifest", () => {
245
- const sdk = createTestSdk({ manifest: {} });
246
- const context = sdk.getContext();
247
- // Verify that no manifest entry is found for any app
248
- const manifestEntry = context.getManifestEntry("slack");
249
- expect(manifestEntry).toBeNull();
227
+ createTestSdk({ manifest: {} });
228
+ // Verify empty manifest is handled correctly
250
229
  });
251
230
  });
252
231
  describe("file loading", () => {
253
- it("should load manifest from file successfully", () => {
254
- mockReadFileSync.mockReturnValue(mockManifestContent);
232
+ it("should load manifest from file successfully", async () => {
233
+ mockReadFile.mockResolvedValue(mockManifestContent);
255
234
  const sdk = createTestSdk({ manifestPath: "manifest.json" });
256
235
  const context = sdk.getContext();
257
236
  // Verify that file operations happen when functions are called
258
- const manifestEntry = context.getManifestEntry("slack");
237
+ await context.getVersionedImplementationId("slack");
259
238
  expect(mockResolve).toHaveBeenCalledWith("manifest.json");
260
- expect(mockReadFileSync).toHaveBeenCalledWith("/resolved/manifest.json", "utf8");
261
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
239
+ expect(mockReadFile).toHaveBeenCalledWith("/resolved/manifest.json");
262
240
  });
263
- it("should handle file read errors gracefully", () => {
264
- mockReadFileSync.mockImplementation(() => {
265
- throw new Error("File not found");
266
- });
241
+ it("should handle file read errors gracefully", async () => {
242
+ mockReadFile.mockRejectedValue(new Error("File not found"));
267
243
  const sdk = createTestSdk({ manifestPath: "nonexistent.json" });
268
244
  const context = sdk.getContext();
269
- // Verify that no manifest entry is found when file loading fails
270
- const manifestEntry = context.getManifestEntry("slack");
271
- expect(manifestEntry).toBeNull();
272
- expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Failed to load manifest from nonexistent.json"));
245
+ // Trigger file loading by calling a function
246
+ await context.getVersionedImplementationId("slack");
247
+ // Verify file loading errors are handled gracefully
248
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Failed to read manifest from nonexistent.json"));
273
249
  });
274
- it("should handle invalid JSON gracefully", () => {
275
- mockReadFileSync.mockReturnValue("invalid json content");
250
+ it("should handle invalid JSON gracefully", async () => {
251
+ mockReadFile.mockResolvedValue("invalid json content");
276
252
  const sdk = createTestSdk({ manifestPath: "invalid.json" });
277
253
  const context = sdk.getContext();
278
- // Verify that no manifest entry is found when JSON parsing fails
279
- const manifestEntry = context.getManifestEntry("slack");
280
- expect(manifestEntry).toBeNull();
254
+ // Trigger file loading by calling a function
255
+ await context.getVersionedImplementationId("slack");
256
+ // Verify JSON parsing errors are handled gracefully
281
257
  expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Failed to parse manifest from /resolved/invalid.json"), expect.any(SyntaxError));
282
258
  });
283
259
  it("should handle JSON without apps property", () => {
284
- mockReadFileSync.mockReturnValue('{"other": "data"}');
285
- const sdk = createTestSdk({ manifestPath: "no-apps.json" });
286
- const context = sdk.getContext();
287
- // Verify that no manifest entry is found when apps property is missing
288
- const manifestEntry = context.getManifestEntry("slack");
289
- expect(manifestEntry).toBeNull();
260
+ mockReadFile.mockResolvedValue('{"other": "data"}');
261
+ createTestSdk({ manifestPath: "no-apps.json" });
262
+ // Verify missing apps property is handled correctly
290
263
  });
291
264
  it("should handle JSON with invalid apps format", () => {
292
- mockReadFileSync.mockReturnValue('{"apps": "not an object"}');
293
- const sdk = createTestSdk({ manifestPath: "invalid-apps.json" });
294
- const context = sdk.getContext();
295
- // Verify that no manifest entry is found when apps format is invalid
296
- const manifestEntry = context.getManifestEntry("slack");
297
- expect(manifestEntry).toBeNull();
265
+ mockReadFile.mockResolvedValue('{"apps": "not an object"}');
266
+ createTestSdk({ manifestPath: "invalid-apps.json" });
267
+ // Verify invalid apps format is handled correctly
298
268
  });
299
269
  });
300
270
  describe("integration with SDK", () => {
@@ -304,15 +274,246 @@ describe("manifestPlugin", () => {
304
274
  .addPlugin(listAppsMockPlugin)
305
275
  .addPlugin(manifestPlugin);
306
276
  const context = sdk.getContext();
307
- // Verify that the manifest data is used correctly
308
- const manifestEntry = context.getManifestEntry("slack");
309
- expect(manifestEntry).toEqual(mockManifest.apps.slack);
277
+ // Verify that the manifest data is used correctly by other functions
310
278
  const versionedId = await context.getVersionedImplementationId("slack");
311
279
  expect(versionedId).toBe("SlackCLIAPI@1.21.1");
312
280
  });
313
281
  });
282
+ describe("findManifestEntry function", () => {
283
+ it("should find entry by direct key match", () => {
284
+ const manifest = {
285
+ apps: {
286
+ slack: { implementationName: "SlackCLIAPI", version: "1.21.1" },
287
+ gmail: { implementationName: "GmailCLIAPI", version: "2.0.0" },
288
+ },
289
+ };
290
+ const result = findManifestEntry({ appKey: "slack", manifest });
291
+ expect(result).toEqual([
292
+ "slack",
293
+ { implementationName: "SlackCLIAPI", version: "1.21.1" },
294
+ ]);
295
+ });
296
+ it("should find entry by implementation name match", () => {
297
+ const manifest = {
298
+ apps: {
299
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
300
+ GmailCLIAPI: { implementationName: "GmailCLIAPI", version: "2.0.0" },
301
+ },
302
+ };
303
+ const result = findManifestEntry({ appKey: "SlackCLIAPI", manifest });
304
+ expect(result).toEqual([
305
+ "SlackCLIAPI",
306
+ { implementationName: "SlackCLIAPI", version: "1.21.1" },
307
+ ]);
308
+ });
309
+ it("should find existing key when searching by implementation name", () => {
310
+ const manifest = {
311
+ apps: {
312
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
313
+ },
314
+ };
315
+ // This is the key test: searching by implementation name should find existing entry
316
+ const result = findManifestEntry({ appKey: "SlackCLIAPI", manifest });
317
+ expect(result).toEqual([
318
+ "SlackCLIAPI",
319
+ { implementationName: "SlackCLIAPI", version: "1.21.1" },
320
+ ]);
321
+ });
322
+ it("should return null when no match found", () => {
323
+ const manifest = {
324
+ apps: {
325
+ gmail: { implementationName: "GmailCLIAPI", version: "2.0.0" },
326
+ },
327
+ };
328
+ const result = findManifestEntry({ appKey: "slack", manifest });
329
+ expect(result).toBeNull();
330
+ });
331
+ it("should handle snake-cased slugs", () => {
332
+ const manifest = {
333
+ apps: {
334
+ "google-sheets": {
335
+ implementationName: "GoogleSheetsCLIAPI",
336
+ version: "1.0.0",
337
+ },
338
+ },
339
+ };
340
+ const result = findManifestEntry({ appKey: "google_sheets", manifest });
341
+ expect(result).toEqual([
342
+ "google-sheets",
343
+ { implementationName: "GoogleSheetsCLIAPI", version: "1.0.0" },
344
+ ]);
345
+ });
346
+ it("should NOT find entry when searching slug against implementation name key (current limitation)", () => {
347
+ const manifest = {
348
+ apps: {
349
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
350
+ },
351
+ };
352
+ // This will fail - we search by "slack" but can't find "SlackCLIAPI" entry
353
+ // This is the core issue we need to solve
354
+ const result = findManifestEntry({ appKey: "slack", manifest });
355
+ expect(result).toBeNull(); // Current behavior - doesn't find it
356
+ // We need to fix this so it CAN find existing implementation name entries
357
+ });
358
+ });
359
+ describe("updateManifestEntry with slug-to-implementation matching", () => {
360
+ it("should find existing implementation name entry when input is slug", async () => {
361
+ // Mock existing manifest with implementation name as key
362
+ mockReadFile.mockResolvedValue(JSON.stringify({
363
+ apps: {
364
+ SlackCLIAPI: {
365
+ implementationName: "SlackCLIAPI",
366
+ version: "1.21.1",
367
+ },
368
+ },
369
+ }));
370
+ // Mock resolveAppKeys to resolve "slack" to "SlackCLIAPI"
371
+ mockResolveAppKeys.mockResolvedValue([
372
+ {
373
+ slug: "slack",
374
+ implementationName: "SlackCLIAPI",
375
+ version: "1.30.0",
376
+ lookupAppKey: "slack",
377
+ },
378
+ ]);
379
+ const sdk = createTestSdk();
380
+ const context = sdk.getContext();
381
+ // This should now find the existing "SlackCLIAPI" entry
382
+ const [manifestKey] = await context.updateManifestEntry("slack", // Input is slug
383
+ { implementationName: "SlackCLIAPI", version: "1.30.0" });
384
+ // Should return the existing key, not create a new one
385
+ expect(manifestKey).toBe("SlackCLIAPI");
386
+ });
387
+ });
388
+ describe("resolveAppKeys function", () => {
389
+ it("should resolve slug to manifest entry even when manifest uses implementation name as key", async () => {
390
+ const manifest = {
391
+ apps: {
392
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
393
+ },
394
+ };
395
+ // Mock API to return slack app data when looking up "slack"
396
+ mockApiClient.get = vi.fn().mockResolvedValue({
397
+ count: 1,
398
+ next: null,
399
+ previous: null,
400
+ results: [
401
+ {
402
+ id: "SlackCLIAPI@1.30.0",
403
+ name: "Slack",
404
+ slug: "slack",
405
+ key: "SlackCLIAPI", // This is the implementation name
406
+ version: "1.30.0",
407
+ },
408
+ ],
409
+ });
410
+ // Test resolveAppKeys directly using the manifest plugin
411
+ const sdk = createSdk({ manifest })
412
+ .addPlugin(apiPlugin)
413
+ .addPlugin(manifestPlugin);
414
+ const context = sdk.getContext();
415
+ const resolved = await context.resolveAppKeys({ appKeys: ["slack"] });
416
+ // Should find the manifest entry and use its version (1.21.1) instead of latest (1.30.0)
417
+ expect(resolved).toHaveLength(1);
418
+ expect(resolved[0]).toEqual({
419
+ slug: "slack",
420
+ implementationName: "SlackCLIAPI",
421
+ version: "1.21.1", // Should use manifest version, not API version
422
+ lookupAppKey: "slack",
423
+ });
424
+ });
425
+ it("should fall back to API version when no manifest entry found", async () => {
426
+ const manifest = { apps: {} }; // Empty manifest
427
+ // Mock API to return slack app data
428
+ mockApiClient.get = vi.fn().mockResolvedValue({
429
+ count: 1,
430
+ next: null,
431
+ previous: null,
432
+ results: [
433
+ {
434
+ id: "SlackCLIAPI@1.30.0",
435
+ name: "Slack",
436
+ slug: "slack",
437
+ key: "SlackCLIAPI",
438
+ version: "1.30.0",
439
+ },
440
+ ],
441
+ });
442
+ const sdk = createSdk({ manifest })
443
+ .addPlugin(apiPlugin)
444
+ .addPlugin(manifestPlugin);
445
+ const context = sdk.getContext();
446
+ const resolved = await context.resolveAppKeys({ appKeys: ["slack"] });
447
+ // Should use API version since no manifest entry found
448
+ expect(resolved).toHaveLength(1);
449
+ expect(resolved[0]).toEqual({
450
+ slug: "slack",
451
+ implementationName: "SlackCLIAPI",
452
+ version: "1.30.0", // Should use API version
453
+ lookupAppKey: "slack",
454
+ });
455
+ });
456
+ it("should always use version from input when specified (slug@version)", async () => {
457
+ const manifest = {
458
+ apps: {
459
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
460
+ },
461
+ };
462
+ // Mock API to return slack app data
463
+ mockApiClient.get = vi.fn().mockResolvedValue({
464
+ count: 1,
465
+ next: null,
466
+ previous: null,
467
+ results: [
468
+ {
469
+ id: "SlackCLIAPI@1.30.0",
470
+ name: "Slack",
471
+ slug: "slack",
472
+ key: "SlackCLIAPI",
473
+ version: "1.30.0",
474
+ },
475
+ ],
476
+ });
477
+ const sdk = createSdk({ manifest })
478
+ .addPlugin(apiPlugin)
479
+ .addPlugin(manifestPlugin);
480
+ const context = sdk.getContext();
481
+ // Input version should take precedence over both manifest (1.21.1) and API (1.30.0)
482
+ const resolved = await context.resolveAppKeys({
483
+ appKeys: ["slack@1.2.3"],
484
+ });
485
+ expect(resolved).toHaveLength(1);
486
+ expect(resolved[0]).toEqual({
487
+ slug: "slack",
488
+ implementationName: "SlackCLIAPI",
489
+ version: "1.2.3", // Should use input version, not manifest or API
490
+ lookupAppKey: "slack",
491
+ });
492
+ });
493
+ it("should always use version from input when specified (implementationName@version)", async () => {
494
+ const manifest = {
495
+ apps: {
496
+ SlackCLIAPI: { implementationName: "SlackCLIAPI", version: "1.21.1" },
497
+ },
498
+ };
499
+ const sdk = createSdk({ manifest })
500
+ .addPlugin(apiPlugin)
501
+ .addPlugin(manifestPlugin);
502
+ const context = sdk.getContext();
503
+ // Input version should take precedence over manifest version
504
+ const resolved = await context.resolveAppKeys({
505
+ appKeys: ["SlackCLIAPI@1.2.3"],
506
+ });
507
+ expect(resolved).toHaveLength(1);
508
+ expect(resolved[0]).toEqual({
509
+ implementationName: "SlackCLIAPI",
510
+ version: "1.2.3", // Should use input version, not manifest version
511
+ lookupAppKey: "SlackCLIAPI",
512
+ });
513
+ });
514
+ });
314
515
  });
315
- describe("loadManifestFromFile", () => {
516
+ describe("readManifestFromFile", () => {
316
517
  const mockManifestContent = JSON.stringify({
317
518
  apps: {
318
519
  slack: {
@@ -328,11 +529,11 @@ describe("loadManifestFromFile", () => {
328
529
  beforeEach(() => {
329
530
  vi.clearAllMocks();
330
531
  });
331
- it("should load and parse manifest from file", () => {
332
- mockReadFileSync.mockReturnValue(mockManifestContent);
333
- const result = loadManifestFromFile("manifest.json");
532
+ it("should read and parse manifest from file", async () => {
533
+ mockReadFile.mockResolvedValue(mockManifestContent);
534
+ const result = await readManifestFromFile("manifest.json");
334
535
  expect(mockResolve).toHaveBeenCalledWith("manifest.json");
335
- expect(mockReadFileSync).toHaveBeenCalledWith("/resolved/manifest.json", "utf8");
536
+ expect(mockReadFile).toHaveBeenCalledWith("/resolved/manifest.json");
336
537
  expect(result).toEqual({
337
538
  apps: {
338
539
  slack: {
@@ -346,16 +547,70 @@ describe("loadManifestFromFile", () => {
346
547
  },
347
548
  });
348
549
  });
349
- it("should handle file read errors", () => {
350
- mockReadFileSync.mockImplementation(() => {
351
- throw new Error("File not found");
352
- });
353
- const result = loadManifestFromFile("nonexistent.json");
550
+ it("should handle file read errors", async () => {
551
+ mockReadFile.mockRejectedValue(new Error("File not found"));
552
+ const result = await readManifestFromFile("nonexistent.json");
354
553
  expect(result).toBeNull();
355
554
  });
356
- it("should handle invalid JSON content", () => {
357
- mockReadFileSync.mockReturnValue("invalid json");
358
- const result = loadManifestFromFile("invalid.json");
555
+ it("should handle invalid JSON content", async () => {
556
+ mockReadFile.mockResolvedValue("invalid json");
557
+ const result = await readManifestFromFile("invalid.json");
359
558
  expect(result).toBeNull();
360
559
  });
560
+ describe("getPreferredManifestEntryKey", () => {
561
+ let mockApi;
562
+ beforeEach(() => {
563
+ mockApi = {
564
+ get: vi.fn(),
565
+ };
566
+ });
567
+ it("should prefer slug when available", async () => {
568
+ const result = await getPreferredManifestEntryKey({
569
+ appKey: "slack",
570
+ api: mockApi,
571
+ });
572
+ expect(result).toBe("slack");
573
+ });
574
+ it("should look up slug for implementation name", async () => {
575
+ // Mock API response for implementation name lookup
576
+ mockApi.get = vi.fn().mockResolvedValueOnce({
577
+ results: [
578
+ {
579
+ key: "SlackCLIAPI",
580
+ slug: "slack",
581
+ version: "1.0.0",
582
+ current_implementation_id: "SlackCLIAPI@1.0.0",
583
+ },
584
+ ],
585
+ next: null,
586
+ });
587
+ const result = await getPreferredManifestEntryKey({
588
+ appKey: "SlackCLIAPI",
589
+ api: mockApi,
590
+ });
591
+ expect(result).toBe("slack");
592
+ expect(mockApi.get).toHaveBeenCalledWith("/api/v4/implementations-meta/lookup/", {
593
+ searchParams: { selected_apis: "SlackCLIAPI" },
594
+ });
595
+ });
596
+ it("should fall back to implementation name if slug lookup fails", async () => {
597
+ // Mock API to return empty results
598
+ mockApi.get = vi.fn().mockResolvedValueOnce({
599
+ results: [],
600
+ next: null,
601
+ });
602
+ const result = await getPreferredManifestEntryKey({
603
+ appKey: "SlackCLIAPI",
604
+ api: mockApi,
605
+ });
606
+ expect(result).toBe("SlackCLIAPI");
607
+ });
608
+ it("should fall back to original key if no slug or implementation name", async () => {
609
+ const result = await getPreferredManifestEntryKey({
610
+ appKey: "some-random-key",
611
+ api: mockApi,
612
+ });
613
+ expect(result).toBe("some-random-key");
614
+ });
615
+ });
361
616
  });