@hubspot/cli 7.8.12-experimental.1 → 7.9.0-beta.1

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 (276) hide show
  1. package/commands/__tests__/cms.test.js +44 -1
  2. package/commands/__tests__/customObject.test.js +22 -1
  3. package/commands/__tests__/project.test.js +2 -0
  4. package/commands/account/auth.js +1 -0
  5. package/commands/auth.js +1 -0
  6. package/commands/{__tests__/remove.test.js → cms/__tests__/delete.test.js} +8 -8
  7. package/commands/{__tests__ → cms/__tests__}/fetch.test.js +3 -3
  8. package/commands/{__tests__ → cms/__tests__}/function.test.js +7 -3
  9. package/commands/{__tests__ → cms/__tests__}/lint.test.js +3 -3
  10. package/commands/{__tests__ → cms/__tests__}/list.test.js +3 -3
  11. package/commands/{__tests__ → cms/__tests__}/mv.test.js +3 -3
  12. package/commands/{__tests__ → cms/__tests__}/theme.test.js +9 -2
  13. package/commands/cms/app/create.d.ts +9 -0
  14. package/commands/cms/app/create.js +82 -0
  15. package/commands/cms/app.d.ts +3 -0
  16. package/commands/cms/app.js +17 -0
  17. package/commands/cms/delete.d.ts +6 -0
  18. package/commands/cms/delete.js +43 -0
  19. package/commands/cms/fetch.d.ts +12 -0
  20. package/commands/cms/fetch.js +79 -0
  21. package/commands/{__tests__ → cms/function/__tests__}/logs.test.js +4 -5
  22. package/commands/cms/function/create.d.ts +12 -0
  23. package/commands/cms/function/create.js +84 -0
  24. package/commands/cms/function/deploy.d.ts +6 -0
  25. package/commands/cms/function/deploy.js +89 -0
  26. package/commands/cms/function/list.d.ts +6 -0
  27. package/commands/cms/function/list.js +60 -0
  28. package/commands/cms/function/logs.d.ts +10 -0
  29. package/commands/cms/function/logs.js +135 -0
  30. package/commands/cms/function/server.d.ts +10 -0
  31. package/commands/cms/function/server.js +69 -0
  32. package/commands/cms/function.d.ts +3 -0
  33. package/commands/cms/function.js +27 -0
  34. package/commands/cms/lint.d.ts +6 -0
  35. package/commands/cms/lint.js +83 -0
  36. package/commands/cms/list.d.ts +6 -0
  37. package/commands/cms/list.js +96 -0
  38. package/commands/cms/module/create.d.ts +11 -0
  39. package/commands/cms/module/create.js +84 -0
  40. package/commands/cms/module/marketplace-validate.d.ts +6 -0
  41. package/commands/cms/module/marketplace-validate.js +45 -0
  42. package/commands/cms/module.d.ts +3 -0
  43. package/commands/cms/module.js +17 -0
  44. package/commands/cms/mv.d.ts +7 -0
  45. package/commands/cms/mv.js +60 -0
  46. package/commands/cms/template/create.d.ts +9 -0
  47. package/commands/cms/template/create.js +72 -0
  48. package/commands/cms/template.d.ts +3 -0
  49. package/commands/cms/template.js +17 -0
  50. package/commands/{theme → cms/theme}/__tests__/marketplace-validate.test.js +2 -2
  51. package/commands/{theme → cms/theme}/__tests__/preview.test.js +2 -2
  52. package/commands/cms/theme/create.d.ts +6 -0
  53. package/commands/cms/theme/create.js +58 -0
  54. package/commands/cms/theme/generate-selectors.d.ts +6 -0
  55. package/commands/cms/theme/generate-selectors.js +171 -0
  56. package/commands/cms/theme/marketplace-validate.d.ts +6 -0
  57. package/commands/cms/theme/marketplace-validate.js +46 -0
  58. package/commands/cms/theme/preview.d.ts +12 -0
  59. package/commands/cms/theme/preview.js +224 -0
  60. package/commands/cms/theme.d.ts +3 -0
  61. package/commands/cms/theme.js +25 -0
  62. package/commands/cms/upload.d.ts +12 -0
  63. package/commands/cms/upload.js +212 -0
  64. package/commands/cms/watch.d.ts +14 -0
  65. package/commands/cms/watch.js +138 -0
  66. package/commands/cms/webpack/create.d.ts +6 -0
  67. package/commands/cms/webpack/create.js +58 -0
  68. package/commands/cms/webpack.d.ts +3 -0
  69. package/commands/cms/webpack.js +17 -0
  70. package/commands/cms.js +26 -0
  71. package/commands/create.js +4 -2
  72. package/commands/customObject/{schema/__tests__/create.test.js → __tests__/createSchema.test.js} +5 -5
  73. package/commands/customObject/{schema/__tests__/delete.test.js → __tests__/deleteSchema.test.js} +5 -5
  74. package/commands/customObject/{schema/__tests__/fetch-all.test.js → __tests__/fetch-all-schemas.test.js} +5 -5
  75. package/commands/customObject/{schema/__tests__/fetch.test.js → __tests__/fetchSchema.test.js} +5 -5
  76. package/commands/customObject/{schema/__tests__/list.test.js → __tests__/listSchemas.test.js} +5 -5
  77. package/commands/customObject/{schema/__tests__/update.test.js → __tests__/updateSchema.test.js} +5 -5
  78. package/commands/customObject/createSchema.d.ts +6 -0
  79. package/commands/customObject/createSchema.js +56 -0
  80. package/commands/customObject/deleteSchema.d.ts +7 -0
  81. package/commands/customObject/deleteSchema.js +69 -0
  82. package/commands/customObject/fetchAllSchemas.d.ts +6 -0
  83. package/commands/customObject/fetchAllSchemas.js +57 -0
  84. package/commands/customObject/fetchSchema.d.ts +7 -0
  85. package/commands/customObject/fetchSchema.js +67 -0
  86. package/commands/customObject/listSchemas.d.ts +4 -0
  87. package/commands/customObject/listSchemas.js +35 -0
  88. package/commands/customObject/schema/create.d.ts +4 -6
  89. package/commands/customObject/schema/create.js +13 -36
  90. package/commands/customObject/schema/delete.d.ts +4 -7
  91. package/commands/customObject/schema/delete.js +15 -50
  92. package/commands/customObject/schema/fetch-all.d.ts +4 -6
  93. package/commands/customObject/schema/fetch-all.js +14 -41
  94. package/commands/customObject/schema/fetch.d.ts +4 -7
  95. package/commands/customObject/schema/fetch.js +14 -49
  96. package/commands/customObject/schema/list.d.ts +4 -4
  97. package/commands/customObject/schema/list.js +10 -19
  98. package/commands/customObject/schema/update.d.ts +4 -7
  99. package/commands/customObject/schema/update.js +15 -50
  100. package/commands/customObject/schema.js +4 -2
  101. package/commands/customObject/updateSchema.d.ts +7 -0
  102. package/commands/customObject/updateSchema.js +71 -0
  103. package/commands/customObject.js +16 -1
  104. package/commands/feedback.js +1 -1
  105. package/commands/fetch.d.ts +4 -12
  106. package/commands/fetch.js +19 -46
  107. package/commands/function/deploy.d.ts +4 -6
  108. package/commands/function/deploy.js +14 -71
  109. package/commands/function/list.d.ts +4 -6
  110. package/commands/function/list.js +14 -40
  111. package/commands/function/server.d.ts +4 -10
  112. package/commands/function/server.js +22 -29
  113. package/commands/function.d.ts +2 -4
  114. package/commands/function.js +25 -14
  115. package/commands/lint.d.ts +4 -6
  116. package/commands/lint.js +13 -65
  117. package/commands/list.d.ts +4 -6
  118. package/commands/list.js +13 -74
  119. package/commands/logs.d.ts +4 -10
  120. package/commands/logs.js +24 -87
  121. package/commands/module/marketplace-validate.d.ts +4 -6
  122. package/commands/module/marketplace-validate.js +15 -27
  123. package/commands/module.d.ts +2 -2
  124. package/commands/module.js +17 -15
  125. package/commands/mv.d.ts +4 -7
  126. package/commands/mv.js +13 -39
  127. package/commands/project/__tests__/add.test.js +12 -12
  128. package/commands/project/__tests__/devUnifiedFlow.test.js +32 -0
  129. package/commands/project/__tests__/list.test.js +31 -0
  130. package/commands/project/__tests__/migrate.test.js +1 -0
  131. package/commands/project/add.d.ts +2 -2
  132. package/commands/project/add.js +3 -2
  133. package/commands/project/create.js +1 -1
  134. package/commands/project/dev/deprecatedFlow.js +2 -2
  135. package/commands/project/dev/index.js +5 -5
  136. package/commands/project/dev/unifiedFlow.js +8 -3
  137. package/commands/project/download.js +5 -2
  138. package/commands/project/installDeps.d.ts +2 -2
  139. package/commands/project/installDeps.js +1 -0
  140. package/commands/project/list.d.ts +4 -0
  141. package/commands/project/list.js +62 -0
  142. package/commands/project/migrate.js +5 -2
  143. package/commands/project.js +2 -0
  144. package/commands/remove.d.ts +4 -6
  145. package/commands/remove.js +12 -24
  146. package/commands/testAccount/create.js +2 -2
  147. package/commands/testAccount/delete.js +1 -1
  148. package/commands/theme/generate-selectors.d.ts +4 -6
  149. package/commands/theme/generate-selectors.js +14 -152
  150. package/commands/theme/marketplace-validate.d.ts +4 -6
  151. package/commands/theme/marketplace-validate.js +14 -25
  152. package/commands/theme/preview.d.ts +4 -12
  153. package/commands/theme/preview.js +18 -180
  154. package/commands/theme.d.ts +2 -2
  155. package/commands/theme.js +19 -13
  156. package/commands/upload.d.ts +4 -12
  157. package/commands/upload.js +19 -169
  158. package/commands/watch.d.ts +4 -14
  159. package/commands/watch.js +23 -88
  160. package/lang/en.d.ts +561 -425
  161. package/lang/en.js +563 -427
  162. package/lang/en.lyaml +2 -2
  163. package/lib/__tests__/buildAccount.test.js +2 -2
  164. package/lib/__tests__/http.test.js +40 -0
  165. package/lib/buildAccount.d.ts +2 -2
  166. package/lib/buildAccount.js +7 -7
  167. package/lib/configMigrate.js +88 -9
  168. package/lib/constants.d.ts +9 -0
  169. package/lib/constants.js +9 -0
  170. package/lib/generateSelectors.js +1 -1
  171. package/lib/http.d.ts +1 -0
  172. package/lib/http.js +26 -0
  173. package/lib/middleware/autoUpdateMiddleware.d.ts +2 -1
  174. package/lib/middleware/autoUpdateMiddleware.js +12 -2
  175. package/lib/middleware/commandTargetingUtils.d.ts +1 -1
  176. package/lib/middleware/commandTargetingUtils.js +16 -20
  177. package/lib/projects/__tests__/AppDevModeInterface.test.js +95 -109
  178. package/lib/projects/__tests__/DevServerManager.test.d.ts +1 -0
  179. package/lib/projects/__tests__/DevServerManager.test.js +183 -0
  180. package/lib/projects/__tests__/LocalDevProcess.test.js +6 -5
  181. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
  182. package/lib/projects/__tests__/UIExtensionsDevModeInterface.test.d.ts +1 -0
  183. package/lib/projects/__tests__/UIExtensionsDevModeInterface.test.js +161 -0
  184. package/lib/projects/__tests__/deploy.test.js +9 -9
  185. package/lib/projects/__tests__/upload.test.js +2 -2
  186. package/lib/projects/add/__tests__/v2AddComponent.test.d.ts +1 -0
  187. package/lib/projects/add/__tests__/{v3AddComponent.test.js → v2AddComponent.test.js} +35 -35
  188. package/lib/projects/add/{v3AddComponent.d.ts → v2AddComponent.d.ts} +1 -1
  189. package/lib/projects/add/{v3AddComponent.js → v2AddComponent.js} +5 -5
  190. package/lib/projects/create/__tests__/v2.test.d.ts +1 -0
  191. package/lib/projects/create/__tests__/{v3.test.js → v2.test.js} +2 -2
  192. package/lib/projects/create/index.js +2 -2
  193. package/lib/projects/create/{v3.d.ts → v2.d.ts} +3 -3
  194. package/lib/projects/create/{v3.js → v2.js} +3 -3
  195. package/lib/projects/deploy.d.ts +1 -1
  196. package/lib/projects/deploy.js +2 -2
  197. package/lib/projects/localDev/AppDevModeInterface.d.ts +10 -3
  198. package/lib/projects/localDev/AppDevModeInterface.js +132 -105
  199. package/lib/projects/localDev/DevServerManager.d.ts +10 -29
  200. package/lib/projects/localDev/DevServerManager.js +20 -76
  201. package/lib/projects/localDev/DevServerManager_DEPRECATED.d.ts +40 -0
  202. package/lib/projects/localDev/DevServerManager_DEPRECATED.js +120 -0
  203. package/lib/projects/localDev/{LocalDevManager.js → LocalDevManager_DEPRECATED.js} +6 -6
  204. package/lib/projects/localDev/LocalDevProcess.js +3 -2
  205. package/lib/projects/localDev/LocalDevState.d.ts +3 -0
  206. package/lib/projects/localDev/LocalDevState.js +9 -0
  207. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +4 -0
  208. package/lib/projects/localDev/LocalDevWebsocketServer.js +39 -6
  209. package/lib/projects/localDev/UIExtensionsDevModeInterface.d.ts +13 -0
  210. package/lib/projects/localDev/UIExtensionsDevModeInterface.js +37 -0
  211. package/lib/projects/localDev/helpers/account.d.ts +1 -1
  212. package/lib/projects/localDev/helpers/account.js +2 -2
  213. package/lib/projects/localDev/helpers/process.d.ts +1 -0
  214. package/lib/projects/localDev/helpers/process.js +15 -0
  215. package/lib/projects/localDev/helpers/project.js +2 -3
  216. package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +3 -0
  217. package/lib/projects/localDev/localDevWebsocketServerUtils.js +9 -0
  218. package/lib/projects/urls.d.ts +0 -1
  219. package/lib/projects/urls.js +0 -3
  220. package/lib/prompts/__tests__/projectAddPrompt.test.js +10 -10
  221. package/lib/prompts/installAppPrompt.d.ts +1 -6
  222. package/lib/prompts/installAppPrompt.js +1 -6
  223. package/lib/prompts/projectAddPrompt.d.ts +2 -2
  224. package/lib/prompts/projectAddPrompt.js +1 -1
  225. package/lib/theme/__tests__/migrate.test.js +4 -4
  226. package/lib/ui/index.d.ts +2 -0
  227. package/lib/ui/index.js +8 -0
  228. package/lib/ui/uiMessages.d.ts +5 -0
  229. package/lib/ui/uiMessages.js +5 -0
  230. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +2 -2
  231. package/mcp-server/tools/project/__tests__/CreateProjectTool.test.js +1 -1
  232. package/package.json +6 -5
  233. package/types/Cms.d.ts +6 -6
  234. package/lib/projects/localDev/DevServerManagerV2.d.ts +0 -22
  235. package/lib/projects/localDev/DevServerManagerV2.js +0 -81
  236. /package/commands/{customObject/schema → cms}/__tests__/delete.test.d.ts +0 -0
  237. /package/commands/{__tests__ → cms/__tests__}/fetch.test.d.ts +0 -0
  238. /package/commands/{__tests__ → cms/__tests__}/function.test.d.ts +0 -0
  239. /package/commands/{__tests__ → cms/__tests__}/lint.test.d.ts +0 -0
  240. /package/commands/{__tests__ → cms/__tests__}/list.test.d.ts +0 -0
  241. /package/commands/{__tests__ → cms/__tests__}/mv.test.d.ts +0 -0
  242. /package/commands/{__tests__ → cms/__tests__}/theme.test.d.ts +0 -0
  243. /package/commands/{__tests__ → cms/function/__tests__}/logs.test.d.ts +0 -0
  244. /package/commands/{theme → cms/theme}/__tests__/generate-selectors.test.d.ts +0 -0
  245. /package/commands/{theme → cms/theme}/__tests__/generate-selectors.test.js +0 -0
  246. /package/commands/{theme → cms/theme}/__tests__/marketplace-validate.test.d.ts +0 -0
  247. /package/commands/{theme → cms/theme}/__tests__/preview.test.d.ts +0 -0
  248. /package/commands/{__tests__/remove.test.d.ts → customObject/__tests__/createSchema.test.d.ts} +0 -0
  249. /package/commands/customObject/{schema/__tests__/create.test.d.ts → __tests__/deleteSchema.test.d.ts} +0 -0
  250. /package/commands/customObject/{schema/__tests__/fetch-all.test.d.ts → __tests__/fetch-all-schemas.test.d.ts} +0 -0
  251. /package/commands/customObject/{schema/__tests__/fetch.test.d.ts → __tests__/fetchSchema.test.d.ts} +0 -0
  252. /package/commands/customObject/{schema/__tests__/list.test.d.ts → __tests__/listSchemas.test.d.ts} +0 -0
  253. /package/commands/customObject/{schema/__tests__/update.test.d.ts → __tests__/updateSchema.test.d.ts} +0 -0
  254. /package/{lib/projects/add/__tests__/v3AddComponent.test.d.ts → commands/project/__tests__/list.test.d.ts} +0 -0
  255. /package/lib/{projects/create/__tests__/v3.test.d.ts → __tests__/http.test.d.ts} +0 -0
  256. /package/{commands/create → lib/cmsAssets}/api-sample.d.ts +0 -0
  257. /package/{commands/create → lib/cmsAssets}/api-sample.js +0 -0
  258. /package/{commands/create → lib/cmsAssets}/app.d.ts +0 -0
  259. /package/{commands/create → lib/cmsAssets}/app.js +0 -0
  260. /package/{commands/create → lib/cmsAssets}/function.d.ts +0 -0
  261. /package/{commands/create → lib/cmsAssets}/function.js +0 -0
  262. /package/{commands/create → lib/cmsAssets}/index.d.ts +0 -0
  263. /package/{commands/create → lib/cmsAssets}/index.js +0 -0
  264. /package/{commands/create → lib/cmsAssets}/module.d.ts +0 -0
  265. /package/{commands/create → lib/cmsAssets}/module.js +0 -0
  266. /package/{commands/create → lib/cmsAssets}/react-app.d.ts +0 -0
  267. /package/{commands/create → lib/cmsAssets}/react-app.js +0 -0
  268. /package/{commands/create → lib/cmsAssets}/template.d.ts +0 -0
  269. /package/{commands/create → lib/cmsAssets}/template.js +0 -0
  270. /package/{commands/create → lib/cmsAssets}/vue-app.d.ts +0 -0
  271. /package/{commands/create → lib/cmsAssets}/vue-app.js +0 -0
  272. /package/{commands/create → lib/cmsAssets}/webpack-serverless.d.ts +0 -0
  273. /package/{commands/create → lib/cmsAssets}/webpack-serverless.js +0 -0
  274. /package/{commands/create → lib/cmsAssets}/website-theme.d.ts +0 -0
  275. /package/{commands/create → lib/cmsAssets}/website-theme.js +0 -0
  276. /package/lib/projects/localDev/{LocalDevManager.d.ts → LocalDevManager_DEPRECATED.d.ts} +0 -0
@@ -12,8 +12,6 @@ vi.mock('@hubspot/ui-extensions-dev-server', () => {
12
12
  });
13
13
  import { fetchAppInstallationData } from '@hubspot/local-dev-lib/api/localDevAuth';
14
14
  import { fetchAppMetadataByUid, fetchPublicAppProductionInstallCounts, installStaticAuthAppOnTestAccount, } from '@hubspot/local-dev-lib/api/appsDev';
15
- import { DevModeUnifiedInterface as UIEDevModeInterface } from '@hubspot/ui-extensions-dev-server';
16
- import { requestPorts } from '@hubspot/local-dev-lib/portManager';
17
15
  import { getAccountConfig } from '@hubspot/local-dev-lib/config';
18
16
  import AppDevModeInterface from '../localDev/AppDevModeInterface.js';
19
17
  import LocalDevState from '../localDev/LocalDevState.js';
@@ -129,7 +127,16 @@ describe('AppDevModeInterface', () => {
129
127
  getOauthAppInstallUrl.mockReturnValue('http://oauth-install-url');
130
128
  getStaticAuthAppInstallUrl.mockReturnValue('http://static-install-url');
131
129
  installAppAutoPrompt.mockResolvedValue(true);
132
- installAppBrowserPrompt.mockResolvedValue(undefined);
130
+ installAppBrowserPrompt.mockImplementation(async () => {
131
+ setTimeout(() => {
132
+ const addListenerCall = mockLocalDevState.addListener.mock.calls.find(call => call[0] === 'devServerMessage');
133
+ if (addListenerCall) {
134
+ const callback = addListenerCall[1];
135
+ callback(LOCAL_DEV_SERVER_MESSAGE_TYPES.STATIC_AUTH_APP_INSTALL_SUCCESS);
136
+ }
137
+ }, 0);
138
+ return undefined;
139
+ });
133
140
  confirmPrompt.mockResolvedValue(true);
134
141
  installStaticAuthAppOnTestAccount.mockResolvedValue(undefined);
135
142
  // Mock process.exit
@@ -190,16 +197,14 @@ describe('AppDevModeInterface', () => {
190
197
  });
191
198
  it('should return early if no app node exists', async () => {
192
199
  mockLocalDevState.projectNodes = {};
193
- await appDevModeInterface.setup({});
200
+ await appDevModeInterface.setup();
194
201
  expect(fetchAppMetadataByUid).not.toHaveBeenCalled();
195
- expect(UIEDevModeInterface.setup).not.toHaveBeenCalled();
196
202
  });
197
203
  it('should setup successfully with private app', async () => {
198
- await appDevModeInterface.setup({});
204
+ await appDevModeInterface.setup();
199
205
  expect(fetchAppMetadataByUid).toHaveBeenCalledWith('test-app-uid', 12345);
200
206
  expect(fetchPublicAppProductionInstallCounts).toHaveBeenCalledWith(123, 12345);
201
207
  expect(fetchAppInstallationData).toHaveBeenCalledWith(67890, 999, 'test-app-uid', ['test-scope'], []);
202
- expect(UIEDevModeInterface.setup).toHaveBeenCalled();
203
208
  });
204
209
  it('should show marketplace warning for marketplace apps', async () => {
205
210
  const marketplaceAppNode = {
@@ -212,7 +217,7 @@ describe('AppDevModeInterface', () => {
212
217
  mockLocalDevState.projectNodes = {
213
218
  [marketplaceAppNode.uid]: marketplaceAppNode,
214
219
  };
215
- await appDevModeInterface.setup({});
220
+ await appDevModeInterface.setup();
216
221
  expect(confirmPrompt).toHaveBeenCalled();
217
222
  expect(mockLocalDevState.addUploadWarning).toHaveBeenCalled();
218
223
  });
@@ -239,24 +244,19 @@ describe('AppDevModeInterface', () => {
239
244
  localDevLogger: mockLocalDevLogger,
240
245
  });
241
246
  // The setup method catches the error, so we check that process.exit was called
242
- await newAppDevModeInterface.setup({});
247
+ await newAppDevModeInterface.setup();
243
248
  expect(process.exit).toHaveBeenCalledWith(0);
244
249
  });
245
- // @TODO: Restore test account auto install functionality
246
- // it('should auto-install static auth app on test account', async () => {
247
- // (fetchAppInstallationData as Mock).mockResolvedValue({
248
- // data: {
249
- // isInstalledWithScopeGroups: false,
250
- // previouslyAuthorizedScopeGroups: [],
251
- // },
252
- // });
253
- // await appDevModeInterface.setup({});
254
- // expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(
255
- // 123,
256
- // 67890,
257
- // [1, 2, 3]
258
- // );
259
- // });
250
+ it('should auto-install static auth app on test account', async () => {
251
+ fetchAppInstallationData.mockResolvedValue({
252
+ data: {
253
+ isInstalledWithScopeGroups: false,
254
+ previouslyAuthorizedScopeGroups: [],
255
+ },
256
+ });
257
+ await appDevModeInterface.setup();
258
+ expect(installStaticAuthAppOnTestAccount).toHaveBeenCalledWith(123, 67890, [1, 2, 3]);
259
+ });
260
260
  it('should open browser for OAuth app installation', async () => {
261
261
  const oauthAppNode = {
262
262
  ...mockAppNode,
@@ -275,7 +275,7 @@ describe('AppDevModeInterface', () => {
275
275
  previouslyAuthorizedScopeGroups: [],
276
276
  },
277
277
  });
278
- await appDevModeInterface.setup({});
278
+ await appDevModeInterface.setup();
279
279
  expect(getOauthAppInstallUrl).toHaveBeenCalledWith({
280
280
  targetAccountId: 67890,
281
281
  env: ENVIRONMENTS.PROD,
@@ -294,97 +294,58 @@ describe('AppDevModeInterface', () => {
294
294
  previouslyAuthorizedScopeGroups: ['old-scope'],
295
295
  },
296
296
  });
297
- await appDevModeInterface.setup({});
298
- expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true, {
299
- appUid: 'test-app-uid',
300
- projectAccountId: 12345,
301
- projectName: 'test-project',
302
- testingAccountId: 67890,
303
- });
297
+ await appDevModeInterface.setup();
298
+ expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', true);
304
299
  });
305
300
  it('should handle errors during setup', async () => {
306
301
  const error = new Error('Setup failed');
307
302
  fetchAppMetadataByUid.mockRejectedValue(error);
308
- await appDevModeInterface.setup({});
303
+ await appDevModeInterface.setup();
309
304
  expect(logError).toHaveBeenCalledWith(error);
310
305
  });
311
- // @TODO: Restore test account auto install functionality
312
- // it('should exit if user declines auto-install', async () => {
313
- // // Set up conditions for automatic installation
314
- // (getAccountConfig as Mock).mockReturnValue({
315
- // parentAccountId: 12345, // matches targetProjectAccountId
316
- // });
317
- // (isDeveloperTestAccount as Mock).mockReturnValue(true);
318
- // (fetchAppInstallationData as Mock).mockResolvedValue({
319
- // data: {
320
- // isInstalledWithScopeGroups: false,
321
- // previouslyAuthorizedScopeGroups: [],
322
- // },
323
- // });
324
- // (installAppAutoPrompt as Mock).mockResolvedValue(false);
325
- // // Create a new instance to trigger the exit during setup
326
- // const newAppDevModeInterface = new AppDevModeInterface({
327
- // localDevState: mockLocalDevState,
328
- // localDevLogger: mockLocalDevLogger,
329
- // });
330
- // // The setup method catches the error, so we check that process.exit was called
331
- // await newAppDevModeInterface.setup({});
332
- // expect(process.exit).toHaveBeenCalledWith(0);
333
- // });
334
- // @TODO: Restore test account auto install functionality
335
- // it('should fallback to browser install if auto-install fails', async () => {
336
- // (fetchAppInstallationData as Mock).mockResolvedValue({
337
- // data: {
338
- // isInstalledWithScopeGroups: false,
339
- // previouslyAuthorizedScopeGroups: [],
340
- // },
341
- // });
342
- // (installStaticAuthAppOnTestAccount as Mock).mockRejectedValue(
343
- // new Error('Install failed')
344
- // );
345
- // await appDevModeInterface.setup({});
346
- // expect(installAppBrowserPrompt).toHaveBeenCalledWith(
347
- // 'http://static-install-url',
348
- // false
349
- // );
350
- // });
351
- });
352
- describe('start()', () => {
353
- it('should return early if no app node exists', async () => {
354
- mockLocalDevState.projectNodes = {};
355
- await appDevModeInterface.start();
356
- expect(UIEDevModeInterface.start).not.toHaveBeenCalled();
306
+ it('should exit if user declines auto-install', async () => {
307
+ // Set up conditions for automatic installation
308
+ getAccountConfig.mockReturnValue({
309
+ parentAccountId: 12345, // matches targetProjectAccountId
310
+ });
311
+ isDeveloperTestAccount.mockReturnValue(true);
312
+ fetchAppInstallationData.mockResolvedValue({
313
+ data: {
314
+ isInstalledWithScopeGroups: false,
315
+ previouslyAuthorizedScopeGroups: [],
316
+ },
317
+ });
318
+ installAppAutoPrompt.mockResolvedValue(false);
319
+ // Create a new instance to trigger the exit during setup
320
+ const newAppDevModeInterface = new AppDevModeInterface({
321
+ localDevState: mockLocalDevState,
322
+ localDevLogger: mockLocalDevLogger,
323
+ });
324
+ // The setup method catches the error, so we check that process.exit was called
325
+ await newAppDevModeInterface.setup();
326
+ expect(process.exit).toHaveBeenCalledWith(0);
357
327
  });
358
- it('should start UIE dev mode interface', async () => {
359
- await appDevModeInterface.start();
360
- expect(UIEDevModeInterface.start).toHaveBeenCalledWith({
361
- accountId: 67890,
362
- projectConfig: mockProjectConfig,
363
- requestPorts,
328
+ it('should fallback to browser install if auto-install fails', async () => {
329
+ fetchAppInstallationData.mockResolvedValue({
330
+ data: {
331
+ isInstalledWithScopeGroups: false,
332
+ previouslyAuthorizedScopeGroups: [],
333
+ },
364
334
  });
335
+ installStaticAuthAppOnTestAccount.mockRejectedValue(new Error('Install failed'));
336
+ await appDevModeInterface.setup();
337
+ expect(installAppBrowserPrompt).toHaveBeenCalledWith('http://static-install-url', false);
365
338
  });
366
339
  });
367
- describe('fileChange()', () => {
368
- it('should return early if no app node exists', async () => {
369
- mockLocalDevState.projectNodes = {};
370
- await appDevModeInterface.fileChange('test.js', 'change');
371
- expect(UIEDevModeInterface.fileChange).not.toHaveBeenCalled();
372
- });
373
- it('should forward file change to UIE dev mode interface', async () => {
374
- await appDevModeInterface.fileChange('test.js', 'change');
375
- expect(UIEDevModeInterface.fileChange).toHaveBeenCalledWith('test.js', 'change');
340
+ describe('start()', () => {
341
+ it('should add state listeners', async () => {
342
+ await appDevModeInterface.start();
343
+ expect(mockLocalDevState.addListener).toHaveBeenCalledWith('projectNodes',
344
+ // @ts-expect-error access private method for testing
345
+ appDevModeInterface.onChangeProjectNodes);
376
346
  });
377
347
  });
378
348
  describe('cleanup()', () => {
379
- it('should return early if no app node exists', async () => {
380
- mockLocalDevState.projectNodes = {};
381
- await appDevModeInterface.cleanup();
382
- expect(UIEDevModeInterface.cleanup).not.toHaveBeenCalled();
383
- });
384
- it('should cleanup UIE dev mode interface', async () => {
385
- await appDevModeInterface.cleanup();
386
- expect(UIEDevModeInterface.cleanup).toHaveBeenCalled();
387
- });
388
349
  it('should remove state listeners', async () => {
389
350
  await appDevModeInterface.cleanup();
390
351
  expect(mockLocalDevState.removeListener).toHaveBeenCalledWith('devServerMessage',
@@ -411,7 +372,14 @@ describe('AppDevModeInterface', () => {
411
372
  data: { uniquePortalInstallCount: 5 },
412
373
  });
413
374
  getStaticAuthAppInstallUrl.mockReturnValue('http://static-install-url');
414
- installAppBrowserPrompt.mockResolvedValue(undefined);
375
+ installAppBrowserPrompt.mockImplementation(async () => {
376
+ const addListenerCall = mockLocalDevState.addListener.mock.calls.find(call => call[0] === 'devServerMessage');
377
+ if (addListenerCall) {
378
+ const callback = addListenerCall[1];
379
+ callback(LOCAL_DEV_SERVER_MESSAGE_TYPES.STATIC_AUTH_APP_INSTALL_SUCCESS);
380
+ }
381
+ return undefined;
382
+ });
415
383
  // Reset the mock LocalDevState
416
384
  mockLocalDevState.getAppDataByUid = vi.fn().mockReturnValue(mockAppData);
417
385
  mockLocalDevState.setAppDataForUid = vi.fn();
@@ -430,7 +398,7 @@ describe('AppDevModeInterface', () => {
430
398
  localDevState: mockLocalDevState,
431
399
  localDevLogger: mockLocalDevLogger,
432
400
  });
433
- await newAppDevModeInterface.setup({});
401
+ await newAppDevModeInterface.setup();
434
402
  expect(installAppBrowserPrompt).toHaveBeenCalled();
435
403
  });
436
404
  it('should return false for OAuth app', async () => {
@@ -444,7 +412,16 @@ describe('AppDevModeInterface', () => {
444
412
  data: { uniquePortalInstallCount: 5 },
445
413
  });
446
414
  getOauthAppInstallUrl.mockReturnValue('http://oauth-install-url');
447
- installAppBrowserPrompt.mockResolvedValue(undefined);
415
+ installAppBrowserPrompt.mockImplementation(async () => {
416
+ setTimeout(() => {
417
+ const addListenerCall = mockLocalDevState.addListener.mock.calls.find(call => call[0] === 'devServerMessage');
418
+ if (addListenerCall) {
419
+ const callback = addListenerCall[1];
420
+ callback(LOCAL_DEV_SERVER_MESSAGE_TYPES.STATIC_AUTH_APP_INSTALL_SUCCESS);
421
+ }
422
+ }, 0);
423
+ return undefined;
424
+ });
448
425
  // Reset the mock LocalDevState
449
426
  mockLocalDevState.getAppDataByUid = vi.fn().mockReturnValue(mockAppData);
450
427
  mockLocalDevState.setAppDataForUid = vi.fn();
@@ -472,7 +449,7 @@ describe('AppDevModeInterface', () => {
472
449
  localDevState: mockLocalDevState,
473
450
  localDevLogger: mockLocalDevLogger,
474
451
  });
475
- await newAppDevModeInterface.setup({});
452
+ await newAppDevModeInterface.setup();
476
453
  expect(installAppBrowserPrompt).toHaveBeenCalled();
477
454
  });
478
455
  });
@@ -488,7 +465,16 @@ describe('AppDevModeInterface', () => {
488
465
  data: { uniquePortalInstallCount: 5 },
489
466
  });
490
467
  getStaticAuthAppInstallUrl.mockReturnValue('http://static-install-url');
491
- installAppBrowserPrompt.mockResolvedValue(undefined);
468
+ installAppBrowserPrompt.mockImplementation(async () => {
469
+ setTimeout(() => {
470
+ const addListenerCall = mockLocalDevState.addListener.mock.calls.find(call => call[0] === 'devServerMessage');
471
+ if (addListenerCall) {
472
+ const callback = addListenerCall[1];
473
+ callback(LOCAL_DEV_SERVER_MESSAGE_TYPES.STATIC_AUTH_APP_INSTALL_SUCCESS);
474
+ }
475
+ }, 0);
476
+ return undefined;
477
+ });
492
478
  // Reset the mock LocalDevState
493
479
  mockLocalDevState.getAppDataByUid = vi.fn().mockReturnValue(mockAppData);
494
480
  mockLocalDevState.setAppDataForUid = vi.fn();
@@ -505,7 +491,7 @@ describe('AppDevModeInterface', () => {
505
491
  localDevState: mockLocalDevState,
506
492
  localDevLogger: mockLocalDevLogger,
507
493
  });
508
- await newAppDevModeInterface.setup({});
494
+ await newAppDevModeInterface.setup();
509
495
  // Simulate websocket server connection
510
496
  const addListenerCall = mockLocalDevState.addListener.mock
511
497
  .calls[0];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,183 @@
1
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
2
+ import DevServerManager from '../localDev/DevServerManager.js';
3
+ import LocalDevState from '../localDev/LocalDevState.js';
4
+ import LocalDevLogger from '../localDev/LocalDevLogger.js';
5
+ import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
6
+ vi.mock('@hubspot/ui-extensions-dev-server', () => ({
7
+ DevModeUnifiedInterface: {
8
+ setup: vi.fn().mockResolvedValue(undefined),
9
+ start: vi.fn().mockResolvedValue(undefined),
10
+ fileChange: vi.fn().mockResolvedValue(undefined),
11
+ cleanup: vi.fn().mockResolvedValue(undefined),
12
+ },
13
+ }));
14
+ vi.mock('@hubspot/local-dev-lib/portManager', () => ({
15
+ requestPorts: vi.fn().mockResolvedValue({ 'test-port': 8080 }),
16
+ }));
17
+ vi.mock('@hubspot/local-dev-lib/api/localDevAuth', () => ({
18
+ fetchAppInstallationData: vi.fn().mockResolvedValue({}),
19
+ }));
20
+ vi.mock('@hubspot/local-dev-lib/api/appsDev', () => ({
21
+ fetchAppMetadataByUid: vi.fn().mockResolvedValue({}),
22
+ fetchPublicAppProductionInstallCounts: vi.fn().mockResolvedValue({}),
23
+ installStaticAuthAppOnTestAccount: vi.fn().mockResolvedValue({}),
24
+ }));
25
+ vi.mock('@hubspot/local-dev-lib/config', () => ({
26
+ getAccountConfig: vi.fn().mockReturnValue({ accountId: 123 }),
27
+ configFileExists: vi.fn().mockReturnValue(true),
28
+ getAccountId: vi.fn().mockReturnValue(123),
29
+ hasLocalStateFlag: vi.fn().mockReturnValue(false),
30
+ getConfigDefaultAccount: vi.fn().mockReturnValue({ accountId: 123 }),
31
+ }));
32
+ vi.mock('@hubspot/local-dev-lib/urls', () => ({
33
+ getHubSpotApiOrigin: vi.fn().mockReturnValue('https://api.hubspot.com'),
34
+ getHubSpotWebsiteOrigin: vi.fn().mockReturnValue('https://app.hubspot.com'),
35
+ }));
36
+ vi.mock('../../ui/SpinniesManager', () => ({
37
+ default: {
38
+ add: vi.fn(),
39
+ succeed: vi.fn(),
40
+ fail: vi.fn(),
41
+ init: vi.fn(),
42
+ },
43
+ }));
44
+ describe('DevServerManager', () => {
45
+ let devServerManager;
46
+ let localDevState;
47
+ let logger;
48
+ beforeEach(() => {
49
+ localDevState = new LocalDevState({
50
+ targetProjectAccountId: 123,
51
+ targetTestingAccountId: 456,
52
+ projectConfig: {
53
+ name: 'test-project',
54
+ srcDir: 'src',
55
+ platformVersion: '1.0.0',
56
+ },
57
+ projectDir: '/test/project',
58
+ projectData: {
59
+ name: 'test-project',
60
+ id: 123,
61
+ createdAt: Date.now(),
62
+ deletedAt: 0,
63
+ isLocked: false,
64
+ portalId: 123,
65
+ updatedAt: Date.now(),
66
+ },
67
+ debug: false,
68
+ initialProjectNodes: {},
69
+ initialProjectProfileData: {},
70
+ profile: 'test',
71
+ env: ENVIRONMENTS.QA,
72
+ });
73
+ logger = new LocalDevLogger(localDevState);
74
+ devServerManager = new DevServerManager({
75
+ localDevState,
76
+ logger,
77
+ });
78
+ });
79
+ describe('constructor', () => {
80
+ it('should initialize with correct state', async () => {
81
+ await expect(async () => {
82
+ await devServerManager.start();
83
+ }).rejects.toThrow('The Dev Server Manager must be initialized before it is started.');
84
+ });
85
+ });
86
+ describe('setup', () => {
87
+ it('should complete setup without errors', async () => {
88
+ await expect(devServerManager.setup()).resolves.not.toThrow();
89
+ });
90
+ it('should allow start after successful setup', async () => {
91
+ await devServerManager.setup();
92
+ await expect(devServerManager.start()).resolves.not.toThrow();
93
+ });
94
+ it('should call setup on dev servers sequentially', async () => {
95
+ const executionOrder = [];
96
+ const { DevModeUnifiedInterface } = await import('@hubspot/ui-extensions-dev-server');
97
+ const originalSetup = DevModeUnifiedInterface.setup;
98
+ DevModeUnifiedInterface.setup = vi.fn().mockImplementation(async () => {
99
+ executionOrder.push('UIExtensions-start');
100
+ await new Promise(resolve => setTimeout(resolve, 20));
101
+ executionOrder.push('UIExtensions-end');
102
+ });
103
+ const AppDevModeInterface = (await import('../localDev/AppDevModeInterface.js')).default;
104
+ const originalAppSetup = AppDevModeInterface.prototype.setup;
105
+ AppDevModeInterface.prototype.setup = vi
106
+ .fn()
107
+ .mockImplementation(async function () {
108
+ executionOrder.push('App-start');
109
+ await new Promise(resolve => setTimeout(resolve, 20));
110
+ executionOrder.push('App-end');
111
+ });
112
+ const testManager = new DevServerManager({
113
+ localDevState,
114
+ logger,
115
+ });
116
+ await testManager.setup();
117
+ expect(executionOrder).toEqual([
118
+ 'App-start',
119
+ 'App-end',
120
+ 'UIExtensions-start',
121
+ 'UIExtensions-end',
122
+ ]);
123
+ expect(AppDevModeInterface.prototype.setup).toHaveBeenCalledTimes(1);
124
+ expect(DevModeUnifiedInterface.setup).toHaveBeenCalledTimes(1);
125
+ // Restore original methods
126
+ DevModeUnifiedInterface.setup = originalSetup;
127
+ AppDevModeInterface.prototype.setup = originalAppSetup;
128
+ });
129
+ });
130
+ describe('start', () => {
131
+ it('should throw error when not initialized', async () => {
132
+ await expect(devServerManager.start()).rejects.toThrow();
133
+ });
134
+ it('should start successfully after setup', async () => {
135
+ await devServerManager.setup();
136
+ await expect(devServerManager.start()).resolves.not.toThrow();
137
+ });
138
+ it('should set started state correctly', async () => {
139
+ await devServerManager.setup();
140
+ await devServerManager.start();
141
+ await expect(devServerManager.fileChange({ filePath: 'test.js', event: 'change' })).resolves.not.toThrow();
142
+ });
143
+ });
144
+ describe('fileChange', () => {
145
+ it('should handle fileChange', async () => {
146
+ await devServerManager.setup();
147
+ await devServerManager.start();
148
+ await expect(devServerManager.fileChange({
149
+ filePath: 'src/test.js',
150
+ event: 'change',
151
+ })).resolves.not.toThrow();
152
+ });
153
+ it('should handle different file events', async () => {
154
+ await devServerManager.setup();
155
+ await devServerManager.start();
156
+ const testCases = [
157
+ { filePath: 'src/component.js', event: 'add' },
158
+ { filePath: 'src/style.css', event: 'change' },
159
+ { filePath: 'src/config.json', event: 'unlink' },
160
+ ];
161
+ for (const testCase of testCases) {
162
+ await expect(devServerManager.fileChange(testCase)).resolves.not.toThrow();
163
+ }
164
+ });
165
+ it('should handle concurrent fileChange calls', async () => {
166
+ await devServerManager.setup();
167
+ await devServerManager.start();
168
+ const fileChanges = [
169
+ devServerManager.fileChange({ filePath: 'file1.js', event: 'change' }),
170
+ devServerManager.fileChange({ filePath: 'file2.js', event: 'add' }),
171
+ devServerManager.fileChange({ filePath: 'file3.js', event: 'unlink' }),
172
+ ];
173
+ await expect(Promise.all(fileChanges)).resolves.not.toThrow();
174
+ });
175
+ });
176
+ describe('cleanup', () => {
177
+ it('should handle cleanup', async () => {
178
+ await devServerManager.setup();
179
+ await devServerManager.start();
180
+ await expect(devServerManager.cleanup()).resolves.not.toThrow();
181
+ });
182
+ });
183
+ });
@@ -7,7 +7,7 @@ import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
7
7
  import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
8
8
  import LocalDevProcess from '../localDev/LocalDevProcess.js';
9
9
  import LocalDevLogger from '../localDev/LocalDevLogger.js';
10
- import DevServerManagerV2 from '../localDev/DevServerManagerV2.js';
10
+ import DevServerManager from '../localDev/DevServerManager.js';
11
11
  import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments';
12
12
  import { vi } from 'vitest';
13
13
  // Mock @hubspot/ui-extensions-dev-server
@@ -27,7 +27,7 @@ vi.mock('../config');
27
27
  vi.mock('@hubspot/local-dev-lib/api/projects');
28
28
  vi.mock('@hubspot/local-dev-lib/errors/index');
29
29
  vi.mock('../localDev/LocalDevLogger');
30
- vi.mock('../localDev/DevServerManagerV2');
30
+ vi.mock('../localDev/DevServerManager');
31
31
  // Tests for LocalDevProcess and LocalDevState
32
32
  describe('LocalDevProcess', () => {
33
33
  let mockLocalDevLogger;
@@ -67,6 +67,7 @@ describe('LocalDevProcess', () => {
67
67
  subbuildStatuses: [],
68
68
  uploadMessage: 'Build completed',
69
69
  autoDeployId: 0,
70
+ platformVersion: '2025.2',
70
71
  },
71
72
  },
72
73
  initialProjectNodes: {},
@@ -104,7 +105,7 @@ describe('LocalDevProcess', () => {
104
105
  };
105
106
  // Mock constructors
106
107
  LocalDevLogger.mockImplementation(() => mockLocalDevLogger);
107
- DevServerManagerV2.mockImplementation(() => mockDevServerManager);
108
+ DevServerManager.mockImplementation(() => mockDevServerManager);
108
109
  // Mock external functions
109
110
  isHubSpotHttpError.mockReturnValue(false);
110
111
  // Create process instance
@@ -398,7 +399,7 @@ describe('LocalDevProcess', () => {
398
399
  expect(handleProjectDeploy).toHaveBeenCalledWith(123, // targetProjectAccountId
399
400
  'test-project', // projectName
400
401
  123, // buildId
401
- true, // useV3Api
402
+ true, // useV2Api
402
403
  false // force
403
404
  );
404
405
  expect(mockLocalDevLogger.deploySuccess).toHaveBeenCalled();
@@ -426,7 +427,7 @@ describe('LocalDevProcess', () => {
426
427
  expect(handleProjectDeploy).toHaveBeenCalledWith(123, // targetProjectAccountId
427
428
  'test-project', // projectName
428
429
  123, // buildId
429
- true, // useV3Api
430
+ true, // useV2Api
430
431
  true // force
431
432
  );
432
433
  expect(result).toEqual({
@@ -252,7 +252,7 @@ describe('LocalDevWebsocketServer', () => {
252
252
  expect(mockWebSocket2.on).toHaveBeenCalledWith('message', expect.any(Function));
253
253
  expect(mockWebSocket3.on).toHaveBeenCalledWith('message', expect.any(Function));
254
254
  // Each connection should trigger state listener setup
255
- expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(9); // 3 listeners per connection * 3 connections
255
+ expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(12); // 4 listeners per connection * 3 connections
256
256
  // Each connection should trigger dev server message
257
257
  expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledTimes(3);
258
258
  expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
@@ -308,16 +308,16 @@ describe('LocalDevWebsocketServer', () => {
308
308
  const closeCallbacks2 = mockWebSocket2.on.mock.calls
309
309
  .filter(call => call[0] === 'close')
310
310
  .map(call => call[1]);
311
- expect(closeCallbacks1).toHaveLength(3); // projectNodes, appData, and uploadWarnings listeners
312
- expect(closeCallbacks2).toHaveLength(3); // projectNodes, appData, and uploadWarnings listeners
311
+ expect(closeCallbacks1).toHaveLength(4); // projectNodes, appData, devServersStarted, and uploadWarnings listeners
312
+ expect(closeCallbacks2).toHaveLength(4); // projectNodes, appData, devServersStarted, and uploadWarnings listeners
313
313
  // Simulate first connection closing (call all close callbacks)
314
314
  closeCallbacks1.forEach(callback => callback());
315
- // Should have removed listeners for first connection (3 listeners: projectNodes, appData, and uploadWarnings)
316
- expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(3);
315
+ // Should have removed listeners for first connection (4 listeners: projectNodes, appData, devServersStarted, and uploadWarnings)
316
+ expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(4);
317
317
  // Simulate second connection closing
318
318
  closeCallbacks2.forEach(callback => callback());
319
319
  // Should have removed listeners for second connection as well
320
- expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(6);
320
+ expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(8);
321
321
  });
322
322
  it('should broadcast state changes to all connected clients', () => {
323
323
  // Establish connections