@elevasis/core 0.7.1 → 0.8.2

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 (476) hide show
  1. package/dist/test-utils/index.d.ts +3122 -0
  2. package/dist/test-utils/index.js +386 -0
  3. package/package.json +6 -1
  4. package/src/README.md +39 -36
  5. package/src/__tests__/publish.test.ts +18 -13
  6. package/src/__tests__/{template-foundations-compatibility.test.ts → template-core-compatibility.test.ts} +99 -99
  7. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1135 -1131
  8. package/src/_gen/__tests__/scaffold-contracts.test.ts +47 -36
  9. package/src/_gen/scaffold-contracts.ts +45 -45
  10. package/src/auth/multi-tenancy/credentials/README.md +38 -38
  11. package/src/auth/multi-tenancy/credentials/index.ts +6 -6
  12. package/src/auth/multi-tenancy/credentials/server/encryption.ts +39 -39
  13. package/src/auth/multi-tenancy/credentials/server/service.ts +60 -60
  14. package/src/auth/multi-tenancy/index.ts +17 -17
  15. package/src/auth/multi-tenancy/invitations/api-schemas.ts +107 -107
  16. package/src/auth/multi-tenancy/invitations/index.ts +37 -37
  17. package/src/auth/multi-tenancy/invitations/invitation.ts +86 -86
  18. package/src/auth/multi-tenancy/invitations/server/index.ts +25 -25
  19. package/src/auth/multi-tenancy/invitations/server/transforms.ts +24 -24
  20. package/src/auth/multi-tenancy/invitations/server/workos.ts +24 -24
  21. package/src/auth/multi-tenancy/invitations/supabase.ts +50 -50
  22. package/src/auth/multi-tenancy/memberships/api-schemas.ts +126 -126
  23. package/src/auth/multi-tenancy/memberships/index.ts +21 -21
  24. package/src/auth/multi-tenancy/memberships/membership.ts +138 -138
  25. package/src/auth/multi-tenancy/memberships/server/index.ts +15 -15
  26. package/src/auth/multi-tenancy/memberships/server/transforms.ts +32 -32
  27. package/src/auth/multi-tenancy/memberships/server/workos.ts +21 -21
  28. package/src/auth/multi-tenancy/memberships/supabase.ts +46 -46
  29. package/src/auth/multi-tenancy/organizations/api-schemas.ts +128 -128
  30. package/src/auth/multi-tenancy/organizations/index.ts +23 -23
  31. package/src/auth/multi-tenancy/organizations/organization.ts +24 -24
  32. package/src/auth/multi-tenancy/organizations/server/index.ts +10 -10
  33. package/src/auth/multi-tenancy/organizations/server/transforms.ts +35 -35
  34. package/src/auth/multi-tenancy/organizations/server/workos.ts +20 -20
  35. package/src/auth/multi-tenancy/types.ts +83 -83
  36. package/src/auth/multi-tenancy/users/api-schemas.ts +194 -194
  37. package/src/auth/multi-tenancy/users/index.ts +27 -27
  38. package/src/auth/multi-tenancy/users/server/index.ts +19 -19
  39. package/src/auth/multi-tenancy/users/server/transforms.ts +21 -21
  40. package/src/auth/multi-tenancy/users/server/workos.ts +16 -16
  41. package/src/auth/multi-tenancy/users/user.ts +65 -65
  42. package/src/business/README.md +52 -52
  43. package/src/business/__tests__/entities-published.test.ts +33 -33
  44. package/src/business/acquisition/api-schemas.ts +759 -759
  45. package/src/business/acquisition/index.ts +109 -109
  46. package/src/business/acquisition/types.ts +402 -402
  47. package/src/business/base-entities.test.ts +481 -481
  48. package/src/business/base-entities.ts +241 -241
  49. package/src/business/entities-published.ts +24 -24
  50. package/src/business/index.ts +15 -15
  51. package/src/business/pdf/browser/pdfmake-browser.ts +229 -229
  52. package/src/business/pdf/index.ts +10 -10
  53. package/src/business/pdf/server/index.ts +21 -21
  54. package/src/business/pdf/server/themes/default.ts +8 -8
  55. package/src/business/pdf/server/themes/index.ts +9 -9
  56. package/src/business/pdf/server/themes/types.ts +8 -8
  57. package/src/business/pdf/types.ts +272 -272
  58. package/src/business/projects/index.ts +2 -2
  59. package/src/business/projects/sse-events.ts +21 -21
  60. package/src/business/projects/types.ts +89 -89
  61. package/src/business/sales/api-schemas.ts +75 -75
  62. package/src/business/seo/__tests__/linking.test.ts +549 -549
  63. package/src/business/seo/__tests__/types.test.ts +404 -404
  64. package/src/business/seo/index.ts +2 -2
  65. package/src/business/seo/linking.ts +281 -281
  66. package/src/business/seo/types.ts +199 -199
  67. package/src/commands/queue/index.ts +3 -3
  68. package/src/commands/queue/schemas.test.ts +593 -593
  69. package/src/commands/queue/schemas.ts +125 -125
  70. package/src/commands/queue/sse-events.ts +61 -61
  71. package/src/commands/queue/types/action.ts +52 -52
  72. package/src/commands/queue/types/checkpoint.ts +44 -44
  73. package/src/commands/queue/types/index.ts +7 -7
  74. package/src/commands/queue/types/task.ts +116 -116
  75. package/src/commands/queue/types.ts +14 -14
  76. package/src/content/distribution-metadata.ts +61 -61
  77. package/src/content/index.ts +10 -10
  78. package/src/deployments/index.ts +22 -22
  79. package/src/execution/core/__tests__/archived-logs.test.ts +72 -72
  80. package/src/execution/core/index.ts +11 -11
  81. package/src/execution/core/runner-types.ts +80 -80
  82. package/src/execution/core/server/environment.ts +31 -31
  83. package/src/execution/core/sse-executions.ts +119 -119
  84. package/src/execution/core/types.ts +29 -29
  85. package/src/execution/engine/__tests__/fixtures/test-agents.ts +4 -4
  86. package/src/execution/engine/__tests__/timeout.test.ts +565 -565
  87. package/src/execution/engine/agent/__tests__/errors.test.ts +508 -508
  88. package/src/execution/engine/agent/actions/__tests__/processor.test.ts +531 -531
  89. package/src/execution/engine/agent/actions/executor.ts +205 -205
  90. package/src/execution/engine/agent/actions/navigate-knowledge-executor.ts +230 -230
  91. package/src/execution/engine/agent/actions/processor.ts +116 -116
  92. package/src/execution/engine/agent/actions/types.ts +70 -70
  93. package/src/execution/engine/agent/core/agent.ts +810 -810
  94. package/src/execution/engine/agent/core/types.ts +155 -155
  95. package/src/execution/engine/agent/errors.ts +251 -251
  96. package/src/execution/engine/agent/index.ts +78 -78
  97. package/src/execution/engine/agent/knowledge-map/types.ts +106 -106
  98. package/src/execution/engine/agent/knowledge-map/utils.ts +101 -101
  99. package/src/execution/engine/agent/memory/__tests__/manager.test.ts +754 -754
  100. package/src/execution/engine/agent/memory/domains.ts +99 -99
  101. package/src/execution/engine/agent/memory/manager.ts +365 -365
  102. package/src/execution/engine/agent/memory/processor.ts +66 -66
  103. package/src/execution/engine/agent/memory/types.ts +90 -90
  104. package/src/execution/engine/agent/memory/utils.ts +134 -134
  105. package/src/execution/engine/agent/observability/logging.ts +467 -467
  106. package/src/execution/engine/agent/observability/types.ts +64 -64
  107. package/src/execution/engine/agent/reasoning/adapters/agent-adapter-helpers.ts +349 -349
  108. package/src/execution/engine/agent/reasoning/processor.ts +92 -92
  109. package/src/execution/engine/agent/reasoning/prompt-sections/base-actions.ts +134 -134
  110. package/src/execution/engine/agent/reasoning/prompt-sections/completion.ts +49 -49
  111. package/src/execution/engine/agent/reasoning/prompt-sections/knowledge-map.ts +93 -93
  112. package/src/execution/engine/agent/reasoning/prompt-sections/memory.ts +65 -65
  113. package/src/execution/engine/agent/reasoning/prompt-sections/tools.ts +44 -44
  114. package/src/execution/engine/agent/reasoning/request-builder.ts +169 -169
  115. package/src/execution/engine/agent/reasoning/types.ts +18 -18
  116. package/src/execution/engine/base/errors.ts +118 -118
  117. package/src/execution/engine/base/index.ts +2 -2
  118. package/src/execution/engine/base/logging.ts +31 -31
  119. package/src/execution/engine/base/serialization.ts +324 -324
  120. package/src/execution/engine/base/types.ts +126 -126
  121. package/src/execution/engine/base/utils.ts +41 -41
  122. package/src/execution/engine/index.ts +434 -434
  123. package/src/execution/engine/interface/index.ts +1 -1
  124. package/src/execution/engine/interface/types.ts +62 -62
  125. package/src/execution/engine/llm/__tests__/model-info.test.ts +50 -50
  126. package/src/execution/engine/llm/__tests__/model-validation.test.ts +321 -321
  127. package/src/execution/engine/llm/__tests__/response-schema-validator.test.ts +115 -115
  128. package/src/execution/engine/llm/adapters/__tests__/adapter-factory.test.ts +375 -375
  129. package/src/execution/engine/llm/adapters/__tests__/anthropic-adapter.test.ts +463 -463
  130. package/src/execution/engine/llm/adapters/__tests__/anthropic.integration.test.ts +177 -177
  131. package/src/execution/engine/llm/adapters/__tests__/google-adapter.test.ts +722 -722
  132. package/src/execution/engine/llm/adapters/__tests__/google.integration.test.ts +376 -376
  133. package/src/execution/engine/llm/adapters/__tests__/openai-adapter.test.ts +551 -551
  134. package/src/execution/engine/llm/adapters/__tests__/openrouter-adapter.test.ts +563 -563
  135. package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +105 -105
  136. package/src/execution/engine/llm/adapters/__tests__/universal-adapter.test.ts +537 -537
  137. package/src/execution/engine/llm/adapters/circuit-breaker.ts +147 -147
  138. package/src/execution/engine/llm/adapters/index.ts +17 -17
  139. package/src/execution/engine/llm/adapters/mock-adapter.ts +116 -116
  140. package/src/execution/engine/llm/adapters/server/adapter-factory.ts +130 -130
  141. package/src/execution/engine/llm/adapters/server/anthropic.ts +137 -137
  142. package/src/execution/engine/llm/adapters/server/google.ts +283 -283
  143. package/src/execution/engine/llm/adapters/server/index.ts +12 -12
  144. package/src/execution/engine/llm/adapters/server/openai.ts +206 -206
  145. package/src/execution/engine/llm/adapters/server/openrouter.ts +235 -235
  146. package/src/execution/engine/llm/adapters/universal-adapter.ts +230 -230
  147. package/src/execution/engine/llm/errors.ts +186 -186
  148. package/src/execution/engine/llm/model-info.ts +332 -332
  149. package/src/execution/engine/llm/response-schema-validator.ts +113 -113
  150. package/src/execution/engine/llm/types.ts +86 -86
  151. package/src/execution/engine/test-utils/index.ts +6 -6
  152. package/src/execution/engine/test-utils/mocks.ts +56 -56
  153. package/src/execution/engine/tools/integration/base-integration-adapter.ts +50 -50
  154. package/src/execution/engine/tools/integration/index.ts +53 -53
  155. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-adapter.ts +73 -73
  156. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-tools.ts +209 -209
  157. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-company-email/index.ts +82 -82
  158. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-decision-maker-email/index.ts +122 -122
  159. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-person-email/index.ts +89 -89
  160. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/verify-email/index.ts +84 -84
  161. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/index.ts +16 -16
  162. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +293 -293
  163. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +100 -100
  164. package/src/execution/engine/tools/integration/server/adapters/apify/apify-tools.ts +217 -217
  165. package/src/execution/engine/tools/integration/server/adapters/apify/fetch/get-dataset-items/index.ts +92 -92
  166. package/src/execution/engine/tools/integration/server/adapters/apify/fetch/run-actor/index.ts +218 -218
  167. package/src/execution/engine/tools/integration/server/adapters/apify/fetch/start-actor/index.ts +87 -87
  168. package/src/execution/engine/tools/integration/server/adapters/apify/index.ts +11 -11
  169. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +361 -361
  170. package/src/execution/engine/tools/integration/server/adapters/attio/attio-adapter.ts +162 -162
  171. package/src/execution/engine/tools/integration/server/adapters/attio/attio-tools.ts +594 -594
  172. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-attribute/index.ts +214 -214
  173. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-note/index.ts +152 -152
  174. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-record/index.ts +141 -141
  175. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-note/index.ts +86 -86
  176. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-record/index.ts +105 -105
  177. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.ts +118 -118
  178. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-attributes/index.ts +165 -165
  179. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-notes/index.ts +96 -96
  180. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-objects/index.ts +104 -104
  181. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.ts +156 -156
  182. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-attribute/index.ts +220 -220
  183. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-record/index.ts +140 -140
  184. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/utils/types.ts +146 -146
  185. package/src/execution/engine/tools/integration/server/adapters/attio/index.ts +31 -31
  186. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +210 -210
  187. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +104 -104
  188. package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
  189. package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-adapter.ts +1189 -1189
  190. package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-tools.ts +641 -641
  191. package/src/execution/engine/tools/integration/server/adapters/google-sheets/index.ts +18 -18
  192. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/activate-campaign/index.ts +86 -86
  193. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/__tests__/index.test.ts +289 -289
  194. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/index.ts +154 -154
  195. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/__tests__/index.test.ts +325 -325
  196. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/index.ts +153 -153
  197. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-delete-leads/index.ts +84 -84
  198. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-campaign/index.ts +125 -125
  199. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-inbox-test/index.ts +107 -107
  200. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/delete-campaign/index.ts +85 -85
  201. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-account-health/index.ts +91 -91
  202. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign/index.ts +92 -92
  203. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/__tests__/index.test.ts +195 -195
  204. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/index.ts +113 -113
  205. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-daily-campaign-analytics/index.ts +104 -104
  206. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-emails/index.ts +155 -155
  207. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/__tests__/index.test.ts +196 -196
  208. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/index.ts +102 -102
  209. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/__tests__/index.test.ts +189 -189
  210. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/index.ts +87 -87
  211. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-leads/index.ts +112 -112
  212. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/patch-lead/index.ts +76 -76
  213. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/pause-campaign/index.ts +86 -86
  214. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/remove-from-subsequence/index.ts +98 -98
  215. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/send-reply/index.ts +126 -126
  216. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/__tests__/index.test.ts +193 -193
  217. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/index.ts +99 -99
  218. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/__tests__/index.test.ts +621 -621
  219. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/index.ts +125 -125
  220. package/src/execution/engine/tools/integration/server/adapters/instantly/index.ts +29 -29
  221. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-adapter.ts +178 -178
  222. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1473 -1473
  223. package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/check-credits/index.ts +59 -59
  224. package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/verify-email/index.ts +102 -102
  225. package/src/execution/engine/tools/integration/server/adapters/millionverifier/index.ts +17 -17
  226. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-adapter.ts +80 -80
  227. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +102 -102
  228. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/get-email/index.ts +102 -102
  229. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +134 -134
  230. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +75 -75
  231. package/src/execution/engine/tools/integration/server/adapters/resend/index.ts +27 -27
  232. package/src/execution/engine/tools/integration/server/adapters/resend/resend-adapter.ts +108 -108
  233. package/src/execution/engine/tools/integration/server/adapters/resend/resend-tools.ts +132 -132
  234. package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/create-envelope/index.ts +274 -274
  235. package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/download-document/index.ts +230 -230
  236. package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/get-envelope/index.ts +133 -133
  237. package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/void-envelope/index.ts +90 -90
  238. package/src/execution/engine/tools/integration/server/adapters/stripe/fetch/utils/types.ts +210 -210
  239. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-adapter.ts +517 -517
  240. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +309 -309
  241. package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/domain-search/index.ts +133 -133
  242. package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-finder/index.ts +122 -122
  243. package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-verifier/index.ts +111 -111
  244. package/src/execution/engine/tools/integration/server/adapters/tomba/index.ts +11 -11
  245. package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-adapter.ts +78 -78
  246. package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-tools.ts +222 -222
  247. package/src/execution/engine/tools/integration/server/index.ts +61 -61
  248. package/src/execution/engine/tools/integration/service.ts +161 -161
  249. package/src/execution/engine/tools/integration/tool.ts +253 -253
  250. package/src/execution/engine/tools/integration/types/anymailfinder.ts +74 -74
  251. package/src/execution/engine/tools/integration/types/apify.ts +92 -92
  252. package/src/execution/engine/tools/integration/types/index.ts +19 -19
  253. package/src/execution/engine/tools/integration/types/instantly.ts +557 -557
  254. package/src/execution/engine/tools/integration/types/millionverifier.ts +56 -56
  255. package/src/execution/engine/tools/integration/types/stripe.ts +162 -162
  256. package/src/execution/engine/tools/integration/types/tomba.ts +94 -94
  257. package/src/execution/engine/tools/lead-service-types.ts +884 -884
  258. package/src/execution/engine/tools/llm/index.ts +11 -11
  259. package/src/execution/engine/tools/llm/server/index.ts +8 -8
  260. package/src/execution/engine/tools/llm/server/llm-call-tool.ts +118 -118
  261. package/src/execution/engine/tools/platform/__tests__/pdf.test.ts +441 -441
  262. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +248 -248
  263. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +319 -319
  264. package/src/execution/engine/tools/platform/acquisition/index.ts +43 -43
  265. package/src/execution/engine/tools/platform/acquisition/list-tools.ts +148 -148
  266. package/src/execution/engine/tools/platform/acquisition/types.ts +260 -260
  267. package/src/execution/engine/tools/platform/email/index.ts +122 -122
  268. package/src/execution/engine/tools/platform/email/types.ts +96 -96
  269. package/src/execution/engine/tools/platform/index.ts +157 -157
  270. package/src/execution/engine/tools/platform/notification.ts +81 -81
  271. package/src/execution/engine/tools/platform/pdf/index.ts +110 -110
  272. package/src/execution/engine/tools/platform/pdf/types.ts +77 -77
  273. package/src/execution/engine/tools/platform/scheduler.ts +87 -87
  274. package/src/execution/engine/tools/platform/storage/index.ts +370 -370
  275. package/src/execution/engine/tools/platform/types.ts +148 -148
  276. package/src/execution/engine/tools/registry.ts +700 -700
  277. package/src/execution/engine/tools/tool-maps.ts +786 -786
  278. package/src/execution/engine/tools/types.ts +233 -233
  279. package/src/execution/engine/workflow/__tests__/errors.test.ts +139 -139
  280. package/src/execution/engine/workflow/errors.ts +63 -63
  281. package/src/execution/engine/workflow/helpers/index.ts +11 -11
  282. package/src/execution/engine/workflow/helpers/server/index.ts +8 -8
  283. package/src/execution/engine/workflow/helpers/server/llm-call.ts +93 -93
  284. package/src/execution/engine/workflow/index.ts +19 -19
  285. package/src/execution/engine/workflow/log-truncate.ts +26 -26
  286. package/src/execution/engine/workflow/logging.ts +191 -191
  287. package/src/execution/engine/workflow/types.ts +182 -182
  288. package/src/execution/engine/workflow/utils.ts +280 -280
  289. package/src/execution/engine/workflow/workflow.ts +168 -168
  290. package/src/execution/index.ts +3 -3
  291. package/src/execution/scheduler/__tests__/api-schemas.test.ts +733 -733
  292. package/src/execution/scheduler/__tests__/utils.test.ts +1009 -1009
  293. package/src/execution/scheduler/api-schemas.ts +296 -296
  294. package/src/execution/scheduler/index.ts +50 -50
  295. package/src/execution/scheduler/schemas.ts +264 -264
  296. package/src/execution/scheduler/types.ts +111 -111
  297. package/src/execution/scheduler/utils.ts +364 -364
  298. package/src/forms/index.ts +7 -7
  299. package/src/forms/schemas.ts +69 -69
  300. package/src/forms/types.ts +70 -70
  301. package/src/index.ts +71 -60
  302. package/src/integrations/credentials/__tests__/schemas.test.ts +82 -82
  303. package/src/integrations/credentials/__tests__/utils.test.ts +144 -144
  304. package/src/integrations/credentials/api-schemas.ts +143 -143
  305. package/src/integrations/credentials/index.ts +32 -32
  306. package/src/integrations/credentials/schemas.ts +164 -164
  307. package/src/integrations/credentials/utils.ts +59 -59
  308. package/src/integrations/oauth/__tests__/provider-registry.test.ts +59 -59
  309. package/src/integrations/oauth/api-schemas.ts +92 -92
  310. package/src/integrations/oauth/index.ts +19 -19
  311. package/src/integrations/oauth/provider-registry.ts +61 -61
  312. package/src/integrations/oauth/server/__tests__/refresh-concurrent.test.ts +183 -183
  313. package/src/integrations/oauth/server/__tests__/refresh.test.ts +577 -577
  314. package/src/integrations/oauth/server/credentials.ts +39 -39
  315. package/src/integrations/oauth/server/refresh.ts +214 -214
  316. package/src/integrations/oauth/types.ts +34 -34
  317. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +318 -318
  318. package/src/integrations/webhook-endpoints/api-schemas.ts +102 -102
  319. package/src/integrations/webhook-endpoints/index.ts +28 -28
  320. package/src/integrations/webhook-endpoints/types.ts +51 -51
  321. package/src/operations/activities/api-schemas.ts +79 -79
  322. package/src/operations/activities/index.ts +9 -9
  323. package/src/operations/activities/sse-events.ts +30 -30
  324. package/src/operations/activities/types.ts +63 -63
  325. package/src/operations/debug-logs/client.ts +60 -60
  326. package/src/operations/debug-logs/debug-logger.ts +83 -83
  327. package/src/operations/debug-logs/index.ts +8 -8
  328. package/src/operations/debug-logs/server.ts +19 -19
  329. package/src/operations/debug-logs/types.ts +33 -33
  330. package/src/operations/index.ts +50 -50
  331. package/src/operations/notifications/api-schemas.ts +91 -91
  332. package/src/operations/notifications/index.ts +3 -3
  333. package/src/operations/notifications/sse-events.ts +21 -21
  334. package/src/operations/notifications/types.ts +47 -47
  335. package/src/operations/observability/__tests__/openrouter-cost-flow.test.ts +297 -297
  336. package/src/operations/observability/__tests__/utils.test.ts +54 -54
  337. package/src/operations/observability/ai-usage-collector.ts +64 -64
  338. package/src/operations/observability/index.ts +13 -13
  339. package/src/operations/observability/metrics-collector.ts +49 -49
  340. package/src/operations/observability/schemas.ts +39 -39
  341. package/src/operations/observability/types.ts +463 -463
  342. package/src/operations/observability/utils.ts +77 -77
  343. package/src/operations/sessions/__tests__/manager.test.ts +821 -821
  344. package/src/operations/sessions/index.ts +26 -26
  345. package/src/operations/sessions/server/manager.ts +90 -90
  346. package/src/operations/sessions/server/session.ts +180 -180
  347. package/src/operations/sessions/types.ts +98 -98
  348. package/src/operations/triggers/index.ts +12 -12
  349. package/src/operations/triggers/webhook/definitions/instantly-account-error.ts +44 -44
  350. package/src/operations/triggers/webhook/definitions/instantly-auto-reply-received.ts +51 -51
  351. package/src/operations/triggers/webhook/definitions/instantly-campaign-completed.ts +45 -45
  352. package/src/operations/triggers/webhook/definitions/instantly-email-bounced.ts +49 -49
  353. package/src/operations/triggers/webhook/definitions/instantly-lead-unsubscribed.ts +45 -45
  354. package/src/operations/triggers/webhook/definitions/instantly-reply-received.ts +54 -54
  355. package/src/operations/triggers/webhook/index.ts +35 -35
  356. package/src/operations/triggers/webhook/types.ts +74 -74
  357. package/src/organization-model/README.md +97 -97
  358. package/src/organization-model/__tests__/defaults.test.ts +175 -175
  359. package/src/organization-model/__tests__/domains/customers.test.ts +295 -295
  360. package/src/organization-model/__tests__/domains/goals.test.ts +479 -479
  361. package/src/organization-model/__tests__/domains/identity.test.ts +279 -279
  362. package/src/organization-model/__tests__/domains/navigation.test.ts +212 -212
  363. package/src/organization-model/__tests__/domains/offerings.test.ts +419 -419
  364. package/src/organization-model/__tests__/domains/operations.test.ts +203 -203
  365. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -362
  366. package/src/organization-model/__tests__/domains/roles.test.ts +347 -347
  367. package/src/organization-model/__tests__/domains/statuses.test.ts +243 -243
  368. package/src/organization-model/__tests__/foundation.test.ts +105 -105
  369. package/src/organization-model/__tests__/graph.test.ts +894 -894
  370. package/src/organization-model/__tests__/resolve.test.ts +690 -690
  371. package/src/organization-model/__tests__/schema.test.ts +407 -407
  372. package/src/organization-model/contracts.ts +14 -14
  373. package/src/organization-model/defaults.ts +148 -148
  374. package/src/organization-model/domains/branding.ts +22 -22
  375. package/src/organization-model/domains/customers.ts +75 -75
  376. package/src/organization-model/domains/features.ts +22 -22
  377. package/src/organization-model/domains/goals.ts +80 -80
  378. package/src/organization-model/domains/identity.ts +94 -94
  379. package/src/organization-model/domains/navigation.ts +391 -391
  380. package/src/organization-model/domains/offerings.ts +66 -66
  381. package/src/organization-model/domains/operations.ts +85 -85
  382. package/src/organization-model/domains/projects.ts +48 -48
  383. package/src/organization-model/domains/prospecting.ts +33 -33
  384. package/src/organization-model/domains/roles.ts +55 -55
  385. package/src/organization-model/domains/sales.ts +94 -94
  386. package/src/organization-model/domains/shared.ts +62 -62
  387. package/src/organization-model/domains/statuses.ts +130 -130
  388. package/src/organization-model/foundation.ts +97 -97
  389. package/src/organization-model/graph/build.ts +399 -399
  390. package/src/organization-model/graph/index.ts +4 -4
  391. package/src/organization-model/graph/schema.ts +48 -48
  392. package/src/organization-model/graph/types.ts +40 -40
  393. package/src/organization-model/index.ts +13 -13
  394. package/src/organization-model/organization-graph.mdx +272 -272
  395. package/src/organization-model/organization-model.mdx +320 -320
  396. package/src/organization-model/published.ts +85 -85
  397. package/src/organization-model/resolve.ts +66 -66
  398. package/src/organization-model/schema.ts +287 -287
  399. package/src/organization-model/types.ts +46 -46
  400. package/src/platform/api/index.ts +1 -1
  401. package/src/platform/api/types.ts +35 -35
  402. package/src/platform/constants/http.ts +37 -37
  403. package/src/platform/constants/index.ts +5 -5
  404. package/src/platform/constants/limits.ts +32 -32
  405. package/src/platform/constants/resilience.ts +51 -51
  406. package/src/platform/constants/timeouts.ts +20 -20
  407. package/src/platform/constants/versions.ts +3 -3
  408. package/src/platform/registry/__tests__/resource-registry-static.test.ts +347 -347
  409. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +1028 -1028
  410. package/src/platform/registry/__tests__/resource-registry.list-executable.test.ts +393 -393
  411. package/src/platform/registry/__tests__/resource-registry.test.ts +2005 -2005
  412. package/src/platform/registry/__tests__/serialization.test.ts +1127 -1127
  413. package/src/platform/registry/command-view.ts +180 -180
  414. package/src/platform/registry/domains.ts +165 -165
  415. package/src/platform/registry/index.ts +93 -93
  416. package/src/platform/registry/reserved.ts +24 -24
  417. package/src/platform/registry/resource-metadata.ts +59 -59
  418. package/src/platform/registry/resource-registry.command-queue-groups.test.ts +129 -129
  419. package/src/platform/registry/resource-registry.ts +876 -876
  420. package/src/platform/registry/serialization.ts +273 -273
  421. package/src/platform/registry/serialized-types.ts +231 -231
  422. package/src/platform/registry/stats-types.ts +66 -66
  423. package/src/platform/registry/types.ts +404 -404
  424. package/src/platform/registry/validation.ts +513 -513
  425. package/src/platform/resilience/__tests__/rate-limiter.test.ts +471 -471
  426. package/src/platform/resilience/circuit-breaker.ts +164 -164
  427. package/src/platform/resilience/errors.ts +68 -68
  428. package/src/platform/resilience/http-error-mapper.ts +129 -129
  429. package/src/platform/resilience/index.ts +93 -93
  430. package/src/platform/resilience/rate-limiter-types.ts +46 -46
  431. package/src/platform/resilience/rate-limiter.ts +140 -140
  432. package/src/platform/resilience/retry.ts +89 -89
  433. package/src/platform/resilience/timeout.ts +63 -63
  434. package/src/platform/sse/events.ts +37 -37
  435. package/src/platform/sse/index.ts +7 -7
  436. package/src/platform/utils/__tests__/validation.test.ts +1083 -1083
  437. package/src/platform/utils/currency.ts +96 -96
  438. package/src/platform/utils/debounce.ts +52 -52
  439. package/src/platform/utils/error.ts +41 -41
  440. package/src/platform/utils/hmac.test.ts +97 -97
  441. package/src/platform/utils/index.ts +32 -32
  442. package/src/platform/utils/server/betterstack-logger.ts +210 -210
  443. package/src/platform/utils/server/hmac.ts +44 -44
  444. package/src/platform/utils/server/unsubscribe.ts +111 -111
  445. package/src/platform/utils/token-counter.ts +96 -96
  446. package/src/platform/utils/validation.ts +425 -425
  447. package/src/projects/api-schemas.ts +268 -268
  448. package/src/published.ts +1 -1
  449. package/src/reference/_generated/contracts.md +607 -607
  450. package/src/reference/glossary.md +105 -105
  451. package/src/requests/__tests__/api-schemas.test.ts +277 -277
  452. package/src/requests/api-schemas.ts +83 -83
  453. package/src/requests/index.ts +1 -1
  454. package/src/scaffold-registry/__tests__/index.test.ts +17 -0
  455. package/src/scaffold-registry/__tests__/schema.test.ts +329 -230
  456. package/src/scaffold-registry/index.ts +205 -189
  457. package/src/scaffold-registry/schema.ts +196 -128
  458. package/src/server.ts +272 -272
  459. package/src/supabase/database.types.ts +2719 -2719
  460. package/src/supabase/helpers.ts +20 -20
  461. package/src/supabase/index.ts +52 -52
  462. package/src/supabase/server/client.ts +58 -58
  463. package/src/test-utils/README.md +30 -138
  464. package/src/test-utils/browser-mocks.ts +54 -54
  465. package/src/test-utils/fixtures/api-keys.ts +52 -52
  466. package/src/test-utils/fixtures/index.ts +4 -4
  467. package/src/test-utils/fixtures/memberships.ts +80 -80
  468. package/src/test-utils/fixtures/organizations.ts +69 -69
  469. package/src/test-utils/fixtures/users.ts +79 -79
  470. package/src/test-utils/index.ts +7 -8
  471. package/src/test-utils/mocks/index.ts +2 -2
  472. package/src/test-utils/mocks/supabase.ts +142 -142
  473. package/src/test-utils/mocks/workos.ts +108 -108
  474. package/src/test-utils/published.ts +4 -0
  475. package/src/test-utils/rls/RLSTestContext.ts +554 -554
  476. package/src/test-utils/rls/index.ts +1 -1
@@ -1,2005 +1,2005 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { z } from 'zod'
3
- import { ResourceRegistry } from '../resource-registry'
4
- import type { RemoteOrgConfig } from '../resource-registry'
5
- import type { WorkflowDefinition } from '../../../execution/engine/workflow/types'
6
- import type { AgentDefinition } from '../../../execution/engine/agent/core/types'
7
- import type { ModelConfig } from '../../../execution/engine/llm/model-info'
8
-
9
- describe('ResourceRegistry', () => {
10
- // Mock data helpers
11
- const createMockWorkflow = (resourceId: string, status: 'dev' | 'prod' = 'dev'): WorkflowDefinition => ({
12
- config: {
13
- resourceId,
14
- name: `Workflow ${resourceId}`,
15
- description: `Test workflow ${resourceId}`,
16
- version: '1.0.0',
17
- type: 'workflow',
18
- status
19
- },
20
- contract: {
21
- inputSchema: z.object({ data: z.string() }),
22
- outputSchema: z.object({ result: z.boolean() })
23
- },
24
- steps: {
25
- step1: {
26
- id: 'step1',
27
- name: 'First Step',
28
- description: 'First step',
29
- handler: async () => ({ result: true }),
30
- inputSchema: z.object({ data: z.string() }),
31
- outputSchema: z.object({ result: z.boolean() }),
32
- next: null
33
- }
34
- },
35
- entryPoint: 'step1'
36
- })
37
-
38
- const createMockAgent = (resourceId: string, status: 'dev' | 'prod' = 'dev'): AgentDefinition => ({
39
- config: {
40
- resourceId,
41
- name: `Agent ${resourceId}`,
42
- description: `Test agent ${resourceId}`,
43
- version: '1.0.0',
44
- type: 'agent',
45
- status,
46
- systemPrompt: 'You are a test agent'
47
- },
48
- contract: {
49
- inputSchema: z.object({ query: z.string() }),
50
- outputSchema: z.object({ response: z.string() })
51
- },
52
- tools: [],
53
- modelConfig: {
54
- provider: 'mock',
55
- apiKey: 'test-key',
56
- model: 'mock'
57
- } as ModelConfig
58
- })
59
-
60
- describe('getResourceDefinition', () => {
61
- it('returns workflow definition by resourceId', () => {
62
- const workflow = createMockWorkflow('test-workflow')
63
- const registry = new ResourceRegistry({
64
- 'test-org': { workflows: [workflow] }
65
- })
66
-
67
- const result = registry.getResourceDefinition('test-org', 'test-workflow')
68
-
69
- expect(result).toBe(workflow)
70
- expect(result?.config.type).toBe('workflow')
71
- })
72
-
73
- it('returns agent definition by resourceId', () => {
74
- const agent = createMockAgent('test-agent')
75
- const registry = new ResourceRegistry({
76
- 'test-org': { agents: [agent] }
77
- })
78
-
79
- const result = registry.getResourceDefinition('test-org', 'test-agent')
80
-
81
- expect(result).toBe(agent)
82
- expect(result?.config.type).toBe('agent')
83
- })
84
-
85
- it('returns null for non-existent organization', () => {
86
- const registry = new ResourceRegistry({})
87
-
88
- const result = registry.getResourceDefinition('nonexistent-org', 'test-workflow')
89
-
90
- expect(result).toBeNull()
91
- })
92
-
93
- it('returns null for non-existent resource in existing organization', () => {
94
- const registry = new ResourceRegistry({
95
- 'test-org': { workflows: [] }
96
- })
97
-
98
- const result = registry.getResourceDefinition('test-org', 'nonexistent-resource')
99
-
100
- expect(result).toBeNull()
101
- })
102
-
103
- it('handles organization with only workflows', () => {
104
- const workflow = createMockWorkflow('test-workflow')
105
- const registry = new ResourceRegistry({
106
- 'test-org': { workflows: [workflow] }
107
- })
108
-
109
- const result = registry.getResourceDefinition('test-org', 'test-workflow')
110
-
111
- expect(result).toBe(workflow)
112
- })
113
-
114
- it('handles organization with only agents', () => {
115
- const agent = createMockAgent('test-agent')
116
- const registry = new ResourceRegistry({
117
- 'test-org': { agents: [agent] }
118
- })
119
-
120
- const result = registry.getResourceDefinition('test-org', 'test-agent')
121
-
122
- expect(result).toBe(agent)
123
- })
124
-
125
- it('handles multiple organizations', () => {
126
- const workflow1 = createMockWorkflow('workflow-1')
127
- const workflow2 = createMockWorkflow('workflow-2')
128
-
129
- const registry = new ResourceRegistry({
130
- 'org-1': { workflows: [workflow1] },
131
- 'org-2': { workflows: [workflow2] }
132
- })
133
-
134
- expect(registry.getResourceDefinition('org-1', 'workflow-1')).toBe(workflow1)
135
- expect(registry.getResourceDefinition('org-2', 'workflow-2')).toBe(workflow2)
136
- expect(registry.getResourceDefinition('org-1', 'workflow-2')).toBeNull()
137
- })
138
- })
139
-
140
- describe('listResourcesForOrganization', () => {
141
- it('returns empty list for non-existent organization', () => {
142
- const registry = new ResourceRegistry({})
143
-
144
- const result = registry.listResourcesForOrganization('nonexistent-org')
145
-
146
- expect(result).toEqual({
147
- workflows: [],
148
- agents: [],
149
- total: 0,
150
- organizationName: 'nonexistent-org',
151
- environment: undefined
152
- })
153
- })
154
-
155
- it('lists all workflows for organization', () => {
156
- const workflow1 = createMockWorkflow('workflow-1')
157
- const workflow2 = createMockWorkflow('workflow-2')
158
-
159
- const registry = new ResourceRegistry({
160
- 'test-org': { workflows: [workflow1, workflow2] }
161
- })
162
-
163
- const result = registry.listResourcesForOrganization('test-org')
164
-
165
- expect(result.workflows).toHaveLength(2)
166
- expect(result.workflows[0]).toEqual({
167
- resourceId: 'workflow-1',
168
- name: 'Workflow workflow-1',
169
- description: 'Test workflow workflow-1',
170
- version: '1.0.0',
171
- type: 'workflow',
172
- status: 'dev',
173
- origin: 'local'
174
- })
175
- expect(result.agents).toHaveLength(0)
176
- expect(result.total).toBe(2)
177
- })
178
-
179
- it('lists all agents for organization', () => {
180
- const agent1 = createMockAgent('agent-1')
181
- const agent2 = createMockAgent('agent-2')
182
-
183
- const registry = new ResourceRegistry({
184
- 'test-org': { agents: [agent1, agent2] }
185
- })
186
-
187
- const result = registry.listResourcesForOrganization('test-org')
188
-
189
- expect(result.agents).toHaveLength(2)
190
- expect(result.agents[0]).toEqual({
191
- resourceId: 'agent-1',
192
- name: 'Agent agent-1',
193
- description: 'Test agent agent-1',
194
- version: '1.0.0',
195
- type: 'agent',
196
- status: 'dev',
197
- sessionCapable: false,
198
- origin: 'local'
199
- })
200
- expect(result.workflows).toHaveLength(0)
201
- expect(result.total).toBe(2)
202
- })
203
-
204
- it('lists both workflows and agents', () => {
205
- const workflow = createMockWorkflow('workflow-1')
206
- const agent = createMockAgent('agent-1')
207
-
208
- const registry = new ResourceRegistry({
209
- 'test-org': {
210
- workflows: [workflow],
211
- agents: [agent]
212
- }
213
- })
214
-
215
- const result = registry.listResourcesForOrganization('test-org')
216
-
217
- expect(result.workflows).toHaveLength(1)
218
- expect(result.agents).toHaveLength(1)
219
- expect(result.total).toBe(2)
220
- })
221
-
222
- it('filters resources by dev environment', () => {
223
- const devWorkflow = createMockWorkflow('workflow-dev', 'dev')
224
- const prodWorkflow = createMockWorkflow('workflow-prod', 'prod')
225
-
226
- const registry = new ResourceRegistry({
227
- 'test-org': { workflows: [devWorkflow, prodWorkflow] }
228
- })
229
-
230
- const result = registry.listResourcesForOrganization('test-org', 'dev')
231
-
232
- expect(result.workflows).toHaveLength(1)
233
- expect(result.workflows[0].resourceId).toBe('workflow-dev')
234
- expect(result.workflows[0].status).toBe('dev')
235
- expect(result.environment).toBe('dev')
236
- })
237
-
238
- it('filters resources by prod environment', () => {
239
- const devAgent = createMockAgent('agent-dev', 'dev')
240
- const prodAgent = createMockAgent('agent-prod', 'prod')
241
-
242
- const registry = new ResourceRegistry({
243
- 'test-org': { agents: [devAgent, prodAgent] }
244
- })
245
-
246
- const result = registry.listResourcesForOrganization('test-org', 'prod')
247
-
248
- expect(result.agents).toHaveLength(1)
249
- expect(result.agents[0].resourceId).toBe('agent-prod')
250
- expect(result.agents[0].status).toBe('prod')
251
- expect(result.environment).toBe('prod')
252
- })
253
-
254
- it('filters both workflows and agents by environment', () => {
255
- const devWorkflow = createMockWorkflow('workflow-dev', 'dev')
256
- const prodWorkflow = createMockWorkflow('workflow-prod', 'prod')
257
- const devAgent = createMockAgent('agent-dev', 'dev')
258
- const prodAgent = createMockAgent('agent-prod', 'prod')
259
-
260
- const registry = new ResourceRegistry({
261
- 'test-org': {
262
- workflows: [devWorkflow, prodWorkflow],
263
- agents: [devAgent, prodAgent]
264
- }
265
- })
266
-
267
- const devResult = registry.listResourcesForOrganization('test-org', 'dev')
268
- expect(devResult.total).toBe(2)
269
- expect(devResult.workflows[0].status).toBe('dev')
270
- expect(devResult.agents[0].status).toBe('dev')
271
-
272
- const prodResult = registry.listResourcesForOrganization('test-org', 'prod')
273
- expect(prodResult.total).toBe(2)
274
- expect(prodResult.workflows[0].status).toBe('prod')
275
- expect(prodResult.agents[0].status).toBe('prod')
276
- })
277
-
278
- it('handles empty workflows array', () => {
279
- const registry = new ResourceRegistry({
280
- 'test-org': { workflows: [] }
281
- })
282
-
283
- const result = registry.listResourcesForOrganization('test-org')
284
-
285
- expect(result.workflows).toEqual([])
286
- expect(result.total).toBe(0)
287
- })
288
-
289
- it('handles empty agents array', () => {
290
- const registry = new ResourceRegistry({
291
- 'test-org': { agents: [] }
292
- })
293
-
294
- const result = registry.listResourcesForOrganization('test-org')
295
-
296
- expect(result.agents).toEqual([])
297
- expect(result.total).toBe(0)
298
- })
299
-
300
- it('includes organizationName in result', () => {
301
- const registry = new ResourceRegistry({
302
- 'test-org': { workflows: [] }
303
- })
304
-
305
- const result = registry.listResourcesForOrganization('test-org')
306
-
307
- expect(result.organizationName).toBe('test-org')
308
- })
309
- })
310
-
311
- describe('listAllResources', () => {
312
- it('returns entire registry', () => {
313
- const workflow = createMockWorkflow('workflow-1')
314
- const agent = createMockAgent('agent-1')
315
-
316
- const registryData = {
317
- 'org-1': { workflows: [workflow] },
318
- 'org-2': { agents: [agent] }
319
- }
320
-
321
- const registry = new ResourceRegistry(registryData)
322
-
323
- const result = registry.listAllResources()
324
-
325
- expect(result).toBe(registryData)
326
- })
327
-
328
- it('returns empty object for empty registry', () => {
329
- const registry = new ResourceRegistry({})
330
-
331
- const result = registry.listAllResources()
332
-
333
- expect(result).toEqual({})
334
- })
335
-
336
- it('returns all organizations and resources', () => {
337
- const registryData = {
338
- 'org-1': {
339
- workflows: [createMockWorkflow('w1'), createMockWorkflow('w2')],
340
- agents: [createMockAgent('a1')]
341
- },
342
- 'org-2': {
343
- workflows: [createMockWorkflow('w3')],
344
- agents: [createMockAgent('a2'), createMockAgent('a3')]
345
- },
346
- 'org-3': {
347
- workflows: [],
348
- agents: []
349
- }
350
- }
351
-
352
- const registry = new ResourceRegistry(registryData)
353
-
354
- const result = registry.listAllResources()
355
-
356
- expect(Object.keys(result)).toHaveLength(3)
357
- expect(result['org-1'].workflows).toHaveLength(2)
358
- expect(result['org-1'].agents).toHaveLength(1)
359
- expect(result['org-2'].workflows).toHaveLength(1)
360
- expect(result['org-2'].agents).toHaveLength(2)
361
- })
362
- })
363
-
364
- describe('edge cases and organization isolation', () => {
365
- it('maintains organization isolation (resources not shared)', () => {
366
- const workflow1 = createMockWorkflow('workflow-1')
367
- const workflow2 = createMockWorkflow('workflow-2')
368
-
369
- const registry = new ResourceRegistry({
370
- 'org-1': { workflows: [workflow1] },
371
- 'org-2': { workflows: [workflow2] }
372
- })
373
-
374
- // org-1 should only see workflow-1
375
- expect(registry.getResourceDefinition('org-1', 'workflow-1')).toBe(workflow1)
376
- expect(registry.getResourceDefinition('org-1', 'workflow-2')).toBeNull()
377
-
378
- // org-2 should only see workflow-2
379
- expect(registry.getResourceDefinition('org-2', 'workflow-2')).toBe(workflow2)
380
- expect(registry.getResourceDefinition('org-2', 'workflow-1')).toBeNull()
381
- })
382
-
383
- it('handles organization with undefined workflows', () => {
384
- const registry = new ResourceRegistry({
385
- 'test-org': { agents: [createMockAgent('agent-1')] }
386
- })
387
-
388
- const result = registry.listResourcesForOrganization('test-org')
389
-
390
- expect(result.workflows).toEqual([])
391
- expect(result.agents).toHaveLength(1)
392
- })
393
-
394
- it('handles organization with undefined agents', () => {
395
- const registry = new ResourceRegistry({
396
- 'test-org': { workflows: [createMockWorkflow('workflow-1')] }
397
- })
398
-
399
- const result = registry.listResourcesForOrganization('test-org')
400
-
401
- expect(result.workflows).toHaveLength(1)
402
- expect(result.agents).toEqual([])
403
- })
404
-
405
- it('handles large number of resources', () => {
406
- const workflows = Array.from({ length: 100 }, (_, i) => createMockWorkflow(`workflow-${i}`))
407
- const agents = Array.from({ length: 100 }, (_, i) => createMockAgent(`agent-${i}`))
408
-
409
- const registry = new ResourceRegistry({
410
- 'test-org': { workflows, agents }
411
- })
412
-
413
- const result = registry.listResourcesForOrganization('test-org')
414
-
415
- expect(result.workflows).toHaveLength(100)
416
- expect(result.agents).toHaveLength(100)
417
- expect(result.total).toBe(200)
418
- })
419
-
420
- it('handles special characters in organization names', () => {
421
- const workflow = createMockWorkflow('test-workflow')
422
-
423
- const registry = new ResourceRegistry({
424
- 'org-with-dashes': { workflows: [workflow] },
425
- org_with_underscores: { workflows: [workflow] },
426
- 'org.with.dots': { workflows: [workflow] }
427
- })
428
-
429
- expect(registry.getResourceDefinition('org-with-dashes', 'test-workflow')).toBe(workflow)
430
- expect(registry.getResourceDefinition('org_with_underscores', 'test-workflow')).toBe(workflow)
431
- expect(registry.getResourceDefinition('org.with.dots', 'test-workflow')).toBe(workflow)
432
- })
433
- })
434
-
435
- describe('duplicate resourceId validation', () => {
436
- it('throws error on duplicate workflow resourceIds', () => {
437
- expect(() => {
438
- new ResourceRegistry({
439
- 'test-org': {
440
- workflows: [createMockWorkflow('duplicate-id'), createMockWorkflow('duplicate-id')]
441
- }
442
- })
443
- }).toThrow('Duplicate resourceId "duplicate-id" in organization "test-org"')
444
- })
445
-
446
- it('throws error on duplicate agent resourceIds', () => {
447
- expect(() => {
448
- new ResourceRegistry({
449
- 'test-org': {
450
- agents: [createMockAgent('duplicate-id'), createMockAgent('duplicate-id')]
451
- }
452
- })
453
- }).toThrow('Duplicate resourceId "duplicate-id" in organization "test-org"')
454
- })
455
-
456
- it('throws error on workflow/agent resourceId collision', () => {
457
- expect(() => {
458
- new ResourceRegistry({
459
- 'test-org': {
460
- workflows: [createMockWorkflow('collision-id')],
461
- agents: [createMockAgent('collision-id')]
462
- }
463
- })
464
- }).toThrow('Duplicate resourceId "collision-id" in organization "test-org"')
465
- })
466
-
467
- it('allows same resourceId across different organizations', () => {
468
- expect(() => {
469
- new ResourceRegistry({
470
- 'org-1': {
471
- workflows: [createMockWorkflow('same-id')]
472
- },
473
- 'org-2': {
474
- workflows: [createMockWorkflow('same-id')]
475
- }
476
- })
477
- }).not.toThrow()
478
- })
479
-
480
- it('throws error on multiple duplicate resourceIds', () => {
481
- expect(() => {
482
- new ResourceRegistry({
483
- 'test-org': {
484
- workflows: [
485
- createMockWorkflow('id-1'),
486
- createMockWorkflow('id-2'),
487
- createMockWorkflow('id-1') // Duplicate
488
- ]
489
- }
490
- })
491
- }).toThrow('Duplicate resourceId "id-1" in organization "test-org"')
492
- })
493
-
494
- it('validates across multiple organizations independently', () => {
495
- // org-1 has duplicate, org-2 is valid - should fail on org-1
496
- expect(() => {
497
- new ResourceRegistry({
498
- 'org-1': {
499
- workflows: [createMockWorkflow('duplicate'), createMockWorkflow('duplicate')]
500
- },
501
- 'org-2': {
502
- workflows: [createMockWorkflow('valid-id')]
503
- }
504
- })
505
- }).toThrow('Duplicate resourceId "duplicate" in organization "org-1"')
506
- })
507
-
508
- it('allows empty workflows and agents arrays', () => {
509
- expect(() => {
510
- new ResourceRegistry({
511
- 'test-org': {
512
- workflows: [],
513
- agents: []
514
- }
515
- })
516
- }).not.toThrow()
517
- })
518
-
519
- it('allows undefined workflows and agents', () => {
520
- expect(() => {
521
- new ResourceRegistry({
522
- 'test-org': {}
523
- })
524
- }).not.toThrow()
525
- })
526
-
527
- it('validates organization with only workflows', () => {
528
- expect(() => {
529
- new ResourceRegistry({
530
- 'test-org': {
531
- workflows: [createMockWorkflow('dup'), createMockWorkflow('dup')]
532
- }
533
- })
534
- }).toThrow('Duplicate resourceId "dup"')
535
- })
536
-
537
- it('validates organization with only agents', () => {
538
- expect(() => {
539
- new ResourceRegistry({
540
- 'test-org': {
541
- agents: [createMockAgent('dup'), createMockAgent('dup')]
542
- }
543
- })
544
- }).toThrow('Duplicate resourceId "dup"')
545
- })
546
-
547
- it('includes organization name in error message', () => {
548
- expect(() => {
549
- new ResourceRegistry({
550
- 'my-special-org': {
551
- workflows: [createMockWorkflow('id'), createMockWorkflow('id')]
552
- }
553
- })
554
- }).toThrow('organization "my-special-org"')
555
- })
556
-
557
- it('includes resourceId in error message', () => {
558
- expect(() => {
559
- new ResourceRegistry({
560
- 'test-org': {
561
- workflows: [createMockWorkflow('my-workflow-id'), createMockWorkflow('my-workflow-id')]
562
- }
563
- })
564
- }).toThrow('resourceId "my-workflow-id"')
565
- })
566
- })
567
-
568
- describe('model config validation', () => {
569
- it('validates agent model configs on construction', () => {
570
- const validAgent = createMockAgent('valid-agent')
571
- validAgent.modelConfig = {
572
- provider: 'openai',
573
- model: 'gpt-5',
574
- apiKey: 'test-key',
575
- temperature: 1,
576
- maxOutputTokens: 8000
577
- }
578
-
579
- expect(() => {
580
- new ResourceRegistry({
581
- 'test-org': { agents: [validAgent] }
582
- })
583
- }).not.toThrow()
584
- })
585
-
586
- it('throws error for invalid agent temperature', () => {
587
- const invalidAgent = createMockAgent('invalid-agent')
588
- invalidAgent.modelConfig = {
589
- provider: 'openai',
590
- model: 'gpt-5',
591
- apiKey: 'test-key',
592
- temperature: 0.7, // Invalid - gpt-5 requires temperature=1
593
- maxOutputTokens: 8000
594
- }
595
-
596
- expect(() => {
597
- new ResourceRegistry({
598
- 'test-org': { agents: [invalidAgent] }
599
- })
600
- }).toThrow('Invalid model config in test-org/invalid-agent')
601
- expect(() => {
602
- new ResourceRegistry({
603
- 'test-org': { agents: [invalidAgent] }
604
- })
605
- }).toThrow('expected 1 (field: temperature)')
606
- })
607
-
608
- it('throws error for invalid agent maxOutputTokens', () => {
609
- const invalidAgent = createMockAgent('invalid-agent')
610
- invalidAgent.modelConfig = {
611
- provider: 'openai',
612
- model: 'gpt-5',
613
- apiKey: 'test-key',
614
- temperature: 1,
615
- maxOutputTokens: 2000 // Invalid - below minimum of 4000
616
- }
617
-
618
- expect(() => {
619
- new ResourceRegistry({
620
- 'test-org': { agents: [invalidAgent] }
621
- })
622
- }).toThrow('Invalid model config in test-org/invalid-agent')
623
- expect(() => {
624
- new ResourceRegistry({
625
- 'test-org': { agents: [invalidAgent] }
626
- })
627
- }).toThrow('expected number to be >=4000 (field: maxOutputTokens)')
628
- })
629
-
630
- it('validates workflow model configs if present', () => {
631
- const workflowWithModel = createMockWorkflow('workflow-with-model') as unknown as Record<string, unknown>
632
- ;(workflowWithModel as Record<string, unknown>).modelConfig = {
633
- provider: 'openai',
634
- model: 'gpt-5',
635
- apiKey: 'test-key',
636
- temperature: 0.5, // Invalid
637
- maxOutputTokens: 8000
638
- }
639
-
640
- expect(() => {
641
- new ResourceRegistry({
642
- 'test-org': { workflows: [workflowWithModel] }
643
- })
644
- }).toThrow('Invalid model config in test-org/workflow-with-model')
645
- })
646
-
647
- it('allows workflows without model configs', () => {
648
- const workflowWithoutModel = createMockWorkflow('workflow-no-model')
649
-
650
- expect(() => {
651
- new ResourceRegistry({
652
- 'test-org': { workflows: [workflowWithoutModel] }
653
- })
654
- }).not.toThrow()
655
- })
656
-
657
- it('validates multiple agents in same organization', () => {
658
- const validAgent1 = createMockAgent('agent-1')
659
- validAgent1.modelConfig = {
660
- provider: 'openai',
661
- model: 'gpt-5',
662
- apiKey: 'test-key',
663
- temperature: 1,
664
- maxOutputTokens: 8000
665
- }
666
-
667
- const invalidAgent2 = createMockAgent('agent-2')
668
- invalidAgent2.modelConfig = {
669
- provider: 'openai',
670
- model: 'gpt-5-mini',
671
- apiKey: 'test-key',
672
- temperature: 0.7, // Invalid
673
- maxOutputTokens: 8000
674
- }
675
-
676
- expect(() => {
677
- new ResourceRegistry({
678
- 'test-org': { agents: [validAgent1, invalidAgent2] }
679
- })
680
- }).toThrow('Invalid model config in test-org/agent-2')
681
- })
682
-
683
- it('validates across multiple organizations', () => {
684
- const invalidAgent = createMockAgent('invalid-agent')
685
- invalidAgent.modelConfig = {
686
- provider: 'openai',
687
- model: 'gpt-5',
688
- apiKey: 'test-key',
689
- temperature: 0.7,
690
- maxOutputTokens: 8000
691
- }
692
-
693
- expect(() => {
694
- new ResourceRegistry({
695
- 'org-1': { agents: [createMockAgent('valid-agent')] },
696
- 'org-2': { agents: [invalidAgent] }
697
- })
698
- }).toThrow('Invalid model config in org-2/invalid-agent')
699
- })
700
-
701
- it('includes field name in error message', () => {
702
- const invalidAgent = createMockAgent('invalid-agent')
703
- invalidAgent.modelConfig = {
704
- provider: 'openai',
705
- model: 'gpt-5',
706
- apiKey: 'test-key',
707
- temperature: 0.7,
708
- maxOutputTokens: 8000
709
- }
710
-
711
- expect(() => {
712
- new ResourceRegistry({
713
- 'test-org': { agents: [invalidAgent] }
714
- })
715
- }).toThrow('field: temperature')
716
- })
717
-
718
- it('allows mock models with any temperature', () => {
719
- const mockAgent = createMockAgent('mock-agent')
720
- mockAgent.modelConfig = {
721
- provider: 'mock',
722
- model: 'mock',
723
- apiKey: 'test-key',
724
- temperature: 0.5, // Any temperature OK for mock
725
- maxOutputTokens: 1000
726
- }
727
-
728
- expect(() => {
729
- new ResourceRegistry({
730
- 'test-org': { agents: [mockAgent] }
731
- })
732
- }).not.toThrow()
733
- })
734
- })
735
-
736
- describe('environment filtering', () => {
737
- it('shows all resources in development environment', () => {
738
- // Mock development environment
739
- const originalEnv = process.env.NODE_ENV
740
- process.env.NODE_ENV = 'development'
741
-
742
- const devWorkflow = createMockWorkflow('dev-workflow', 'dev')
743
- const prodWorkflow = createMockWorkflow('prod-workflow', 'prod')
744
- const devAgent = createMockAgent('dev-agent', 'dev')
745
- const prodAgent = createMockAgent('prod-agent', 'prod')
746
-
747
- const registry = new ResourceRegistry({
748
- 'test-org': {
749
- workflows: [devWorkflow, prodWorkflow],
750
- agents: [devAgent, prodAgent]
751
- }
752
- })
753
-
754
- const result = registry.listResourcesForOrganization('test-org')
755
-
756
- expect(result.total).toBe(4) // All resources visible
757
- expect(result.workflows).toHaveLength(2)
758
- expect(result.agents).toHaveLength(2)
759
- expect(result.workflows.some((w) => w.status === 'dev')).toBe(true)
760
- expect(result.workflows.some((w) => w.status === 'prod')).toBe(true)
761
-
762
- // Restore environment
763
- process.env.NODE_ENV = originalEnv
764
- })
765
-
766
- it('shows only prod resources when explicit environment filter is passed', () => {
767
- const devWorkflow = createMockWorkflow('dev-workflow', 'dev')
768
- const prodWorkflow = createMockWorkflow('prod-workflow', 'prod')
769
- const devAgent = createMockAgent('dev-agent', 'dev')
770
- const prodAgent = createMockAgent('prod-agent', 'prod')
771
-
772
- const registry = new ResourceRegistry({
773
- 'test-org': {
774
- workflows: [devWorkflow, prodWorkflow],
775
- agents: [devAgent, prodAgent]
776
- }
777
- })
778
-
779
- const result = registry.listResourcesForOrganization('test-org', 'prod')
780
-
781
- expect(result.total).toBe(2) // Only prod resources
782
- expect(result.workflows).toHaveLength(1)
783
- expect(result.agents).toHaveLength(1)
784
- expect(result.workflows[0].status).toBe('prod')
785
- expect(result.agents[0].status).toBe('prod')
786
- expect(result.workflows.some((w) => w.status === 'dev')).toBe(false)
787
- expect(result.agents.some((a) => a.status === 'dev')).toBe(false)
788
- })
789
-
790
- it('returns all resources when no environment filter is passed', () => {
791
- const devWorkflow = createMockWorkflow('dev-workflow', 'dev')
792
- const prodWorkflow = createMockWorkflow('prod-workflow', 'prod')
793
-
794
- const registry = new ResourceRegistry({
795
- 'test-org': {
796
- workflows: [devWorkflow, prodWorkflow]
797
- }
798
- })
799
-
800
- // No environment filter -- returns all resources regardless of status
801
- const result = registry.listResourcesForOrganization('test-org')
802
-
803
- expect(result.total).toBe(2)
804
- expect(result.workflows).toHaveLength(2)
805
- })
806
-
807
- it('returns empty list when filtering for prod but only dev resources exist', () => {
808
- const devWorkflow = createMockWorkflow('dev-workflow', 'dev')
809
- const devAgent = createMockAgent('dev-agent', 'dev')
810
-
811
- const registry = new ResourceRegistry({
812
- 'test-org': {
813
- workflows: [devWorkflow],
814
- agents: [devAgent]
815
- }
816
- })
817
-
818
- const result = registry.listResourcesForOrganization('test-org', 'prod')
819
-
820
- expect(result.total).toBe(0)
821
- expect(result.workflows).toHaveLength(0)
822
- expect(result.agents).toHaveLength(0)
823
- })
824
-
825
- it('handles mixed status resources correctly with explicit prod filter', () => {
826
- const registry = new ResourceRegistry({
827
- 'test-org': {
828
- workflows: [
829
- createMockWorkflow('w1', 'dev'),
830
- createMockWorkflow('w2', 'prod'),
831
- createMockWorkflow('w3', 'dev'),
832
- createMockWorkflow('w4', 'prod')
833
- ],
834
- agents: [createMockAgent('a1', 'dev'), createMockAgent('a2', 'prod'), createMockAgent('a3', 'dev')]
835
- }
836
- })
837
-
838
- const result = registry.listResourcesForOrganization('test-org', 'prod')
839
-
840
- expect(result.total).toBe(3) // 2 prod workflows + 1 prod agent
841
- expect(result.workflows).toHaveLength(2)
842
- expect(result.agents).toHaveLength(1)
843
- })
844
-
845
- it('includes environment in response when explicit filter is passed', () => {
846
- const registry = new ResourceRegistry({
847
- 'test-org': {
848
- workflows: [createMockWorkflow('w1', 'prod')]
849
- }
850
- })
851
-
852
- const result = registry.listResourcesForOrganization('test-org', 'prod')
853
-
854
- expect(result.environment).toBe('prod')
855
- })
856
-
857
- it('does not include environment in response for development', () => {
858
- const originalEnv = process.env.NODE_ENV
859
- process.env.NODE_ENV = 'development'
860
-
861
- const registry = new ResourceRegistry({
862
- 'test-org': {
863
- workflows: [createMockWorkflow('w1', 'dev')]
864
- }
865
- })
866
-
867
- const result = registry.listResourcesForOrganization('test-org')
868
-
869
- expect(result.environment).toBeUndefined()
870
-
871
- process.env.NODE_ENV = originalEnv
872
- })
873
- })
874
-
875
- describe('Resource Manifest Accessors', () => {
876
- // Helper to create mock triggers, integrations, etc.
877
- const createMockTrigger = (resourceId: string): import('../types').TriggerDefinition => ({
878
- resourceId,
879
- type: 'trigger',
880
- triggerType: 'manual',
881
- name: `Trigger ${resourceId}`,
882
- description: `Test trigger ${resourceId}`,
883
- version: '1.0.0',
884
- status: 'dev'
885
- // NOTE: No triggers field - will be added in tests as needed to reference valid resources
886
- })
887
-
888
- const createMockIntegration = (resourceId: string): import('../types').IntegrationDefinition => ({
889
- resourceId,
890
- type: 'integration',
891
- name: `Integration ${resourceId}`,
892
- description: `Test integration ${resourceId}`,
893
- version: '1.0.0',
894
- status: 'dev',
895
- provider: 'webhook',
896
- credentialName: 'test-cred'
897
- })
898
-
899
- const createMockExternalResource = (resourceId: string): import('../types').ExternalResourceDefinition => ({
900
- resourceId,
901
- type: 'external',
902
- name: `External ${resourceId}`,
903
- description: `Test external ${resourceId}`,
904
- version: '1.0.0',
905
- status: 'dev',
906
- platform: 'n8n'
907
- // NOTE: No triggeredBy field
908
- })
909
-
910
- const createMockHumanCheckpoint = (resourceId: string): import('../types').HumanCheckpointDefinition => ({
911
- resourceId,
912
- type: 'human',
913
- name: `Human ${resourceId}`,
914
- description: `Test human checkpoint ${resourceId}`,
915
- version: '1.0.0',
916
- status: 'dev'
917
- })
918
-
919
- describe('getTrigger', () => {
920
- it('finds trigger by resourceId', () => {
921
- const agent = createMockAgent('basic-agent')
922
- const trigger = createMockTrigger('test-trigger')
923
- trigger.triggers = { agents: ['basic-agent'] }
924
-
925
- const registry = new ResourceRegistry({
926
- 'test-org': {
927
- agents: [agent],
928
- triggers: [trigger]
929
- }
930
- })
931
-
932
- const result = registry.getTrigger('test-org', 'test-trigger')
933
-
934
- expect(result).toBe(trigger)
935
- expect(result?.resourceId).toBe('test-trigger')
936
- expect(result?.type).toBe('trigger')
937
- })
938
-
939
- it('returns null for non-existent trigger', () => {
940
- const agent = createMockAgent('basic-agent')
941
- const trigger = createMockTrigger('trigger-1')
942
- trigger.triggers = { agents: ['basic-agent'] }
943
-
944
- const registry = new ResourceRegistry({
945
- 'test-org': {
946
- agents: [agent],
947
- triggers: [trigger]
948
- }
949
- })
950
-
951
- const result = registry.getTrigger('test-org', 'nonexistent-trigger')
952
-
953
- expect(result).toBeNull()
954
- })
955
-
956
- it('returns null for non-existent organization', () => {
957
- const registry = new ResourceRegistry({})
958
-
959
- const result = registry.getTrigger('nonexistent-org', 'test-trigger')
960
-
961
- expect(result).toBeNull()
962
- })
963
- })
964
-
965
- describe('getIntegration', () => {
966
- it('finds integration by resourceId', () => {
967
- const integration = createMockIntegration('test-integration')
968
- const registry = new ResourceRegistry({
969
- 'test-org': { integrations: [integration] }
970
- })
971
-
972
- const result = registry.getIntegration('test-org', 'test-integration')
973
-
974
- expect(result).toBe(integration)
975
- expect(result?.resourceId).toBe('test-integration')
976
- expect(result?.type).toBe('integration')
977
- })
978
-
979
- it('returns null for non-existent integration', () => {
980
- const registry = new ResourceRegistry({
981
- 'test-org': { integrations: [createMockIntegration('integration-1')] }
982
- })
983
-
984
- const result = registry.getIntegration('test-org', 'nonexistent-integration')
985
-
986
- expect(result).toBeNull()
987
- })
988
-
989
- it('returns null for non-existent organization', () => {
990
- const registry = new ResourceRegistry({})
991
-
992
- const result = registry.getIntegration('nonexistent-org', 'test-integration')
993
-
994
- expect(result).toBeNull()
995
- })
996
- })
997
-
998
- describe('getExternalResource', () => {
999
- it('finds external resource by resourceId', () => {
1000
- const external = createMockExternalResource('test-external')
1001
- const registry = new ResourceRegistry({
1002
- 'test-org': { externalResources: [external] }
1003
- })
1004
-
1005
- const result = registry.getExternalResource('test-org', 'test-external')
1006
-
1007
- expect(result).toBe(external)
1008
- expect(result?.resourceId).toBe('test-external')
1009
- expect(result?.platform).toBe('n8n')
1010
- })
1011
-
1012
- it('returns null for non-existent external resource', () => {
1013
- const registry = new ResourceRegistry({
1014
- 'test-org': { externalResources: [createMockExternalResource('external-1')] }
1015
- })
1016
-
1017
- const result = registry.getExternalResource('test-org', 'nonexistent-external')
1018
-
1019
- expect(result).toBeNull()
1020
- })
1021
-
1022
- it('returns null for non-existent organization', () => {
1023
- const registry = new ResourceRegistry({})
1024
-
1025
- const result = registry.getExternalResource('nonexistent-org', 'test-external')
1026
-
1027
- expect(result).toBeNull()
1028
- })
1029
- })
1030
-
1031
- describe('getHumanCheckpoint', () => {
1032
- it('finds human checkpoint by resourceId', () => {
1033
- const humanCheckpoint = createMockHumanCheckpoint('test-human')
1034
- const registry = new ResourceRegistry({
1035
- 'test-org': { humanCheckpoints: [humanCheckpoint] }
1036
- })
1037
-
1038
- const result = registry.getHumanCheckpoint('test-org', 'test-human')
1039
-
1040
- expect(result).toBe(humanCheckpoint)
1041
- expect(result?.resourceId).toBe('test-human')
1042
- expect(result?.type).toBe('human')
1043
- })
1044
-
1045
- it('returns null for non-existent human checkpoint', () => {
1046
- const registry = new ResourceRegistry({
1047
- 'test-org': { humanCheckpoints: [createMockHumanCheckpoint('human-1')] }
1048
- })
1049
-
1050
- const result = registry.getHumanCheckpoint('test-org', 'nonexistent-human')
1051
-
1052
- expect(result).toBeNull()
1053
- })
1054
-
1055
- it('returns null for non-existent organization', () => {
1056
- const registry = new ResourceRegistry({})
1057
-
1058
- const result = registry.getHumanCheckpoint('nonexistent-org', 'test-human')
1059
-
1060
- expect(result).toBeNull()
1061
- })
1062
- })
1063
-
1064
- describe('getTriggers, getIntegrations, getExternalResources, getHumanCheckpoints', () => {
1065
- it('returns all triggers for organization', () => {
1066
- const agent = createMockAgent('basic-agent')
1067
- const trigger1 = createMockTrigger('trigger-1')
1068
- trigger1.triggers = { agents: ['basic-agent'] }
1069
- const trigger2 = createMockTrigger('trigger-2')
1070
- trigger2.triggers = { agents: ['basic-agent'] }
1071
-
1072
- const registry = new ResourceRegistry({
1073
- 'test-org': {
1074
- agents: [agent],
1075
- triggers: [trigger1, trigger2]
1076
- }
1077
- })
1078
-
1079
- const result = registry.getTriggers('test-org')
1080
-
1081
- expect(result).toHaveLength(2)
1082
- expect(result[0]).toBe(trigger1)
1083
- expect(result[1]).toBe(trigger2)
1084
- })
1085
-
1086
- it('returns all integrations for organization', () => {
1087
- const integration1 = createMockIntegration('integration-1')
1088
- const integration2 = createMockIntegration('integration-2')
1089
- const registry = new ResourceRegistry({
1090
- 'test-org': { integrations: [integration1, integration2] }
1091
- })
1092
-
1093
- const result = registry.getIntegrations('test-org')
1094
-
1095
- expect(result).toHaveLength(2)
1096
- expect(result[0]).toBe(integration1)
1097
- expect(result[1]).toBe(integration2)
1098
- })
1099
-
1100
- it('returns all external resources for organization', () => {
1101
- const external1 = createMockExternalResource('external-1')
1102
- const external2 = createMockExternalResource('external-2')
1103
- const registry = new ResourceRegistry({
1104
- 'test-org': { externalResources: [external1, external2] }
1105
- })
1106
-
1107
- const result = registry.getExternalResources('test-org')
1108
-
1109
- expect(result).toHaveLength(2)
1110
- expect(result[0]).toBe(external1)
1111
- expect(result[1]).toBe(external2)
1112
- })
1113
-
1114
- it('returns all human checkpoints for organization', () => {
1115
- const human1 = createMockHumanCheckpoint('human-1')
1116
- const human2 = createMockHumanCheckpoint('human-2')
1117
- const registry = new ResourceRegistry({
1118
- 'test-org': { humanCheckpoints: [human1, human2] }
1119
- })
1120
-
1121
- const result = registry.getHumanCheckpoints('test-org')
1122
-
1123
- expect(result).toHaveLength(2)
1124
- expect(result[0]).toBe(human1)
1125
- expect(result[1]).toBe(human2)
1126
- })
1127
-
1128
- it('returns empty array for organization with no manifest data', () => {
1129
- const registry = new ResourceRegistry({
1130
- 'test-org': { workflows: [createMockWorkflow('workflow-1')] }
1131
- })
1132
-
1133
- expect(registry.getTriggers('test-org')).toEqual([])
1134
- expect(registry.getIntegrations('test-org')).toEqual([])
1135
- expect(registry.getExternalResources('test-org')).toEqual([])
1136
- expect(registry.getHumanCheckpoints('test-org')).toEqual([])
1137
- })
1138
- })
1139
- })
1140
-
1141
- describe('getCommandViewData', () => {
1142
- it('returns correct edge types (triggers, uses, approval only)', () => {
1143
- const trigger = {
1144
- resourceId: 'trigger-1',
1145
- type: 'trigger' as const,
1146
- triggerType: 'manual' as const,
1147
- name: 'Manual Trigger',
1148
- description: 'Test trigger',
1149
- version: '1.0.0',
1150
- status: 'dev' as const,
1151
- triggers: { workflows: ['workflow-1'] }
1152
- }
1153
-
1154
- const integration = {
1155
- resourceId: 'integration-1',
1156
- type: 'integration' as const,
1157
- name: 'Test Integration',
1158
- description: 'Test integration',
1159
- version: '1.0.0',
1160
- status: 'dev' as const,
1161
- provider: 'webhook' as const,
1162
- credentialName: 'test-cred'
1163
- }
1164
-
1165
- const humanCheckpoint = {
1166
- resourceId: 'human-1',
1167
- type: 'human' as const,
1168
- name: 'Approval Point',
1169
- description: 'Test human checkpoint',
1170
- version: '1.0.0',
1171
- status: 'dev' as const,
1172
- requestedBy: { workflows: ['workflow-1'] },
1173
- routesTo: { agents: ['agent-1'] }
1174
- }
1175
-
1176
- const relationships = {
1177
- 'workflow-1': {
1178
- uses: { integrations: ['integration-1'] }
1179
- }
1180
- }
1181
-
1182
- const registry = new ResourceRegistry({
1183
- 'test-org': {
1184
- workflows: [createMockWorkflow('workflow-1')],
1185
- agents: [createMockAgent('agent-1')],
1186
- triggers: [trigger],
1187
- integrations: [integration],
1188
- humanCheckpoints: [humanCheckpoint],
1189
- relationships
1190
- }
1191
- })
1192
-
1193
- const result = registry.getCommandViewData('test-org')
1194
-
1195
- // Verify all edge types are valid
1196
- const edgeTypes = new Set(result.edges.map((e) => e.relationship))
1197
- expect(edgeTypes.size).toBeGreaterThan(0)
1198
- for (const edgeType of edgeTypes) {
1199
- expect(['triggers', 'uses', 'approval']).toContain(edgeType)
1200
- }
1201
- })
1202
-
1203
- it('does NOT return edges with type invokes', () => {
1204
- const trigger = {
1205
- resourceId: 'trigger-1',
1206
- type: 'trigger' as const,
1207
- triggerType: 'manual' as const,
1208
- name: 'Manual Trigger',
1209
- description: 'Test trigger',
1210
- version: '1.0.0',
1211
- status: 'dev' as const,
1212
- triggers: { workflows: ['workflow-1'] }
1213
- }
1214
-
1215
- const registry = new ResourceRegistry({
1216
- 'test-org': {
1217
- workflows: [createMockWorkflow('workflow-1')],
1218
- triggers: [trigger]
1219
- }
1220
- })
1221
-
1222
- const result = registry.getCommandViewData('test-org')
1223
-
1224
- // Verify no 'invokes' edge type exists
1225
- const hasInvokes = result.edges.some((e) => e.relationship === 'invokes')
1226
- expect(hasInvokes).toBe(false)
1227
- })
1228
-
1229
- it('includes all resource types in response', () => {
1230
- const registry = new ResourceRegistry({
1231
- 'test-org': {
1232
- workflows: [createMockWorkflow('workflow-1')],
1233
- agents: [createMockAgent('agent-1')],
1234
- triggers: [
1235
- {
1236
- resourceId: 'trigger-1',
1237
- type: 'trigger' as const,
1238
- triggerType: 'manual' as const,
1239
- name: 'Manual Trigger',
1240
- description: 'Test trigger',
1241
- version: '1.0.0',
1242
- status: 'dev' as const,
1243
- triggers: { workflows: ['workflow-1'] }
1244
- }
1245
- ],
1246
- integrations: [
1247
- {
1248
- resourceId: 'integration-1',
1249
- type: 'integration' as const,
1250
- name: 'Test Integration',
1251
- description: 'Test integration',
1252
- version: '1.0.0',
1253
- status: 'dev' as const,
1254
- provider: 'webhook' as const,
1255
- credentialName: 'test-cred'
1256
- }
1257
- ],
1258
- externalResources: [
1259
- {
1260
- resourceId: 'external-1',
1261
- type: 'external' as const,
1262
- name: 'External Resource',
1263
- description: 'Test external',
1264
- version: '1.0.0',
1265
- status: 'dev' as const,
1266
- platform: 'n8n' as const
1267
- }
1268
- ],
1269
- humanCheckpoints: [
1270
- {
1271
- resourceId: 'human-1',
1272
- type: 'human' as const,
1273
- name: 'Approval Point',
1274
- description: 'Test human checkpoint',
1275
- version: '1.0.0',
1276
- status: 'dev' as const
1277
- }
1278
- ]
1279
- }
1280
- })
1281
-
1282
- const result = registry.getCommandViewData('test-org')
1283
-
1284
- expect(result.workflows).toHaveLength(1)
1285
- expect(result.agents).toHaveLength(1)
1286
- expect(result.triggers).toHaveLength(1)
1287
- expect(result.integrations).toHaveLength(1)
1288
- expect(result.externalResources).toHaveLength(1)
1289
- expect(result.humanCheckpoints).toHaveLength(1)
1290
- expect(result.edges).toBeDefined()
1291
- })
1292
-
1293
- it('returns empty data for non-existent organization', () => {
1294
- const registry = new ResourceRegistry({})
1295
-
1296
- const result = registry.getCommandViewData('nonexistent-org')
1297
-
1298
- expect(result).toEqual({
1299
- workflows: [],
1300
- agents: [],
1301
- triggers: [],
1302
- integrations: [],
1303
- externalResources: [],
1304
- humanCheckpoints: [],
1305
- edges: []
1306
- })
1307
- })
1308
-
1309
- it('returns edges referencing valid resourceIds', () => {
1310
- const trigger = {
1311
- resourceId: 'trigger-1',
1312
- type: 'trigger' as const,
1313
- triggerType: 'manual' as const,
1314
- name: 'Manual Trigger',
1315
- description: 'Test trigger',
1316
- version: '1.0.0',
1317
- status: 'dev' as const,
1318
- triggers: { workflows: ['workflow-1'], agents: ['agent-1'] }
1319
- }
1320
-
1321
- const integration = {
1322
- resourceId: 'integration-1',
1323
- type: 'integration' as const,
1324
- name: 'Test Integration',
1325
- description: 'Test integration',
1326
- version: '1.0.0',
1327
- status: 'dev' as const,
1328
- provider: 'webhook' as const,
1329
- credentialName: 'test-cred'
1330
- }
1331
-
1332
- const relationships = {
1333
- 'workflow-1': {
1334
- uses: { integrations: ['integration-1'] }
1335
- }
1336
- }
1337
-
1338
- const registry = new ResourceRegistry({
1339
- 'test-org': {
1340
- workflows: [createMockWorkflow('workflow-1')],
1341
- agents: [createMockAgent('agent-1')],
1342
- triggers: [trigger],
1343
- integrations: [integration],
1344
- relationships
1345
- }
1346
- })
1347
-
1348
- const result = registry.getCommandViewData('test-org')
1349
-
1350
- // Collect all valid resourceIds
1351
- const validIds = new Set(['workflow-1', 'agent-1', 'trigger-1', 'integration-1'])
1352
-
1353
- // Verify all edge sources and targets reference valid resourceIds
1354
- for (const edge of result.edges) {
1355
- expect(validIds.has(edge.source) || validIds.has(edge.target)).toBe(true)
1356
- }
1357
- })
1358
- })
1359
-
1360
- describe('New Field Validation on Construction', () => {
1361
- it('validates triggers use resourceId (not id)', () => {
1362
- const agent = createMockAgent('basic-agent')
1363
- const trigger = {
1364
- resourceId: 'trigger-1',
1365
- type: 'trigger' as const,
1366
- triggerType: 'manual' as const,
1367
- name: 'Manual Trigger',
1368
- description: 'Test trigger',
1369
- version: '1.0.0',
1370
- status: 'dev' as const,
1371
- triggers: { agents: ['basic-agent'] }
1372
- }
1373
-
1374
- expect(() => {
1375
- new ResourceRegistry({
1376
- 'test-org': {
1377
- agents: [agent],
1378
- triggers: [trigger]
1379
- }
1380
- })
1381
- }).not.toThrow()
1382
-
1383
- // Verify trigger is stored with resourceId
1384
- const registry = new ResourceRegistry({
1385
- 'test-org': {
1386
- agents: [agent],
1387
- triggers: [trigger]
1388
- }
1389
- })
1390
- const result = registry.getTrigger('test-org', 'trigger-1')
1391
- expect(result?.resourceId).toBe('trigger-1')
1392
- })
1393
-
1394
- it('validates triggers have triggerType field', () => {
1395
- const workflow = createMockWorkflow('test-workflow')
1396
- const trigger = {
1397
- resourceId: 'trigger-1',
1398
- type: 'trigger' as const,
1399
- triggerType: 'webhook' as const,
1400
- name: 'Webhook Trigger',
1401
- description: 'Test webhook trigger',
1402
- version: '1.0.0',
1403
- status: 'dev' as const,
1404
- webhookPath: '/webhooks/test',
1405
- triggers: { workflows: ['test-workflow'] }
1406
- }
1407
-
1408
- expect(() => {
1409
- new ResourceRegistry({
1410
- 'test-org': {
1411
- workflows: [workflow],
1412
- triggers: [trigger]
1413
- }
1414
- })
1415
- }).not.toThrow()
1416
-
1417
- const registry = new ResourceRegistry({
1418
- 'test-org': {
1419
- workflows: [workflow],
1420
- triggers: [trigger]
1421
- }
1422
- })
1423
- const result = registry.getTrigger('test-org', 'trigger-1')
1424
- expect(result?.triggerType).toBe('webhook')
1425
- })
1426
-
1427
- it('validates integrations have status field', () => {
1428
- const integration = {
1429
- resourceId: 'integration-1',
1430
- type: 'integration' as const,
1431
- name: 'Test Integration',
1432
- description: 'Test integration',
1433
- version: '1.0.0',
1434
- status: 'prod' as const,
1435
- provider: 'webhook' as const,
1436
- credentialName: 'test-cred'
1437
- }
1438
-
1439
- expect(() => {
1440
- new ResourceRegistry({
1441
- 'test-org': { integrations: [integration] }
1442
- })
1443
- }).not.toThrow()
1444
-
1445
- const registry = new ResourceRegistry({
1446
- 'test-org': { integrations: [integration] }
1447
- })
1448
- const result = registry.getIntegration('test-org', 'integration-1')
1449
- expect(result?.status).toBe('prod')
1450
- })
1451
-
1452
- it('validates integrations have version field', () => {
1453
- const integration = {
1454
- resourceId: 'integration-1',
1455
- type: 'integration' as const,
1456
- name: 'Test Integration',
1457
- description: 'Test integration',
1458
- version: '2.1.0',
1459
- status: 'dev' as const,
1460
- provider: 'webhook' as const,
1461
- credentialName: 'test-cred'
1462
- }
1463
-
1464
- expect(() => {
1465
- new ResourceRegistry({
1466
- 'test-org': { integrations: [integration] }
1467
- })
1468
- }).not.toThrow()
1469
-
1470
- const registry = new ResourceRegistry({
1471
- 'test-org': { integrations: [integration] }
1472
- })
1473
- const result = registry.getIntegration('test-org', 'integration-1')
1474
- expect(result?.version).toBe('2.1.0')
1475
- })
1476
-
1477
- it('validates human checkpoints have description (required)', () => {
1478
- const humanCheckpoint = {
1479
- resourceId: 'human-1',
1480
- type: 'human' as const,
1481
- name: 'Approval Point',
1482
- description: 'Human decision point for high-value orders',
1483
- version: '1.0.0',
1484
- status: 'dev' as const
1485
- }
1486
-
1487
- expect(() => {
1488
- new ResourceRegistry({
1489
- 'test-org': { humanCheckpoints: [humanCheckpoint] }
1490
- })
1491
- }).not.toThrow()
1492
-
1493
- const registry = new ResourceRegistry({
1494
- 'test-org': { humanCheckpoints: [humanCheckpoint] }
1495
- })
1496
- const result = registry.getHumanCheckpoint('test-org', 'human-1')
1497
- expect(result?.description).toBe('Human decision point for high-value orders')
1498
- })
1499
- })
1500
-
1501
- describe('Runtime Registration', () => {
1502
- const createMockRemoteConfig = (overrides: Partial<RemoteOrgConfig> = {}): RemoteOrgConfig => ({
1503
- storagePath: 'test-org/deploy-001/bundle.js',
1504
- deploymentId: 'deploy-001',
1505
- ...overrides
1506
- })
1507
-
1508
- describe('registerOrganization -- merge into existing org', () => {
1509
- it('registers remote workflows alongside existing static workflows', () => {
1510
- const staticWorkflow = createMockWorkflow('static-wf')
1511
- const remoteWorkflow = createMockWorkflow('remote-wf')
1512
-
1513
- const registry = new ResourceRegistry({
1514
- 'test-org': { workflows: [staticWorkflow] }
1515
- })
1516
-
1517
- registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1518
-
1519
- const result = registry.listResourcesForOrganization('test-org')
1520
- expect(result.workflows).toHaveLength(2)
1521
- expect(result.workflows.find((w) => w.resourceId === 'static-wf')).toBeDefined()
1522
- expect(result.workflows.find((w) => w.resourceId === 'remote-wf')).toBeDefined()
1523
- })
1524
-
1525
- it('registers remote agents alongside existing static agents', () => {
1526
- const staticAgent = createMockAgent('static-agent')
1527
- const remoteAgent = createMockAgent('remote-agent')
1528
-
1529
- const registry = new ResourceRegistry({
1530
- 'test-org': { agents: [staticAgent] }
1531
- })
1532
-
1533
- registry.registerOrganization('test-org', { agents: [remoteAgent] }, createMockRemoteConfig())
1534
-
1535
- const result = registry.listResourcesForOrganization('test-org')
1536
- expect(result.agents).toHaveLength(2)
1537
- expect(result.agents.find((a) => a.resourceId === 'static-agent')).toBeDefined()
1538
- expect(result.agents.find((a) => a.resourceId === 'remote-agent')).toBeDefined()
1539
- })
1540
-
1541
- it('marks remote resources with origin "remote" and static resources with origin "local"', () => {
1542
- const staticWorkflow = createMockWorkflow('static-wf')
1543
- const remoteWorkflow = createMockWorkflow('remote-wf')
1544
-
1545
- const registry = new ResourceRegistry({
1546
- 'test-org': { workflows: [staticWorkflow] }
1547
- })
1548
-
1549
- registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1550
-
1551
- const result = registry.listResourcesForOrganization('test-org')
1552
- expect(result.workflows.find((w) => w.resourceId === 'static-wf')?.origin).toBe('local')
1553
- expect(result.workflows.find((w) => w.resourceId === 'remote-wf')?.origin).toBe('remote')
1554
- })
1555
-
1556
- it('getRemoteConfig returns config for remote resource and null for static resource', () => {
1557
- const staticWorkflow = createMockWorkflow('static-wf')
1558
- const remoteWorkflow = createMockWorkflow('remote-wf')
1559
- const remoteConfig = createMockRemoteConfig({ deploymentId: 'deploy-xyz' })
1560
-
1561
- const registry = new ResourceRegistry({
1562
- 'test-org': { workflows: [staticWorkflow] }
1563
- })
1564
-
1565
- registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, remoteConfig)
1566
-
1567
- expect(registry.getRemoteConfig('test-org', 'remote-wf')).toMatchObject({
1568
- storagePath: 'test-org/deploy-001/bundle.js',
1569
- deploymentId: 'deploy-xyz'
1570
- })
1571
- expect(registry.getRemoteConfig('test-org', 'static-wf')).toBeNull()
1572
- })
1573
-
1574
- it('isRemote returns true for org with remote resources', () => {
1575
- const staticWorkflow = createMockWorkflow('static-wf')
1576
- const remoteWorkflow = createMockWorkflow('remote-wf')
1577
-
1578
- const registry = new ResourceRegistry({
1579
- 'test-org': { workflows: [staticWorkflow] }
1580
- })
1581
-
1582
- expect(registry.isRemote('test-org')).toBe(false)
1583
-
1584
- registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1585
-
1586
- expect(registry.isRemote('test-org')).toBe(true)
1587
- })
1588
- })
1589
-
1590
- describe('registerOrganization -- conflict detection', () => {
1591
- it('throws when remote resourceId collides with an existing static resource', () => {
1592
- const staticWorkflow = createMockWorkflow('shared-id')
1593
- const remoteWorkflow = createMockWorkflow('shared-id')
1594
-
1595
- const registry = new ResourceRegistry({
1596
- 'test-org': { workflows: [staticWorkflow] }
1597
- })
1598
-
1599
- expect(() => {
1600
- registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1601
- }).toThrow("Resource 'shared-id' already exists in 'test-org' as an internal resource")
1602
- })
1603
-
1604
- it('throws when deployment contains duplicate resourceIds within itself', () => {
1605
- const workflow1 = createMockWorkflow('dup-id')
1606
- const workflow2 = createMockWorkflow('dup-id')
1607
-
1608
- const registry = new ResourceRegistry({})
1609
-
1610
- expect(() => {
1611
- registry.registerOrganization('new-org', { workflows: [workflow1, workflow2] }, createMockRemoteConfig())
1612
- }).toThrow("Duplicate resource ID 'dup-id' in deployment")
1613
- })
1614
-
1615
- it('throws a validation error when incoming relationships reference missing resources after merge', () => {
1616
- const remoteWorkflow = createMockWorkflow('remote-wf')
1617
-
1618
- const registry = new ResourceRegistry({
1619
- 'test-org': {
1620
- workflows: [createMockWorkflow('static-wf')]
1621
- }
1622
- })
1623
-
1624
- expect(() => {
1625
- registry.registerOrganization(
1626
- 'test-org',
1627
- {
1628
- workflows: [remoteWorkflow],
1629
- relationships: {
1630
- 'remote-wf': {
1631
- triggers: { workflows: ['missing-target'] }
1632
- }
1633
- }
1634
- },
1635
- createMockRemoteConfig()
1636
- )
1637
- }).toThrow("[test-org] Resource 'remote-wf' triggers non-existent workflow: missing-target")
1638
- })
1639
- })
1640
-
1641
- describe('registerOrganization -- new org (no static resources)', () => {
1642
- it('registers an org that does not exist in the static registry', () => {
1643
- const remoteWorkflow = createMockWorkflow('remote-wf')
1644
- const remoteAgent = createMockAgent('remote-agent')
1645
-
1646
- const registry = new ResourceRegistry({})
1647
-
1648
- registry.registerOrganization(
1649
- 'new-org',
1650
- {
1651
- workflows: [remoteWorkflow],
1652
- agents: [remoteAgent]
1653
- },
1654
- createMockRemoteConfig()
1655
- )
1656
-
1657
- const result = registry.listResourcesForOrganization('new-org')
1658
- expect(result.workflows).toHaveLength(1)
1659
- expect(result.workflows[0].resourceId).toBe('remote-wf')
1660
- expect(result.agents).toHaveLength(1)
1661
- expect(result.agents[0].resourceId).toBe('remote-agent')
1662
- expect(result.total).toBe(2)
1663
- })
1664
-
1665
- it('getResourceDefinition finds the registered remote resource', () => {
1666
- const remoteWorkflow = createMockWorkflow('remote-wf')
1667
-
1668
- const registry = new ResourceRegistry({})
1669
-
1670
- registry.registerOrganization('new-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1671
-
1672
- const definition = registry.getResourceDefinition('new-org', 'remote-wf')
1673
- expect(definition).not.toBeNull()
1674
- expect(definition?.config.resourceId).toBe('remote-wf')
1675
- expect(definition?.config.type).toBe('workflow')
1676
- })
1677
- })
1678
-
1679
- describe('unregisterOrganization -- cleanup', () => {
1680
- it('remote resources no longer appear in listing after unregister', () => {
1681
- const remoteWorkflow = createMockWorkflow('remote-wf')
1682
-
1683
- const registry = new ResourceRegistry({})
1684
- registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1685
-
1686
- registry.unregisterOrganization('test-org')
1687
-
1688
- const result = registry.listResourcesForOrganization('test-org')
1689
- expect(result.workflows).toHaveLength(0)
1690
- expect(result.total).toBe(0)
1691
- })
1692
-
1693
- it('getRemoteConfig returns null after unregister', () => {
1694
- const remoteWorkflow = createMockWorkflow('remote-wf')
1695
-
1696
- const registry = new ResourceRegistry({})
1697
- registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1698
-
1699
- expect(registry.getRemoteConfig('test-org', 'remote-wf')).not.toBeNull()
1700
-
1701
- registry.unregisterOrganization('test-org')
1702
-
1703
- expect(registry.getRemoteConfig('test-org', 'remote-wf')).toBeNull()
1704
- })
1705
-
1706
- it('isRemote returns false after unregister', () => {
1707
- const remoteWorkflow = createMockWorkflow('remote-wf')
1708
-
1709
- const registry = new ResourceRegistry({})
1710
- registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1711
-
1712
- expect(registry.isRemote('test-org')).toBe(true)
1713
-
1714
- registry.unregisterOrganization('test-org')
1715
-
1716
- expect(registry.isRemote('test-org')).toBe(false)
1717
- })
1718
-
1719
- it('static resources survive unregister -- only remote resources are removed', () => {
1720
- const staticWorkflow = createMockWorkflow('static-wf')
1721
- const remoteWorkflow = createMockWorkflow('remote-wf')
1722
-
1723
- const registry = new ResourceRegistry({
1724
- 'test-org': { workflows: [staticWorkflow] }
1725
- })
1726
- registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1727
-
1728
- // Both should be present before unregister
1729
- expect(registry.listResourcesForOrganization('test-org').workflows).toHaveLength(2)
1730
-
1731
- registry.unregisterOrganization('test-org')
1732
-
1733
- const result = registry.listResourcesForOrganization('test-org')
1734
- expect(result.workflows).toHaveLength(1)
1735
- expect(result.workflows[0].resourceId).toBe('static-wf')
1736
- expect(result.workflows[0].origin).toBe('local')
1737
- })
1738
-
1739
- it('unregistering an org with no remote resources is a no-op', () => {
1740
- const staticWorkflow = createMockWorkflow('static-wf')
1741
-
1742
- const registry = new ResourceRegistry({
1743
- 'test-org': { workflows: [staticWorkflow] }
1744
- })
1745
-
1746
- // Should not throw or alter existing resources
1747
- registry.unregisterOrganization('test-org')
1748
-
1749
- const result = registry.listResourcesForOrganization('test-org')
1750
- expect(result.workflows).toHaveLength(1)
1751
- expect(result.workflows[0].resourceId).toBe('static-wf')
1752
- })
1753
- })
1754
-
1755
- describe('registerOrganization -- redeploy (register twice)', () => {
1756
- it('second registerOrganization call replaces previous remote resources', () => {
1757
- const remoteWorkflowV1 = createMockWorkflow('remote-wf')
1758
- const remoteWorkflowV2 = createMockWorkflow('remote-wf-v2')
1759
-
1760
- const registry = new ResourceRegistry({})
1761
-
1762
- registry.registerOrganization(
1763
- 'test-org',
1764
- { workflows: [remoteWorkflowV1] },
1765
- createMockRemoteConfig({ deploymentId: 'deploy-v1' })
1766
- )
1767
- expect(registry.listResourcesForOrganization('test-org').workflows).toHaveLength(1)
1768
- expect(registry.listResourcesForOrganization('test-org').workflows[0].resourceId).toBe('remote-wf')
1769
-
1770
- registry.registerOrganization(
1771
- 'test-org',
1772
- { workflows: [remoteWorkflowV2] },
1773
- createMockRemoteConfig({ deploymentId: 'deploy-v2' })
1774
- )
1775
-
1776
- const result = registry.listResourcesForOrganization('test-org')
1777
- expect(result.workflows).toHaveLength(1)
1778
- expect(result.workflows[0].resourceId).toBe('remote-wf-v2')
1779
- })
1780
-
1781
- it('getRemoteConfig returns the new config after redeploy, not the old one', () => {
1782
- const remoteWorkflow = createMockWorkflow('remote-wf')
1783
-
1784
- const registry = new ResourceRegistry({})
1785
-
1786
- registry.registerOrganization(
1787
- 'test-org',
1788
- { workflows: [remoteWorkflow] },
1789
- createMockRemoteConfig({
1790
- deploymentId: 'deploy-old',
1791
- storagePath: 'test-org/deploy-old/bundle.js'
1792
- })
1793
- )
1794
-
1795
- expect(registry.getRemoteConfig('test-org', 'remote-wf')?.deploymentId).toBe('deploy-old')
1796
-
1797
- // Redeploy with same resource but new config
1798
- registry.registerOrganization(
1799
- 'test-org',
1800
- { workflows: [remoteWorkflow] },
1801
- createMockRemoteConfig({
1802
- deploymentId: 'deploy-new',
1803
- storagePath: 'test-org/deploy-new/bundle.js'
1804
- })
1805
- )
1806
-
1807
- const config = registry.getRemoteConfig('test-org', 'remote-wf')
1808
- expect(config?.deploymentId).toBe('deploy-new')
1809
- expect(config?.storagePath).toBe('test-org/deploy-new/bundle.js')
1810
- })
1811
-
1812
- it('preserves the current remote deployment when a redeploy fails merged validation', () => {
1813
- const staticWorkflow = createMockWorkflow('static-wf')
1814
- const remoteWorkflow = createMockWorkflow('remote-wf')
1815
- const replacementWorkflow = createMockWorkflow('remote-wf-v2')
1816
-
1817
- const registry = new ResourceRegistry({
1818
- 'test-org': {
1819
- workflows: [staticWorkflow]
1820
- }
1821
- })
1822
-
1823
- registry.registerOrganization(
1824
- 'test-org',
1825
- {
1826
- workflows: [remoteWorkflow],
1827
- relationships: {
1828
- 'static-wf': {
1829
- triggers: { workflows: ['remote-wf'] }
1830
- }
1831
- }
1832
- },
1833
- createMockRemoteConfig({ deploymentId: 'deploy-old' })
1834
- )
1835
-
1836
- expect(() => {
1837
- registry.registerOrganization(
1838
- 'test-org',
1839
- { workflows: [replacementWorkflow] },
1840
- createMockRemoteConfig({ deploymentId: 'deploy-new' })
1841
- )
1842
- }).toThrow("[test-org] Resource 'static-wf' triggers non-existent workflow: remote-wf")
1843
-
1844
- const result = registry.listResourcesForOrganization('test-org')
1845
- expect(result.workflows.find((w) => w.resourceId === 'remote-wf')).toBeDefined()
1846
- expect(result.workflows.find((w) => w.resourceId === 'remote-wf-v2')).toBeUndefined()
1847
- expect(registry.getRemoteConfig('test-org', 'remote-wf')?.deploymentId).toBe('deploy-old')
1848
- })
1849
- })
1850
-
1851
- describe('environment filter with remote resources', () => {
1852
- it('remote dev resources are filtered out when environment is prod', () => {
1853
- const staticProdWorkflow = createMockWorkflow('static-prod', 'prod')
1854
- const remoteDevWorkflow = createMockWorkflow('remote-dev', 'dev')
1855
-
1856
- const registry = new ResourceRegistry({
1857
- 'test-org': { workflows: [staticProdWorkflow] }
1858
- })
1859
- registry.registerOrganization('test-org', { workflows: [remoteDevWorkflow] }, createMockRemoteConfig())
1860
-
1861
- const result = registry.listResourcesForOrganization('test-org', 'prod')
1862
-
1863
- expect(result.workflows).toHaveLength(1)
1864
- expect(result.workflows[0].resourceId).toBe('static-prod')
1865
- expect(result.workflows.find((w) => w.resourceId === 'remote-dev')).toBeUndefined()
1866
- })
1867
-
1868
- it('remote prod resources are included when environment is prod', () => {
1869
- const staticProdWorkflow = createMockWorkflow('static-prod', 'prod')
1870
- const remoteProdWorkflow = createMockWorkflow('remote-prod', 'prod')
1871
-
1872
- const registry = new ResourceRegistry({
1873
- 'test-org': { workflows: [staticProdWorkflow] }
1874
- })
1875
- registry.registerOrganization('test-org', { workflows: [remoteProdWorkflow] }, createMockRemoteConfig())
1876
-
1877
- const result = registry.listResourcesForOrganization('test-org', 'prod')
1878
-
1879
- expect(result.workflows).toHaveLength(2)
1880
- expect(result.workflows.find((w) => w.resourceId === 'static-prod')).toBeDefined()
1881
- expect(result.workflows.find((w) => w.resourceId === 'remote-prod')).toBeDefined()
1882
- expect(result.workflows.find((w) => w.resourceId === 'remote-prod')?.origin).toBe('remote')
1883
- })
1884
- })
1885
- })
1886
-
1887
- describe('Archived Resource Filtering', () => {
1888
- it('excludes archived workflows from registerStaticResources', () => {
1889
- const activeWorkflow = createMockWorkflow('active-wf')
1890
- const archivedWorkflow: WorkflowDefinition = {
1891
- ...createMockWorkflow('archived-wf'),
1892
- config: {
1893
- ...createMockWorkflow('archived-wf').config,
1894
- archived: true
1895
- }
1896
- }
1897
-
1898
- const registry = new ResourceRegistry({})
1899
- registry.registerStaticResources('test-org', {
1900
- workflows: [activeWorkflow, archivedWorkflow]
1901
- })
1902
-
1903
- const result = registry.listResourcesForOrganization('test-org')
1904
-
1905
- expect(result.workflows).toHaveLength(1)
1906
- expect(result.workflows[0].resourceId).toBe('active-wf')
1907
- expect(result.total).toBe(1)
1908
- })
1909
-
1910
- it('excludes archived agents from registerStaticResources', () => {
1911
- const activeAgent = createMockAgent('active-agent')
1912
- const archivedAgent: AgentDefinition = {
1913
- ...createMockAgent('archived-agent'),
1914
- config: {
1915
- ...createMockAgent('archived-agent').config,
1916
- archived: true
1917
- }
1918
- }
1919
-
1920
- const registry = new ResourceRegistry({})
1921
- registry.registerStaticResources('test-org', {
1922
- agents: [activeAgent, archivedAgent]
1923
- })
1924
-
1925
- const result = registry.listResourcesForOrganization('test-org')
1926
-
1927
- expect(result.agents).toHaveLength(1)
1928
- expect(result.agents[0].resourceId).toBe('active-agent')
1929
- expect(result.total).toBe(1)
1930
- })
1931
-
1932
- it('includes resources without archived field (backwards compatibility)', () => {
1933
- const workflow = createMockWorkflow('normal-wf')
1934
- const agent = createMockAgent('normal-agent')
1935
-
1936
- // Verify neither has archived set
1937
- expect(workflow.config).not.toHaveProperty('archived')
1938
- expect(agent.config).not.toHaveProperty('archived')
1939
-
1940
- const registry = new ResourceRegistry({})
1941
- registry.registerStaticResources('test-org', {
1942
- workflows: [workflow],
1943
- agents: [agent]
1944
- })
1945
-
1946
- const result = registry.listResourcesForOrganization('test-org')
1947
-
1948
- expect(result.workflows).toHaveLength(1)
1949
- expect(result.agents).toHaveLength(1)
1950
- expect(result.total).toBe(2)
1951
- })
1952
-
1953
- it('excludes archived workflows from registerOrganization', () => {
1954
- const createMockRemoteConfig = (): RemoteOrgConfig => ({
1955
- storagePath: 'test-org/deploy-001/bundle.js',
1956
- deploymentId: 'deploy-001'
1957
- })
1958
-
1959
- const activeWorkflow = createMockWorkflow('remote-active-wf')
1960
- const archivedWorkflow: WorkflowDefinition = {
1961
- ...createMockWorkflow('remote-archived-wf'),
1962
- config: {
1963
- ...createMockWorkflow('remote-archived-wf').config,
1964
- archived: true
1965
- }
1966
- }
1967
-
1968
- const registry = new ResourceRegistry({})
1969
- registry.registerOrganization(
1970
- 'test-org',
1971
- { workflows: [activeWorkflow, archivedWorkflow] },
1972
- createMockRemoteConfig()
1973
- )
1974
-
1975
- const result = registry.listResourcesForOrganization('test-org')
1976
-
1977
- expect(result.workflows).toHaveLength(1)
1978
- expect(result.workflows[0].resourceId).toBe('remote-active-wf')
1979
- })
1980
-
1981
- it('excludes archived agents from registerOrganization', () => {
1982
- const createMockRemoteConfig = (): RemoteOrgConfig => ({
1983
- storagePath: 'test-org/deploy-001/bundle.js',
1984
- deploymentId: 'deploy-001'
1985
- })
1986
-
1987
- const activeAgent = createMockAgent('remote-active-agent')
1988
- const archivedAgent: AgentDefinition = {
1989
- ...createMockAgent('remote-archived-agent'),
1990
- config: {
1991
- ...createMockAgent('remote-archived-agent').config,
1992
- archived: true
1993
- }
1994
- }
1995
-
1996
- const registry = new ResourceRegistry({})
1997
- registry.registerOrganization('test-org', { agents: [activeAgent, archivedAgent] }, createMockRemoteConfig())
1998
-
1999
- const result = registry.listResourcesForOrganization('test-org')
2000
-
2001
- expect(result.agents).toHaveLength(1)
2002
- expect(result.agents[0].resourceId).toBe('remote-active-agent')
2003
- })
2004
- })
2005
- })
1
+ import { describe, it, expect } from 'vitest'
2
+ import { z } from 'zod'
3
+ import { ResourceRegistry } from '../resource-registry'
4
+ import type { RemoteOrgConfig } from '../resource-registry'
5
+ import type { WorkflowDefinition } from '../../../execution/engine/workflow/types'
6
+ import type { AgentDefinition } from '../../../execution/engine/agent/core/types'
7
+ import type { ModelConfig } from '../../../execution/engine/llm/model-info'
8
+
9
+ describe('ResourceRegistry', () => {
10
+ // Mock data helpers
11
+ const createMockWorkflow = (resourceId: string, status: 'dev' | 'prod' = 'dev'): WorkflowDefinition => ({
12
+ config: {
13
+ resourceId,
14
+ name: `Workflow ${resourceId}`,
15
+ description: `Test workflow ${resourceId}`,
16
+ version: '1.0.0',
17
+ type: 'workflow',
18
+ status
19
+ },
20
+ contract: {
21
+ inputSchema: z.object({ data: z.string() }),
22
+ outputSchema: z.object({ result: z.boolean() })
23
+ },
24
+ steps: {
25
+ step1: {
26
+ id: 'step1',
27
+ name: 'First Step',
28
+ description: 'First step',
29
+ handler: async () => ({ result: true }),
30
+ inputSchema: z.object({ data: z.string() }),
31
+ outputSchema: z.object({ result: z.boolean() }),
32
+ next: null
33
+ }
34
+ },
35
+ entryPoint: 'step1'
36
+ })
37
+
38
+ const createMockAgent = (resourceId: string, status: 'dev' | 'prod' = 'dev'): AgentDefinition => ({
39
+ config: {
40
+ resourceId,
41
+ name: `Agent ${resourceId}`,
42
+ description: `Test agent ${resourceId}`,
43
+ version: '1.0.0',
44
+ type: 'agent',
45
+ status,
46
+ systemPrompt: 'You are a test agent'
47
+ },
48
+ contract: {
49
+ inputSchema: z.object({ query: z.string() }),
50
+ outputSchema: z.object({ response: z.string() })
51
+ },
52
+ tools: [],
53
+ modelConfig: {
54
+ provider: 'mock',
55
+ apiKey: 'test-key',
56
+ model: 'mock'
57
+ } as ModelConfig
58
+ })
59
+
60
+ describe('getResourceDefinition', () => {
61
+ it('returns workflow definition by resourceId', () => {
62
+ const workflow = createMockWorkflow('test-workflow')
63
+ const registry = new ResourceRegistry({
64
+ 'test-org': { workflows: [workflow] }
65
+ })
66
+
67
+ const result = registry.getResourceDefinition('test-org', 'test-workflow')
68
+
69
+ expect(result).toBe(workflow)
70
+ expect(result?.config.type).toBe('workflow')
71
+ })
72
+
73
+ it('returns agent definition by resourceId', () => {
74
+ const agent = createMockAgent('test-agent')
75
+ const registry = new ResourceRegistry({
76
+ 'test-org': { agents: [agent] }
77
+ })
78
+
79
+ const result = registry.getResourceDefinition('test-org', 'test-agent')
80
+
81
+ expect(result).toBe(agent)
82
+ expect(result?.config.type).toBe('agent')
83
+ })
84
+
85
+ it('returns null for non-existent organization', () => {
86
+ const registry = new ResourceRegistry({})
87
+
88
+ const result = registry.getResourceDefinition('nonexistent-org', 'test-workflow')
89
+
90
+ expect(result).toBeNull()
91
+ })
92
+
93
+ it('returns null for non-existent resource in existing organization', () => {
94
+ const registry = new ResourceRegistry({
95
+ 'test-org': { workflows: [] }
96
+ })
97
+
98
+ const result = registry.getResourceDefinition('test-org', 'nonexistent-resource')
99
+
100
+ expect(result).toBeNull()
101
+ })
102
+
103
+ it('handles organization with only workflows', () => {
104
+ const workflow = createMockWorkflow('test-workflow')
105
+ const registry = new ResourceRegistry({
106
+ 'test-org': { workflows: [workflow] }
107
+ })
108
+
109
+ const result = registry.getResourceDefinition('test-org', 'test-workflow')
110
+
111
+ expect(result).toBe(workflow)
112
+ })
113
+
114
+ it('handles organization with only agents', () => {
115
+ const agent = createMockAgent('test-agent')
116
+ const registry = new ResourceRegistry({
117
+ 'test-org': { agents: [agent] }
118
+ })
119
+
120
+ const result = registry.getResourceDefinition('test-org', 'test-agent')
121
+
122
+ expect(result).toBe(agent)
123
+ })
124
+
125
+ it('handles multiple organizations', () => {
126
+ const workflow1 = createMockWorkflow('workflow-1')
127
+ const workflow2 = createMockWorkflow('workflow-2')
128
+
129
+ const registry = new ResourceRegistry({
130
+ 'org-1': { workflows: [workflow1] },
131
+ 'org-2': { workflows: [workflow2] }
132
+ })
133
+
134
+ expect(registry.getResourceDefinition('org-1', 'workflow-1')).toBe(workflow1)
135
+ expect(registry.getResourceDefinition('org-2', 'workflow-2')).toBe(workflow2)
136
+ expect(registry.getResourceDefinition('org-1', 'workflow-2')).toBeNull()
137
+ })
138
+ })
139
+
140
+ describe('listResourcesForOrganization', () => {
141
+ it('returns empty list for non-existent organization', () => {
142
+ const registry = new ResourceRegistry({})
143
+
144
+ const result = registry.listResourcesForOrganization('nonexistent-org')
145
+
146
+ expect(result).toEqual({
147
+ workflows: [],
148
+ agents: [],
149
+ total: 0,
150
+ organizationName: 'nonexistent-org',
151
+ environment: undefined
152
+ })
153
+ })
154
+
155
+ it('lists all workflows for organization', () => {
156
+ const workflow1 = createMockWorkflow('workflow-1')
157
+ const workflow2 = createMockWorkflow('workflow-2')
158
+
159
+ const registry = new ResourceRegistry({
160
+ 'test-org': { workflows: [workflow1, workflow2] }
161
+ })
162
+
163
+ const result = registry.listResourcesForOrganization('test-org')
164
+
165
+ expect(result.workflows).toHaveLength(2)
166
+ expect(result.workflows[0]).toEqual({
167
+ resourceId: 'workflow-1',
168
+ name: 'Workflow workflow-1',
169
+ description: 'Test workflow workflow-1',
170
+ version: '1.0.0',
171
+ type: 'workflow',
172
+ status: 'dev',
173
+ origin: 'local'
174
+ })
175
+ expect(result.agents).toHaveLength(0)
176
+ expect(result.total).toBe(2)
177
+ })
178
+
179
+ it('lists all agents for organization', () => {
180
+ const agent1 = createMockAgent('agent-1')
181
+ const agent2 = createMockAgent('agent-2')
182
+
183
+ const registry = new ResourceRegistry({
184
+ 'test-org': { agents: [agent1, agent2] }
185
+ })
186
+
187
+ const result = registry.listResourcesForOrganization('test-org')
188
+
189
+ expect(result.agents).toHaveLength(2)
190
+ expect(result.agents[0]).toEqual({
191
+ resourceId: 'agent-1',
192
+ name: 'Agent agent-1',
193
+ description: 'Test agent agent-1',
194
+ version: '1.0.0',
195
+ type: 'agent',
196
+ status: 'dev',
197
+ sessionCapable: false,
198
+ origin: 'local'
199
+ })
200
+ expect(result.workflows).toHaveLength(0)
201
+ expect(result.total).toBe(2)
202
+ })
203
+
204
+ it('lists both workflows and agents', () => {
205
+ const workflow = createMockWorkflow('workflow-1')
206
+ const agent = createMockAgent('agent-1')
207
+
208
+ const registry = new ResourceRegistry({
209
+ 'test-org': {
210
+ workflows: [workflow],
211
+ agents: [agent]
212
+ }
213
+ })
214
+
215
+ const result = registry.listResourcesForOrganization('test-org')
216
+
217
+ expect(result.workflows).toHaveLength(1)
218
+ expect(result.agents).toHaveLength(1)
219
+ expect(result.total).toBe(2)
220
+ })
221
+
222
+ it('filters resources by dev environment', () => {
223
+ const devWorkflow = createMockWorkflow('workflow-dev', 'dev')
224
+ const prodWorkflow = createMockWorkflow('workflow-prod', 'prod')
225
+
226
+ const registry = new ResourceRegistry({
227
+ 'test-org': { workflows: [devWorkflow, prodWorkflow] }
228
+ })
229
+
230
+ const result = registry.listResourcesForOrganization('test-org', 'dev')
231
+
232
+ expect(result.workflows).toHaveLength(1)
233
+ expect(result.workflows[0].resourceId).toBe('workflow-dev')
234
+ expect(result.workflows[0].status).toBe('dev')
235
+ expect(result.environment).toBe('dev')
236
+ })
237
+
238
+ it('filters resources by prod environment', () => {
239
+ const devAgent = createMockAgent('agent-dev', 'dev')
240
+ const prodAgent = createMockAgent('agent-prod', 'prod')
241
+
242
+ const registry = new ResourceRegistry({
243
+ 'test-org': { agents: [devAgent, prodAgent] }
244
+ })
245
+
246
+ const result = registry.listResourcesForOrganization('test-org', 'prod')
247
+
248
+ expect(result.agents).toHaveLength(1)
249
+ expect(result.agents[0].resourceId).toBe('agent-prod')
250
+ expect(result.agents[0].status).toBe('prod')
251
+ expect(result.environment).toBe('prod')
252
+ })
253
+
254
+ it('filters both workflows and agents by environment', () => {
255
+ const devWorkflow = createMockWorkflow('workflow-dev', 'dev')
256
+ const prodWorkflow = createMockWorkflow('workflow-prod', 'prod')
257
+ const devAgent = createMockAgent('agent-dev', 'dev')
258
+ const prodAgent = createMockAgent('agent-prod', 'prod')
259
+
260
+ const registry = new ResourceRegistry({
261
+ 'test-org': {
262
+ workflows: [devWorkflow, prodWorkflow],
263
+ agents: [devAgent, prodAgent]
264
+ }
265
+ })
266
+
267
+ const devResult = registry.listResourcesForOrganization('test-org', 'dev')
268
+ expect(devResult.total).toBe(2)
269
+ expect(devResult.workflows[0].status).toBe('dev')
270
+ expect(devResult.agents[0].status).toBe('dev')
271
+
272
+ const prodResult = registry.listResourcesForOrganization('test-org', 'prod')
273
+ expect(prodResult.total).toBe(2)
274
+ expect(prodResult.workflows[0].status).toBe('prod')
275
+ expect(prodResult.agents[0].status).toBe('prod')
276
+ })
277
+
278
+ it('handles empty workflows array', () => {
279
+ const registry = new ResourceRegistry({
280
+ 'test-org': { workflows: [] }
281
+ })
282
+
283
+ const result = registry.listResourcesForOrganization('test-org')
284
+
285
+ expect(result.workflows).toEqual([])
286
+ expect(result.total).toBe(0)
287
+ })
288
+
289
+ it('handles empty agents array', () => {
290
+ const registry = new ResourceRegistry({
291
+ 'test-org': { agents: [] }
292
+ })
293
+
294
+ const result = registry.listResourcesForOrganization('test-org')
295
+
296
+ expect(result.agents).toEqual([])
297
+ expect(result.total).toBe(0)
298
+ })
299
+
300
+ it('includes organizationName in result', () => {
301
+ const registry = new ResourceRegistry({
302
+ 'test-org': { workflows: [] }
303
+ })
304
+
305
+ const result = registry.listResourcesForOrganization('test-org')
306
+
307
+ expect(result.organizationName).toBe('test-org')
308
+ })
309
+ })
310
+
311
+ describe('listAllResources', () => {
312
+ it('returns entire registry', () => {
313
+ const workflow = createMockWorkflow('workflow-1')
314
+ const agent = createMockAgent('agent-1')
315
+
316
+ const registryData = {
317
+ 'org-1': { workflows: [workflow] },
318
+ 'org-2': { agents: [agent] }
319
+ }
320
+
321
+ const registry = new ResourceRegistry(registryData)
322
+
323
+ const result = registry.listAllResources()
324
+
325
+ expect(result).toBe(registryData)
326
+ })
327
+
328
+ it('returns empty object for empty registry', () => {
329
+ const registry = new ResourceRegistry({})
330
+
331
+ const result = registry.listAllResources()
332
+
333
+ expect(result).toEqual({})
334
+ })
335
+
336
+ it('returns all organizations and resources', () => {
337
+ const registryData = {
338
+ 'org-1': {
339
+ workflows: [createMockWorkflow('w1'), createMockWorkflow('w2')],
340
+ agents: [createMockAgent('a1')]
341
+ },
342
+ 'org-2': {
343
+ workflows: [createMockWorkflow('w3')],
344
+ agents: [createMockAgent('a2'), createMockAgent('a3')]
345
+ },
346
+ 'org-3': {
347
+ workflows: [],
348
+ agents: []
349
+ }
350
+ }
351
+
352
+ const registry = new ResourceRegistry(registryData)
353
+
354
+ const result = registry.listAllResources()
355
+
356
+ expect(Object.keys(result)).toHaveLength(3)
357
+ expect(result['org-1'].workflows).toHaveLength(2)
358
+ expect(result['org-1'].agents).toHaveLength(1)
359
+ expect(result['org-2'].workflows).toHaveLength(1)
360
+ expect(result['org-2'].agents).toHaveLength(2)
361
+ })
362
+ })
363
+
364
+ describe('edge cases and organization isolation', () => {
365
+ it('maintains organization isolation (resources not shared)', () => {
366
+ const workflow1 = createMockWorkflow('workflow-1')
367
+ const workflow2 = createMockWorkflow('workflow-2')
368
+
369
+ const registry = new ResourceRegistry({
370
+ 'org-1': { workflows: [workflow1] },
371
+ 'org-2': { workflows: [workflow2] }
372
+ })
373
+
374
+ // org-1 should only see workflow-1
375
+ expect(registry.getResourceDefinition('org-1', 'workflow-1')).toBe(workflow1)
376
+ expect(registry.getResourceDefinition('org-1', 'workflow-2')).toBeNull()
377
+
378
+ // org-2 should only see workflow-2
379
+ expect(registry.getResourceDefinition('org-2', 'workflow-2')).toBe(workflow2)
380
+ expect(registry.getResourceDefinition('org-2', 'workflow-1')).toBeNull()
381
+ })
382
+
383
+ it('handles organization with undefined workflows', () => {
384
+ const registry = new ResourceRegistry({
385
+ 'test-org': { agents: [createMockAgent('agent-1')] }
386
+ })
387
+
388
+ const result = registry.listResourcesForOrganization('test-org')
389
+
390
+ expect(result.workflows).toEqual([])
391
+ expect(result.agents).toHaveLength(1)
392
+ })
393
+
394
+ it('handles organization with undefined agents', () => {
395
+ const registry = new ResourceRegistry({
396
+ 'test-org': { workflows: [createMockWorkflow('workflow-1')] }
397
+ })
398
+
399
+ const result = registry.listResourcesForOrganization('test-org')
400
+
401
+ expect(result.workflows).toHaveLength(1)
402
+ expect(result.agents).toEqual([])
403
+ })
404
+
405
+ it('handles large number of resources', () => {
406
+ const workflows = Array.from({ length: 100 }, (_, i) => createMockWorkflow(`workflow-${i}`))
407
+ const agents = Array.from({ length: 100 }, (_, i) => createMockAgent(`agent-${i}`))
408
+
409
+ const registry = new ResourceRegistry({
410
+ 'test-org': { workflows, agents }
411
+ })
412
+
413
+ const result = registry.listResourcesForOrganization('test-org')
414
+
415
+ expect(result.workflows).toHaveLength(100)
416
+ expect(result.agents).toHaveLength(100)
417
+ expect(result.total).toBe(200)
418
+ })
419
+
420
+ it('handles special characters in organization names', () => {
421
+ const workflow = createMockWorkflow('test-workflow')
422
+
423
+ const registry = new ResourceRegistry({
424
+ 'org-with-dashes': { workflows: [workflow] },
425
+ org_with_underscores: { workflows: [workflow] },
426
+ 'org.with.dots': { workflows: [workflow] }
427
+ })
428
+
429
+ expect(registry.getResourceDefinition('org-with-dashes', 'test-workflow')).toBe(workflow)
430
+ expect(registry.getResourceDefinition('org_with_underscores', 'test-workflow')).toBe(workflow)
431
+ expect(registry.getResourceDefinition('org.with.dots', 'test-workflow')).toBe(workflow)
432
+ })
433
+ })
434
+
435
+ describe('duplicate resourceId validation', () => {
436
+ it('throws error on duplicate workflow resourceIds', () => {
437
+ expect(() => {
438
+ new ResourceRegistry({
439
+ 'test-org': {
440
+ workflows: [createMockWorkflow('duplicate-id'), createMockWorkflow('duplicate-id')]
441
+ }
442
+ })
443
+ }).toThrow('Duplicate resourceId "duplicate-id" in organization "test-org"')
444
+ })
445
+
446
+ it('throws error on duplicate agent resourceIds', () => {
447
+ expect(() => {
448
+ new ResourceRegistry({
449
+ 'test-org': {
450
+ agents: [createMockAgent('duplicate-id'), createMockAgent('duplicate-id')]
451
+ }
452
+ })
453
+ }).toThrow('Duplicate resourceId "duplicate-id" in organization "test-org"')
454
+ })
455
+
456
+ it('throws error on workflow/agent resourceId collision', () => {
457
+ expect(() => {
458
+ new ResourceRegistry({
459
+ 'test-org': {
460
+ workflows: [createMockWorkflow('collision-id')],
461
+ agents: [createMockAgent('collision-id')]
462
+ }
463
+ })
464
+ }).toThrow('Duplicate resourceId "collision-id" in organization "test-org"')
465
+ })
466
+
467
+ it('allows same resourceId across different organizations', () => {
468
+ expect(() => {
469
+ new ResourceRegistry({
470
+ 'org-1': {
471
+ workflows: [createMockWorkflow('same-id')]
472
+ },
473
+ 'org-2': {
474
+ workflows: [createMockWorkflow('same-id')]
475
+ }
476
+ })
477
+ }).not.toThrow()
478
+ })
479
+
480
+ it('throws error on multiple duplicate resourceIds', () => {
481
+ expect(() => {
482
+ new ResourceRegistry({
483
+ 'test-org': {
484
+ workflows: [
485
+ createMockWorkflow('id-1'),
486
+ createMockWorkflow('id-2'),
487
+ createMockWorkflow('id-1') // Duplicate
488
+ ]
489
+ }
490
+ })
491
+ }).toThrow('Duplicate resourceId "id-1" in organization "test-org"')
492
+ })
493
+
494
+ it('validates across multiple organizations independently', () => {
495
+ // org-1 has duplicate, org-2 is valid - should fail on org-1
496
+ expect(() => {
497
+ new ResourceRegistry({
498
+ 'org-1': {
499
+ workflows: [createMockWorkflow('duplicate'), createMockWorkflow('duplicate')]
500
+ },
501
+ 'org-2': {
502
+ workflows: [createMockWorkflow('valid-id')]
503
+ }
504
+ })
505
+ }).toThrow('Duplicate resourceId "duplicate" in organization "org-1"')
506
+ })
507
+
508
+ it('allows empty workflows and agents arrays', () => {
509
+ expect(() => {
510
+ new ResourceRegistry({
511
+ 'test-org': {
512
+ workflows: [],
513
+ agents: []
514
+ }
515
+ })
516
+ }).not.toThrow()
517
+ })
518
+
519
+ it('allows undefined workflows and agents', () => {
520
+ expect(() => {
521
+ new ResourceRegistry({
522
+ 'test-org': {}
523
+ })
524
+ }).not.toThrow()
525
+ })
526
+
527
+ it('validates organization with only workflows', () => {
528
+ expect(() => {
529
+ new ResourceRegistry({
530
+ 'test-org': {
531
+ workflows: [createMockWorkflow('dup'), createMockWorkflow('dup')]
532
+ }
533
+ })
534
+ }).toThrow('Duplicate resourceId "dup"')
535
+ })
536
+
537
+ it('validates organization with only agents', () => {
538
+ expect(() => {
539
+ new ResourceRegistry({
540
+ 'test-org': {
541
+ agents: [createMockAgent('dup'), createMockAgent('dup')]
542
+ }
543
+ })
544
+ }).toThrow('Duplicate resourceId "dup"')
545
+ })
546
+
547
+ it('includes organization name in error message', () => {
548
+ expect(() => {
549
+ new ResourceRegistry({
550
+ 'my-special-org': {
551
+ workflows: [createMockWorkflow('id'), createMockWorkflow('id')]
552
+ }
553
+ })
554
+ }).toThrow('organization "my-special-org"')
555
+ })
556
+
557
+ it('includes resourceId in error message', () => {
558
+ expect(() => {
559
+ new ResourceRegistry({
560
+ 'test-org': {
561
+ workflows: [createMockWorkflow('my-workflow-id'), createMockWorkflow('my-workflow-id')]
562
+ }
563
+ })
564
+ }).toThrow('resourceId "my-workflow-id"')
565
+ })
566
+ })
567
+
568
+ describe('model config validation', () => {
569
+ it('validates agent model configs on construction', () => {
570
+ const validAgent = createMockAgent('valid-agent')
571
+ validAgent.modelConfig = {
572
+ provider: 'openai',
573
+ model: 'gpt-5',
574
+ apiKey: 'test-key',
575
+ temperature: 1,
576
+ maxOutputTokens: 8000
577
+ }
578
+
579
+ expect(() => {
580
+ new ResourceRegistry({
581
+ 'test-org': { agents: [validAgent] }
582
+ })
583
+ }).not.toThrow()
584
+ })
585
+
586
+ it('throws error for invalid agent temperature', () => {
587
+ const invalidAgent = createMockAgent('invalid-agent')
588
+ invalidAgent.modelConfig = {
589
+ provider: 'openai',
590
+ model: 'gpt-5',
591
+ apiKey: 'test-key',
592
+ temperature: 0.7, // Invalid - gpt-5 requires temperature=1
593
+ maxOutputTokens: 8000
594
+ }
595
+
596
+ expect(() => {
597
+ new ResourceRegistry({
598
+ 'test-org': { agents: [invalidAgent] }
599
+ })
600
+ }).toThrow('Invalid model config in test-org/invalid-agent')
601
+ expect(() => {
602
+ new ResourceRegistry({
603
+ 'test-org': { agents: [invalidAgent] }
604
+ })
605
+ }).toThrow('expected 1 (field: temperature)')
606
+ })
607
+
608
+ it('throws error for invalid agent maxOutputTokens', () => {
609
+ const invalidAgent = createMockAgent('invalid-agent')
610
+ invalidAgent.modelConfig = {
611
+ provider: 'openai',
612
+ model: 'gpt-5',
613
+ apiKey: 'test-key',
614
+ temperature: 1,
615
+ maxOutputTokens: 2000 // Invalid - below minimum of 4000
616
+ }
617
+
618
+ expect(() => {
619
+ new ResourceRegistry({
620
+ 'test-org': { agents: [invalidAgent] }
621
+ })
622
+ }).toThrow('Invalid model config in test-org/invalid-agent')
623
+ expect(() => {
624
+ new ResourceRegistry({
625
+ 'test-org': { agents: [invalidAgent] }
626
+ })
627
+ }).toThrow('expected number to be >=4000 (field: maxOutputTokens)')
628
+ })
629
+
630
+ it('validates workflow model configs if present', () => {
631
+ const workflowWithModel = createMockWorkflow('workflow-with-model') as unknown as Record<string, unknown>
632
+ ;(workflowWithModel as Record<string, unknown>).modelConfig = {
633
+ provider: 'openai',
634
+ model: 'gpt-5',
635
+ apiKey: 'test-key',
636
+ temperature: 0.5, // Invalid
637
+ maxOutputTokens: 8000
638
+ }
639
+
640
+ expect(() => {
641
+ new ResourceRegistry({
642
+ 'test-org': { workflows: [workflowWithModel] }
643
+ })
644
+ }).toThrow('Invalid model config in test-org/workflow-with-model')
645
+ })
646
+
647
+ it('allows workflows without model configs', () => {
648
+ const workflowWithoutModel = createMockWorkflow('workflow-no-model')
649
+
650
+ expect(() => {
651
+ new ResourceRegistry({
652
+ 'test-org': { workflows: [workflowWithoutModel] }
653
+ })
654
+ }).not.toThrow()
655
+ })
656
+
657
+ it('validates multiple agents in same organization', () => {
658
+ const validAgent1 = createMockAgent('agent-1')
659
+ validAgent1.modelConfig = {
660
+ provider: 'openai',
661
+ model: 'gpt-5',
662
+ apiKey: 'test-key',
663
+ temperature: 1,
664
+ maxOutputTokens: 8000
665
+ }
666
+
667
+ const invalidAgent2 = createMockAgent('agent-2')
668
+ invalidAgent2.modelConfig = {
669
+ provider: 'openai',
670
+ model: 'gpt-5-mini',
671
+ apiKey: 'test-key',
672
+ temperature: 0.7, // Invalid
673
+ maxOutputTokens: 8000
674
+ }
675
+
676
+ expect(() => {
677
+ new ResourceRegistry({
678
+ 'test-org': { agents: [validAgent1, invalidAgent2] }
679
+ })
680
+ }).toThrow('Invalid model config in test-org/agent-2')
681
+ })
682
+
683
+ it('validates across multiple organizations', () => {
684
+ const invalidAgent = createMockAgent('invalid-agent')
685
+ invalidAgent.modelConfig = {
686
+ provider: 'openai',
687
+ model: 'gpt-5',
688
+ apiKey: 'test-key',
689
+ temperature: 0.7,
690
+ maxOutputTokens: 8000
691
+ }
692
+
693
+ expect(() => {
694
+ new ResourceRegistry({
695
+ 'org-1': { agents: [createMockAgent('valid-agent')] },
696
+ 'org-2': { agents: [invalidAgent] }
697
+ })
698
+ }).toThrow('Invalid model config in org-2/invalid-agent')
699
+ })
700
+
701
+ it('includes field name in error message', () => {
702
+ const invalidAgent = createMockAgent('invalid-agent')
703
+ invalidAgent.modelConfig = {
704
+ provider: 'openai',
705
+ model: 'gpt-5',
706
+ apiKey: 'test-key',
707
+ temperature: 0.7,
708
+ maxOutputTokens: 8000
709
+ }
710
+
711
+ expect(() => {
712
+ new ResourceRegistry({
713
+ 'test-org': { agents: [invalidAgent] }
714
+ })
715
+ }).toThrow('field: temperature')
716
+ })
717
+
718
+ it('allows mock models with any temperature', () => {
719
+ const mockAgent = createMockAgent('mock-agent')
720
+ mockAgent.modelConfig = {
721
+ provider: 'mock',
722
+ model: 'mock',
723
+ apiKey: 'test-key',
724
+ temperature: 0.5, // Any temperature OK for mock
725
+ maxOutputTokens: 1000
726
+ }
727
+
728
+ expect(() => {
729
+ new ResourceRegistry({
730
+ 'test-org': { agents: [mockAgent] }
731
+ })
732
+ }).not.toThrow()
733
+ })
734
+ })
735
+
736
+ describe('environment filtering', () => {
737
+ it('shows all resources in development environment', () => {
738
+ // Mock development environment
739
+ const originalEnv = process.env.NODE_ENV
740
+ process.env.NODE_ENV = 'development'
741
+
742
+ const devWorkflow = createMockWorkflow('dev-workflow', 'dev')
743
+ const prodWorkflow = createMockWorkflow('prod-workflow', 'prod')
744
+ const devAgent = createMockAgent('dev-agent', 'dev')
745
+ const prodAgent = createMockAgent('prod-agent', 'prod')
746
+
747
+ const registry = new ResourceRegistry({
748
+ 'test-org': {
749
+ workflows: [devWorkflow, prodWorkflow],
750
+ agents: [devAgent, prodAgent]
751
+ }
752
+ })
753
+
754
+ const result = registry.listResourcesForOrganization('test-org')
755
+
756
+ expect(result.total).toBe(4) // All resources visible
757
+ expect(result.workflows).toHaveLength(2)
758
+ expect(result.agents).toHaveLength(2)
759
+ expect(result.workflows.some((w) => w.status === 'dev')).toBe(true)
760
+ expect(result.workflows.some((w) => w.status === 'prod')).toBe(true)
761
+
762
+ // Restore environment
763
+ process.env.NODE_ENV = originalEnv
764
+ })
765
+
766
+ it('shows only prod resources when explicit environment filter is passed', () => {
767
+ const devWorkflow = createMockWorkflow('dev-workflow', 'dev')
768
+ const prodWorkflow = createMockWorkflow('prod-workflow', 'prod')
769
+ const devAgent = createMockAgent('dev-agent', 'dev')
770
+ const prodAgent = createMockAgent('prod-agent', 'prod')
771
+
772
+ const registry = new ResourceRegistry({
773
+ 'test-org': {
774
+ workflows: [devWorkflow, prodWorkflow],
775
+ agents: [devAgent, prodAgent]
776
+ }
777
+ })
778
+
779
+ const result = registry.listResourcesForOrganization('test-org', 'prod')
780
+
781
+ expect(result.total).toBe(2) // Only prod resources
782
+ expect(result.workflows).toHaveLength(1)
783
+ expect(result.agents).toHaveLength(1)
784
+ expect(result.workflows[0].status).toBe('prod')
785
+ expect(result.agents[0].status).toBe('prod')
786
+ expect(result.workflows.some((w) => w.status === 'dev')).toBe(false)
787
+ expect(result.agents.some((a) => a.status === 'dev')).toBe(false)
788
+ })
789
+
790
+ it('returns all resources when no environment filter is passed', () => {
791
+ const devWorkflow = createMockWorkflow('dev-workflow', 'dev')
792
+ const prodWorkflow = createMockWorkflow('prod-workflow', 'prod')
793
+
794
+ const registry = new ResourceRegistry({
795
+ 'test-org': {
796
+ workflows: [devWorkflow, prodWorkflow]
797
+ }
798
+ })
799
+
800
+ // No environment filter -- returns all resources regardless of status
801
+ const result = registry.listResourcesForOrganization('test-org')
802
+
803
+ expect(result.total).toBe(2)
804
+ expect(result.workflows).toHaveLength(2)
805
+ })
806
+
807
+ it('returns empty list when filtering for prod but only dev resources exist', () => {
808
+ const devWorkflow = createMockWorkflow('dev-workflow', 'dev')
809
+ const devAgent = createMockAgent('dev-agent', 'dev')
810
+
811
+ const registry = new ResourceRegistry({
812
+ 'test-org': {
813
+ workflows: [devWorkflow],
814
+ agents: [devAgent]
815
+ }
816
+ })
817
+
818
+ const result = registry.listResourcesForOrganization('test-org', 'prod')
819
+
820
+ expect(result.total).toBe(0)
821
+ expect(result.workflows).toHaveLength(0)
822
+ expect(result.agents).toHaveLength(0)
823
+ })
824
+
825
+ it('handles mixed status resources correctly with explicit prod filter', () => {
826
+ const registry = new ResourceRegistry({
827
+ 'test-org': {
828
+ workflows: [
829
+ createMockWorkflow('w1', 'dev'),
830
+ createMockWorkflow('w2', 'prod'),
831
+ createMockWorkflow('w3', 'dev'),
832
+ createMockWorkflow('w4', 'prod')
833
+ ],
834
+ agents: [createMockAgent('a1', 'dev'), createMockAgent('a2', 'prod'), createMockAgent('a3', 'dev')]
835
+ }
836
+ })
837
+
838
+ const result = registry.listResourcesForOrganization('test-org', 'prod')
839
+
840
+ expect(result.total).toBe(3) // 2 prod workflows + 1 prod agent
841
+ expect(result.workflows).toHaveLength(2)
842
+ expect(result.agents).toHaveLength(1)
843
+ })
844
+
845
+ it('includes environment in response when explicit filter is passed', () => {
846
+ const registry = new ResourceRegistry({
847
+ 'test-org': {
848
+ workflows: [createMockWorkflow('w1', 'prod')]
849
+ }
850
+ })
851
+
852
+ const result = registry.listResourcesForOrganization('test-org', 'prod')
853
+
854
+ expect(result.environment).toBe('prod')
855
+ })
856
+
857
+ it('does not include environment in response for development', () => {
858
+ const originalEnv = process.env.NODE_ENV
859
+ process.env.NODE_ENV = 'development'
860
+
861
+ const registry = new ResourceRegistry({
862
+ 'test-org': {
863
+ workflows: [createMockWorkflow('w1', 'dev')]
864
+ }
865
+ })
866
+
867
+ const result = registry.listResourcesForOrganization('test-org')
868
+
869
+ expect(result.environment).toBeUndefined()
870
+
871
+ process.env.NODE_ENV = originalEnv
872
+ })
873
+ })
874
+
875
+ describe('Resource Manifest Accessors', () => {
876
+ // Helper to create mock triggers, integrations, etc.
877
+ const createMockTrigger = (resourceId: string): import('../types').TriggerDefinition => ({
878
+ resourceId,
879
+ type: 'trigger',
880
+ triggerType: 'manual',
881
+ name: `Trigger ${resourceId}`,
882
+ description: `Test trigger ${resourceId}`,
883
+ version: '1.0.0',
884
+ status: 'dev'
885
+ // NOTE: No triggers field - will be added in tests as needed to reference valid resources
886
+ })
887
+
888
+ const createMockIntegration = (resourceId: string): import('../types').IntegrationDefinition => ({
889
+ resourceId,
890
+ type: 'integration',
891
+ name: `Integration ${resourceId}`,
892
+ description: `Test integration ${resourceId}`,
893
+ version: '1.0.0',
894
+ status: 'dev',
895
+ provider: 'webhook',
896
+ credentialName: 'test-cred'
897
+ })
898
+
899
+ const createMockExternalResource = (resourceId: string): import('../types').ExternalResourceDefinition => ({
900
+ resourceId,
901
+ type: 'external',
902
+ name: `External ${resourceId}`,
903
+ description: `Test external ${resourceId}`,
904
+ version: '1.0.0',
905
+ status: 'dev',
906
+ platform: 'n8n'
907
+ // NOTE: No triggeredBy field
908
+ })
909
+
910
+ const createMockHumanCheckpoint = (resourceId: string): import('../types').HumanCheckpointDefinition => ({
911
+ resourceId,
912
+ type: 'human',
913
+ name: `Human ${resourceId}`,
914
+ description: `Test human checkpoint ${resourceId}`,
915
+ version: '1.0.0',
916
+ status: 'dev'
917
+ })
918
+
919
+ describe('getTrigger', () => {
920
+ it('finds trigger by resourceId', () => {
921
+ const agent = createMockAgent('basic-agent')
922
+ const trigger = createMockTrigger('test-trigger')
923
+ trigger.triggers = { agents: ['basic-agent'] }
924
+
925
+ const registry = new ResourceRegistry({
926
+ 'test-org': {
927
+ agents: [agent],
928
+ triggers: [trigger]
929
+ }
930
+ })
931
+
932
+ const result = registry.getTrigger('test-org', 'test-trigger')
933
+
934
+ expect(result).toBe(trigger)
935
+ expect(result?.resourceId).toBe('test-trigger')
936
+ expect(result?.type).toBe('trigger')
937
+ })
938
+
939
+ it('returns null for non-existent trigger', () => {
940
+ const agent = createMockAgent('basic-agent')
941
+ const trigger = createMockTrigger('trigger-1')
942
+ trigger.triggers = { agents: ['basic-agent'] }
943
+
944
+ const registry = new ResourceRegistry({
945
+ 'test-org': {
946
+ agents: [agent],
947
+ triggers: [trigger]
948
+ }
949
+ })
950
+
951
+ const result = registry.getTrigger('test-org', 'nonexistent-trigger')
952
+
953
+ expect(result).toBeNull()
954
+ })
955
+
956
+ it('returns null for non-existent organization', () => {
957
+ const registry = new ResourceRegistry({})
958
+
959
+ const result = registry.getTrigger('nonexistent-org', 'test-trigger')
960
+
961
+ expect(result).toBeNull()
962
+ })
963
+ })
964
+
965
+ describe('getIntegration', () => {
966
+ it('finds integration by resourceId', () => {
967
+ const integration = createMockIntegration('test-integration')
968
+ const registry = new ResourceRegistry({
969
+ 'test-org': { integrations: [integration] }
970
+ })
971
+
972
+ const result = registry.getIntegration('test-org', 'test-integration')
973
+
974
+ expect(result).toBe(integration)
975
+ expect(result?.resourceId).toBe('test-integration')
976
+ expect(result?.type).toBe('integration')
977
+ })
978
+
979
+ it('returns null for non-existent integration', () => {
980
+ const registry = new ResourceRegistry({
981
+ 'test-org': { integrations: [createMockIntegration('integration-1')] }
982
+ })
983
+
984
+ const result = registry.getIntegration('test-org', 'nonexistent-integration')
985
+
986
+ expect(result).toBeNull()
987
+ })
988
+
989
+ it('returns null for non-existent organization', () => {
990
+ const registry = new ResourceRegistry({})
991
+
992
+ const result = registry.getIntegration('nonexistent-org', 'test-integration')
993
+
994
+ expect(result).toBeNull()
995
+ })
996
+ })
997
+
998
+ describe('getExternalResource', () => {
999
+ it('finds external resource by resourceId', () => {
1000
+ const external = createMockExternalResource('test-external')
1001
+ const registry = new ResourceRegistry({
1002
+ 'test-org': { externalResources: [external] }
1003
+ })
1004
+
1005
+ const result = registry.getExternalResource('test-org', 'test-external')
1006
+
1007
+ expect(result).toBe(external)
1008
+ expect(result?.resourceId).toBe('test-external')
1009
+ expect(result?.platform).toBe('n8n')
1010
+ })
1011
+
1012
+ it('returns null for non-existent external resource', () => {
1013
+ const registry = new ResourceRegistry({
1014
+ 'test-org': { externalResources: [createMockExternalResource('external-1')] }
1015
+ })
1016
+
1017
+ const result = registry.getExternalResource('test-org', 'nonexistent-external')
1018
+
1019
+ expect(result).toBeNull()
1020
+ })
1021
+
1022
+ it('returns null for non-existent organization', () => {
1023
+ const registry = new ResourceRegistry({})
1024
+
1025
+ const result = registry.getExternalResource('nonexistent-org', 'test-external')
1026
+
1027
+ expect(result).toBeNull()
1028
+ })
1029
+ })
1030
+
1031
+ describe('getHumanCheckpoint', () => {
1032
+ it('finds human checkpoint by resourceId', () => {
1033
+ const humanCheckpoint = createMockHumanCheckpoint('test-human')
1034
+ const registry = new ResourceRegistry({
1035
+ 'test-org': { humanCheckpoints: [humanCheckpoint] }
1036
+ })
1037
+
1038
+ const result = registry.getHumanCheckpoint('test-org', 'test-human')
1039
+
1040
+ expect(result).toBe(humanCheckpoint)
1041
+ expect(result?.resourceId).toBe('test-human')
1042
+ expect(result?.type).toBe('human')
1043
+ })
1044
+
1045
+ it('returns null for non-existent human checkpoint', () => {
1046
+ const registry = new ResourceRegistry({
1047
+ 'test-org': { humanCheckpoints: [createMockHumanCheckpoint('human-1')] }
1048
+ })
1049
+
1050
+ const result = registry.getHumanCheckpoint('test-org', 'nonexistent-human')
1051
+
1052
+ expect(result).toBeNull()
1053
+ })
1054
+
1055
+ it('returns null for non-existent organization', () => {
1056
+ const registry = new ResourceRegistry({})
1057
+
1058
+ const result = registry.getHumanCheckpoint('nonexistent-org', 'test-human')
1059
+
1060
+ expect(result).toBeNull()
1061
+ })
1062
+ })
1063
+
1064
+ describe('getTriggers, getIntegrations, getExternalResources, getHumanCheckpoints', () => {
1065
+ it('returns all triggers for organization', () => {
1066
+ const agent = createMockAgent('basic-agent')
1067
+ const trigger1 = createMockTrigger('trigger-1')
1068
+ trigger1.triggers = { agents: ['basic-agent'] }
1069
+ const trigger2 = createMockTrigger('trigger-2')
1070
+ trigger2.triggers = { agents: ['basic-agent'] }
1071
+
1072
+ const registry = new ResourceRegistry({
1073
+ 'test-org': {
1074
+ agents: [agent],
1075
+ triggers: [trigger1, trigger2]
1076
+ }
1077
+ })
1078
+
1079
+ const result = registry.getTriggers('test-org')
1080
+
1081
+ expect(result).toHaveLength(2)
1082
+ expect(result[0]).toBe(trigger1)
1083
+ expect(result[1]).toBe(trigger2)
1084
+ })
1085
+
1086
+ it('returns all integrations for organization', () => {
1087
+ const integration1 = createMockIntegration('integration-1')
1088
+ const integration2 = createMockIntegration('integration-2')
1089
+ const registry = new ResourceRegistry({
1090
+ 'test-org': { integrations: [integration1, integration2] }
1091
+ })
1092
+
1093
+ const result = registry.getIntegrations('test-org')
1094
+
1095
+ expect(result).toHaveLength(2)
1096
+ expect(result[0]).toBe(integration1)
1097
+ expect(result[1]).toBe(integration2)
1098
+ })
1099
+
1100
+ it('returns all external resources for organization', () => {
1101
+ const external1 = createMockExternalResource('external-1')
1102
+ const external2 = createMockExternalResource('external-2')
1103
+ const registry = new ResourceRegistry({
1104
+ 'test-org': { externalResources: [external1, external2] }
1105
+ })
1106
+
1107
+ const result = registry.getExternalResources('test-org')
1108
+
1109
+ expect(result).toHaveLength(2)
1110
+ expect(result[0]).toBe(external1)
1111
+ expect(result[1]).toBe(external2)
1112
+ })
1113
+
1114
+ it('returns all human checkpoints for organization', () => {
1115
+ const human1 = createMockHumanCheckpoint('human-1')
1116
+ const human2 = createMockHumanCheckpoint('human-2')
1117
+ const registry = new ResourceRegistry({
1118
+ 'test-org': { humanCheckpoints: [human1, human2] }
1119
+ })
1120
+
1121
+ const result = registry.getHumanCheckpoints('test-org')
1122
+
1123
+ expect(result).toHaveLength(2)
1124
+ expect(result[0]).toBe(human1)
1125
+ expect(result[1]).toBe(human2)
1126
+ })
1127
+
1128
+ it('returns empty array for organization with no manifest data', () => {
1129
+ const registry = new ResourceRegistry({
1130
+ 'test-org': { workflows: [createMockWorkflow('workflow-1')] }
1131
+ })
1132
+
1133
+ expect(registry.getTriggers('test-org')).toEqual([])
1134
+ expect(registry.getIntegrations('test-org')).toEqual([])
1135
+ expect(registry.getExternalResources('test-org')).toEqual([])
1136
+ expect(registry.getHumanCheckpoints('test-org')).toEqual([])
1137
+ })
1138
+ })
1139
+ })
1140
+
1141
+ describe('getCommandViewData', () => {
1142
+ it('returns correct edge types (triggers, uses, approval only)', () => {
1143
+ const trigger = {
1144
+ resourceId: 'trigger-1',
1145
+ type: 'trigger' as const,
1146
+ triggerType: 'manual' as const,
1147
+ name: 'Manual Trigger',
1148
+ description: 'Test trigger',
1149
+ version: '1.0.0',
1150
+ status: 'dev' as const,
1151
+ triggers: { workflows: ['workflow-1'] }
1152
+ }
1153
+
1154
+ const integration = {
1155
+ resourceId: 'integration-1',
1156
+ type: 'integration' as const,
1157
+ name: 'Test Integration',
1158
+ description: 'Test integration',
1159
+ version: '1.0.0',
1160
+ status: 'dev' as const,
1161
+ provider: 'webhook' as const,
1162
+ credentialName: 'test-cred'
1163
+ }
1164
+
1165
+ const humanCheckpoint = {
1166
+ resourceId: 'human-1',
1167
+ type: 'human' as const,
1168
+ name: 'Approval Point',
1169
+ description: 'Test human checkpoint',
1170
+ version: '1.0.0',
1171
+ status: 'dev' as const,
1172
+ requestedBy: { workflows: ['workflow-1'] },
1173
+ routesTo: { agents: ['agent-1'] }
1174
+ }
1175
+
1176
+ const relationships = {
1177
+ 'workflow-1': {
1178
+ uses: { integrations: ['integration-1'] }
1179
+ }
1180
+ }
1181
+
1182
+ const registry = new ResourceRegistry({
1183
+ 'test-org': {
1184
+ workflows: [createMockWorkflow('workflow-1')],
1185
+ agents: [createMockAgent('agent-1')],
1186
+ triggers: [trigger],
1187
+ integrations: [integration],
1188
+ humanCheckpoints: [humanCheckpoint],
1189
+ relationships
1190
+ }
1191
+ })
1192
+
1193
+ const result = registry.getCommandViewData('test-org')
1194
+
1195
+ // Verify all edge types are valid
1196
+ const edgeTypes = new Set(result.edges.map((e) => e.relationship))
1197
+ expect(edgeTypes.size).toBeGreaterThan(0)
1198
+ for (const edgeType of edgeTypes) {
1199
+ expect(['triggers', 'uses', 'approval']).toContain(edgeType)
1200
+ }
1201
+ })
1202
+
1203
+ it('does NOT return edges with type invokes', () => {
1204
+ const trigger = {
1205
+ resourceId: 'trigger-1',
1206
+ type: 'trigger' as const,
1207
+ triggerType: 'manual' as const,
1208
+ name: 'Manual Trigger',
1209
+ description: 'Test trigger',
1210
+ version: '1.0.0',
1211
+ status: 'dev' as const,
1212
+ triggers: { workflows: ['workflow-1'] }
1213
+ }
1214
+
1215
+ const registry = new ResourceRegistry({
1216
+ 'test-org': {
1217
+ workflows: [createMockWorkflow('workflow-1')],
1218
+ triggers: [trigger]
1219
+ }
1220
+ })
1221
+
1222
+ const result = registry.getCommandViewData('test-org')
1223
+
1224
+ // Verify no 'invokes' edge type exists
1225
+ const hasInvokes = result.edges.some((e) => e.relationship === 'invokes')
1226
+ expect(hasInvokes).toBe(false)
1227
+ })
1228
+
1229
+ it('includes all resource types in response', () => {
1230
+ const registry = new ResourceRegistry({
1231
+ 'test-org': {
1232
+ workflows: [createMockWorkflow('workflow-1')],
1233
+ agents: [createMockAgent('agent-1')],
1234
+ triggers: [
1235
+ {
1236
+ resourceId: 'trigger-1',
1237
+ type: 'trigger' as const,
1238
+ triggerType: 'manual' as const,
1239
+ name: 'Manual Trigger',
1240
+ description: 'Test trigger',
1241
+ version: '1.0.0',
1242
+ status: 'dev' as const,
1243
+ triggers: { workflows: ['workflow-1'] }
1244
+ }
1245
+ ],
1246
+ integrations: [
1247
+ {
1248
+ resourceId: 'integration-1',
1249
+ type: 'integration' as const,
1250
+ name: 'Test Integration',
1251
+ description: 'Test integration',
1252
+ version: '1.0.0',
1253
+ status: 'dev' as const,
1254
+ provider: 'webhook' as const,
1255
+ credentialName: 'test-cred'
1256
+ }
1257
+ ],
1258
+ externalResources: [
1259
+ {
1260
+ resourceId: 'external-1',
1261
+ type: 'external' as const,
1262
+ name: 'External Resource',
1263
+ description: 'Test external',
1264
+ version: '1.0.0',
1265
+ status: 'dev' as const,
1266
+ platform: 'n8n' as const
1267
+ }
1268
+ ],
1269
+ humanCheckpoints: [
1270
+ {
1271
+ resourceId: 'human-1',
1272
+ type: 'human' as const,
1273
+ name: 'Approval Point',
1274
+ description: 'Test human checkpoint',
1275
+ version: '1.0.0',
1276
+ status: 'dev' as const
1277
+ }
1278
+ ]
1279
+ }
1280
+ })
1281
+
1282
+ const result = registry.getCommandViewData('test-org')
1283
+
1284
+ expect(result.workflows).toHaveLength(1)
1285
+ expect(result.agents).toHaveLength(1)
1286
+ expect(result.triggers).toHaveLength(1)
1287
+ expect(result.integrations).toHaveLength(1)
1288
+ expect(result.externalResources).toHaveLength(1)
1289
+ expect(result.humanCheckpoints).toHaveLength(1)
1290
+ expect(result.edges).toBeDefined()
1291
+ })
1292
+
1293
+ it('returns empty data for non-existent organization', () => {
1294
+ const registry = new ResourceRegistry({})
1295
+
1296
+ const result = registry.getCommandViewData('nonexistent-org')
1297
+
1298
+ expect(result).toEqual({
1299
+ workflows: [],
1300
+ agents: [],
1301
+ triggers: [],
1302
+ integrations: [],
1303
+ externalResources: [],
1304
+ humanCheckpoints: [],
1305
+ edges: []
1306
+ })
1307
+ })
1308
+
1309
+ it('returns edges referencing valid resourceIds', () => {
1310
+ const trigger = {
1311
+ resourceId: 'trigger-1',
1312
+ type: 'trigger' as const,
1313
+ triggerType: 'manual' as const,
1314
+ name: 'Manual Trigger',
1315
+ description: 'Test trigger',
1316
+ version: '1.0.0',
1317
+ status: 'dev' as const,
1318
+ triggers: { workflows: ['workflow-1'], agents: ['agent-1'] }
1319
+ }
1320
+
1321
+ const integration = {
1322
+ resourceId: 'integration-1',
1323
+ type: 'integration' as const,
1324
+ name: 'Test Integration',
1325
+ description: 'Test integration',
1326
+ version: '1.0.0',
1327
+ status: 'dev' as const,
1328
+ provider: 'webhook' as const,
1329
+ credentialName: 'test-cred'
1330
+ }
1331
+
1332
+ const relationships = {
1333
+ 'workflow-1': {
1334
+ uses: { integrations: ['integration-1'] }
1335
+ }
1336
+ }
1337
+
1338
+ const registry = new ResourceRegistry({
1339
+ 'test-org': {
1340
+ workflows: [createMockWorkflow('workflow-1')],
1341
+ agents: [createMockAgent('agent-1')],
1342
+ triggers: [trigger],
1343
+ integrations: [integration],
1344
+ relationships
1345
+ }
1346
+ })
1347
+
1348
+ const result = registry.getCommandViewData('test-org')
1349
+
1350
+ // Collect all valid resourceIds
1351
+ const validIds = new Set(['workflow-1', 'agent-1', 'trigger-1', 'integration-1'])
1352
+
1353
+ // Verify all edge sources and targets reference valid resourceIds
1354
+ for (const edge of result.edges) {
1355
+ expect(validIds.has(edge.source) || validIds.has(edge.target)).toBe(true)
1356
+ }
1357
+ })
1358
+ })
1359
+
1360
+ describe('New Field Validation on Construction', () => {
1361
+ it('validates triggers use resourceId (not id)', () => {
1362
+ const agent = createMockAgent('basic-agent')
1363
+ const trigger = {
1364
+ resourceId: 'trigger-1',
1365
+ type: 'trigger' as const,
1366
+ triggerType: 'manual' as const,
1367
+ name: 'Manual Trigger',
1368
+ description: 'Test trigger',
1369
+ version: '1.0.0',
1370
+ status: 'dev' as const,
1371
+ triggers: { agents: ['basic-agent'] }
1372
+ }
1373
+
1374
+ expect(() => {
1375
+ new ResourceRegistry({
1376
+ 'test-org': {
1377
+ agents: [agent],
1378
+ triggers: [trigger]
1379
+ }
1380
+ })
1381
+ }).not.toThrow()
1382
+
1383
+ // Verify trigger is stored with resourceId
1384
+ const registry = new ResourceRegistry({
1385
+ 'test-org': {
1386
+ agents: [agent],
1387
+ triggers: [trigger]
1388
+ }
1389
+ })
1390
+ const result = registry.getTrigger('test-org', 'trigger-1')
1391
+ expect(result?.resourceId).toBe('trigger-1')
1392
+ })
1393
+
1394
+ it('validates triggers have triggerType field', () => {
1395
+ const workflow = createMockWorkflow('test-workflow')
1396
+ const trigger = {
1397
+ resourceId: 'trigger-1',
1398
+ type: 'trigger' as const,
1399
+ triggerType: 'webhook' as const,
1400
+ name: 'Webhook Trigger',
1401
+ description: 'Test webhook trigger',
1402
+ version: '1.0.0',
1403
+ status: 'dev' as const,
1404
+ webhookPath: '/webhooks/test',
1405
+ triggers: { workflows: ['test-workflow'] }
1406
+ }
1407
+
1408
+ expect(() => {
1409
+ new ResourceRegistry({
1410
+ 'test-org': {
1411
+ workflows: [workflow],
1412
+ triggers: [trigger]
1413
+ }
1414
+ })
1415
+ }).not.toThrow()
1416
+
1417
+ const registry = new ResourceRegistry({
1418
+ 'test-org': {
1419
+ workflows: [workflow],
1420
+ triggers: [trigger]
1421
+ }
1422
+ })
1423
+ const result = registry.getTrigger('test-org', 'trigger-1')
1424
+ expect(result?.triggerType).toBe('webhook')
1425
+ })
1426
+
1427
+ it('validates integrations have status field', () => {
1428
+ const integration = {
1429
+ resourceId: 'integration-1',
1430
+ type: 'integration' as const,
1431
+ name: 'Test Integration',
1432
+ description: 'Test integration',
1433
+ version: '1.0.0',
1434
+ status: 'prod' as const,
1435
+ provider: 'webhook' as const,
1436
+ credentialName: 'test-cred'
1437
+ }
1438
+
1439
+ expect(() => {
1440
+ new ResourceRegistry({
1441
+ 'test-org': { integrations: [integration] }
1442
+ })
1443
+ }).not.toThrow()
1444
+
1445
+ const registry = new ResourceRegistry({
1446
+ 'test-org': { integrations: [integration] }
1447
+ })
1448
+ const result = registry.getIntegration('test-org', 'integration-1')
1449
+ expect(result?.status).toBe('prod')
1450
+ })
1451
+
1452
+ it('validates integrations have version field', () => {
1453
+ const integration = {
1454
+ resourceId: 'integration-1',
1455
+ type: 'integration' as const,
1456
+ name: 'Test Integration',
1457
+ description: 'Test integration',
1458
+ version: '2.1.0',
1459
+ status: 'dev' as const,
1460
+ provider: 'webhook' as const,
1461
+ credentialName: 'test-cred'
1462
+ }
1463
+
1464
+ expect(() => {
1465
+ new ResourceRegistry({
1466
+ 'test-org': { integrations: [integration] }
1467
+ })
1468
+ }).not.toThrow()
1469
+
1470
+ const registry = new ResourceRegistry({
1471
+ 'test-org': { integrations: [integration] }
1472
+ })
1473
+ const result = registry.getIntegration('test-org', 'integration-1')
1474
+ expect(result?.version).toBe('2.1.0')
1475
+ })
1476
+
1477
+ it('validates human checkpoints have description (required)', () => {
1478
+ const humanCheckpoint = {
1479
+ resourceId: 'human-1',
1480
+ type: 'human' as const,
1481
+ name: 'Approval Point',
1482
+ description: 'Human decision point for high-value orders',
1483
+ version: '1.0.0',
1484
+ status: 'dev' as const
1485
+ }
1486
+
1487
+ expect(() => {
1488
+ new ResourceRegistry({
1489
+ 'test-org': { humanCheckpoints: [humanCheckpoint] }
1490
+ })
1491
+ }).not.toThrow()
1492
+
1493
+ const registry = new ResourceRegistry({
1494
+ 'test-org': { humanCheckpoints: [humanCheckpoint] }
1495
+ })
1496
+ const result = registry.getHumanCheckpoint('test-org', 'human-1')
1497
+ expect(result?.description).toBe('Human decision point for high-value orders')
1498
+ })
1499
+ })
1500
+
1501
+ describe('Runtime Registration', () => {
1502
+ const createMockRemoteConfig = (overrides: Partial<RemoteOrgConfig> = {}): RemoteOrgConfig => ({
1503
+ storagePath: 'test-org/deploy-001/bundle.js',
1504
+ deploymentId: 'deploy-001',
1505
+ ...overrides
1506
+ })
1507
+
1508
+ describe('registerOrganization -- merge into existing org', () => {
1509
+ it('registers remote workflows alongside existing static workflows', () => {
1510
+ const staticWorkflow = createMockWorkflow('static-wf')
1511
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1512
+
1513
+ const registry = new ResourceRegistry({
1514
+ 'test-org': { workflows: [staticWorkflow] }
1515
+ })
1516
+
1517
+ registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1518
+
1519
+ const result = registry.listResourcesForOrganization('test-org')
1520
+ expect(result.workflows).toHaveLength(2)
1521
+ expect(result.workflows.find((w) => w.resourceId === 'static-wf')).toBeDefined()
1522
+ expect(result.workflows.find((w) => w.resourceId === 'remote-wf')).toBeDefined()
1523
+ })
1524
+
1525
+ it('registers remote agents alongside existing static agents', () => {
1526
+ const staticAgent = createMockAgent('static-agent')
1527
+ const remoteAgent = createMockAgent('remote-agent')
1528
+
1529
+ const registry = new ResourceRegistry({
1530
+ 'test-org': { agents: [staticAgent] }
1531
+ })
1532
+
1533
+ registry.registerOrganization('test-org', { agents: [remoteAgent] }, createMockRemoteConfig())
1534
+
1535
+ const result = registry.listResourcesForOrganization('test-org')
1536
+ expect(result.agents).toHaveLength(2)
1537
+ expect(result.agents.find((a) => a.resourceId === 'static-agent')).toBeDefined()
1538
+ expect(result.agents.find((a) => a.resourceId === 'remote-agent')).toBeDefined()
1539
+ })
1540
+
1541
+ it('marks remote resources with origin "remote" and static resources with origin "local"', () => {
1542
+ const staticWorkflow = createMockWorkflow('static-wf')
1543
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1544
+
1545
+ const registry = new ResourceRegistry({
1546
+ 'test-org': { workflows: [staticWorkflow] }
1547
+ })
1548
+
1549
+ registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1550
+
1551
+ const result = registry.listResourcesForOrganization('test-org')
1552
+ expect(result.workflows.find((w) => w.resourceId === 'static-wf')?.origin).toBe('local')
1553
+ expect(result.workflows.find((w) => w.resourceId === 'remote-wf')?.origin).toBe('remote')
1554
+ })
1555
+
1556
+ it('getRemoteConfig returns config for remote resource and null for static resource', () => {
1557
+ const staticWorkflow = createMockWorkflow('static-wf')
1558
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1559
+ const remoteConfig = createMockRemoteConfig({ deploymentId: 'deploy-xyz' })
1560
+
1561
+ const registry = new ResourceRegistry({
1562
+ 'test-org': { workflows: [staticWorkflow] }
1563
+ })
1564
+
1565
+ registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, remoteConfig)
1566
+
1567
+ expect(registry.getRemoteConfig('test-org', 'remote-wf')).toMatchObject({
1568
+ storagePath: 'test-org/deploy-001/bundle.js',
1569
+ deploymentId: 'deploy-xyz'
1570
+ })
1571
+ expect(registry.getRemoteConfig('test-org', 'static-wf')).toBeNull()
1572
+ })
1573
+
1574
+ it('isRemote returns true for org with remote resources', () => {
1575
+ const staticWorkflow = createMockWorkflow('static-wf')
1576
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1577
+
1578
+ const registry = new ResourceRegistry({
1579
+ 'test-org': { workflows: [staticWorkflow] }
1580
+ })
1581
+
1582
+ expect(registry.isRemote('test-org')).toBe(false)
1583
+
1584
+ registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1585
+
1586
+ expect(registry.isRemote('test-org')).toBe(true)
1587
+ })
1588
+ })
1589
+
1590
+ describe('registerOrganization -- conflict detection', () => {
1591
+ it('throws when remote resourceId collides with an existing static resource', () => {
1592
+ const staticWorkflow = createMockWorkflow('shared-id')
1593
+ const remoteWorkflow = createMockWorkflow('shared-id')
1594
+
1595
+ const registry = new ResourceRegistry({
1596
+ 'test-org': { workflows: [staticWorkflow] }
1597
+ })
1598
+
1599
+ expect(() => {
1600
+ registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1601
+ }).toThrow("Resource 'shared-id' already exists in 'test-org' as an internal resource")
1602
+ })
1603
+
1604
+ it('throws when deployment contains duplicate resourceIds within itself', () => {
1605
+ const workflow1 = createMockWorkflow('dup-id')
1606
+ const workflow2 = createMockWorkflow('dup-id')
1607
+
1608
+ const registry = new ResourceRegistry({})
1609
+
1610
+ expect(() => {
1611
+ registry.registerOrganization('new-org', { workflows: [workflow1, workflow2] }, createMockRemoteConfig())
1612
+ }).toThrow("Duplicate resource ID 'dup-id' in deployment")
1613
+ })
1614
+
1615
+ it('throws a validation error when incoming relationships reference missing resources after merge', () => {
1616
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1617
+
1618
+ const registry = new ResourceRegistry({
1619
+ 'test-org': {
1620
+ workflows: [createMockWorkflow('static-wf')]
1621
+ }
1622
+ })
1623
+
1624
+ expect(() => {
1625
+ registry.registerOrganization(
1626
+ 'test-org',
1627
+ {
1628
+ workflows: [remoteWorkflow],
1629
+ relationships: {
1630
+ 'remote-wf': {
1631
+ triggers: { workflows: ['missing-target'] }
1632
+ }
1633
+ }
1634
+ },
1635
+ createMockRemoteConfig()
1636
+ )
1637
+ }).toThrow("[test-org] Resource 'remote-wf' triggers non-existent workflow: missing-target")
1638
+ })
1639
+ })
1640
+
1641
+ describe('registerOrganization -- new org (no static resources)', () => {
1642
+ it('registers an org that does not exist in the static registry', () => {
1643
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1644
+ const remoteAgent = createMockAgent('remote-agent')
1645
+
1646
+ const registry = new ResourceRegistry({})
1647
+
1648
+ registry.registerOrganization(
1649
+ 'new-org',
1650
+ {
1651
+ workflows: [remoteWorkflow],
1652
+ agents: [remoteAgent]
1653
+ },
1654
+ createMockRemoteConfig()
1655
+ )
1656
+
1657
+ const result = registry.listResourcesForOrganization('new-org')
1658
+ expect(result.workflows).toHaveLength(1)
1659
+ expect(result.workflows[0].resourceId).toBe('remote-wf')
1660
+ expect(result.agents).toHaveLength(1)
1661
+ expect(result.agents[0].resourceId).toBe('remote-agent')
1662
+ expect(result.total).toBe(2)
1663
+ })
1664
+
1665
+ it('getResourceDefinition finds the registered remote resource', () => {
1666
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1667
+
1668
+ const registry = new ResourceRegistry({})
1669
+
1670
+ registry.registerOrganization('new-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1671
+
1672
+ const definition = registry.getResourceDefinition('new-org', 'remote-wf')
1673
+ expect(definition).not.toBeNull()
1674
+ expect(definition?.config.resourceId).toBe('remote-wf')
1675
+ expect(definition?.config.type).toBe('workflow')
1676
+ })
1677
+ })
1678
+
1679
+ describe('unregisterOrganization -- cleanup', () => {
1680
+ it('remote resources no longer appear in listing after unregister', () => {
1681
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1682
+
1683
+ const registry = new ResourceRegistry({})
1684
+ registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1685
+
1686
+ registry.unregisterOrganization('test-org')
1687
+
1688
+ const result = registry.listResourcesForOrganization('test-org')
1689
+ expect(result.workflows).toHaveLength(0)
1690
+ expect(result.total).toBe(0)
1691
+ })
1692
+
1693
+ it('getRemoteConfig returns null after unregister', () => {
1694
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1695
+
1696
+ const registry = new ResourceRegistry({})
1697
+ registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1698
+
1699
+ expect(registry.getRemoteConfig('test-org', 'remote-wf')).not.toBeNull()
1700
+
1701
+ registry.unregisterOrganization('test-org')
1702
+
1703
+ expect(registry.getRemoteConfig('test-org', 'remote-wf')).toBeNull()
1704
+ })
1705
+
1706
+ it('isRemote returns false after unregister', () => {
1707
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1708
+
1709
+ const registry = new ResourceRegistry({})
1710
+ registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1711
+
1712
+ expect(registry.isRemote('test-org')).toBe(true)
1713
+
1714
+ registry.unregisterOrganization('test-org')
1715
+
1716
+ expect(registry.isRemote('test-org')).toBe(false)
1717
+ })
1718
+
1719
+ it('static resources survive unregister -- only remote resources are removed', () => {
1720
+ const staticWorkflow = createMockWorkflow('static-wf')
1721
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1722
+
1723
+ const registry = new ResourceRegistry({
1724
+ 'test-org': { workflows: [staticWorkflow] }
1725
+ })
1726
+ registry.registerOrganization('test-org', { workflows: [remoteWorkflow] }, createMockRemoteConfig())
1727
+
1728
+ // Both should be present before unregister
1729
+ expect(registry.listResourcesForOrganization('test-org').workflows).toHaveLength(2)
1730
+
1731
+ registry.unregisterOrganization('test-org')
1732
+
1733
+ const result = registry.listResourcesForOrganization('test-org')
1734
+ expect(result.workflows).toHaveLength(1)
1735
+ expect(result.workflows[0].resourceId).toBe('static-wf')
1736
+ expect(result.workflows[0].origin).toBe('local')
1737
+ })
1738
+
1739
+ it('unregistering an org with no remote resources is a no-op', () => {
1740
+ const staticWorkflow = createMockWorkflow('static-wf')
1741
+
1742
+ const registry = new ResourceRegistry({
1743
+ 'test-org': { workflows: [staticWorkflow] }
1744
+ })
1745
+
1746
+ // Should not throw or alter existing resources
1747
+ registry.unregisterOrganization('test-org')
1748
+
1749
+ const result = registry.listResourcesForOrganization('test-org')
1750
+ expect(result.workflows).toHaveLength(1)
1751
+ expect(result.workflows[0].resourceId).toBe('static-wf')
1752
+ })
1753
+ })
1754
+
1755
+ describe('registerOrganization -- redeploy (register twice)', () => {
1756
+ it('second registerOrganization call replaces previous remote resources', () => {
1757
+ const remoteWorkflowV1 = createMockWorkflow('remote-wf')
1758
+ const remoteWorkflowV2 = createMockWorkflow('remote-wf-v2')
1759
+
1760
+ const registry = new ResourceRegistry({})
1761
+
1762
+ registry.registerOrganization(
1763
+ 'test-org',
1764
+ { workflows: [remoteWorkflowV1] },
1765
+ createMockRemoteConfig({ deploymentId: 'deploy-v1' })
1766
+ )
1767
+ expect(registry.listResourcesForOrganization('test-org').workflows).toHaveLength(1)
1768
+ expect(registry.listResourcesForOrganization('test-org').workflows[0].resourceId).toBe('remote-wf')
1769
+
1770
+ registry.registerOrganization(
1771
+ 'test-org',
1772
+ { workflows: [remoteWorkflowV2] },
1773
+ createMockRemoteConfig({ deploymentId: 'deploy-v2' })
1774
+ )
1775
+
1776
+ const result = registry.listResourcesForOrganization('test-org')
1777
+ expect(result.workflows).toHaveLength(1)
1778
+ expect(result.workflows[0].resourceId).toBe('remote-wf-v2')
1779
+ })
1780
+
1781
+ it('getRemoteConfig returns the new config after redeploy, not the old one', () => {
1782
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1783
+
1784
+ const registry = new ResourceRegistry({})
1785
+
1786
+ registry.registerOrganization(
1787
+ 'test-org',
1788
+ { workflows: [remoteWorkflow] },
1789
+ createMockRemoteConfig({
1790
+ deploymentId: 'deploy-old',
1791
+ storagePath: 'test-org/deploy-old/bundle.js'
1792
+ })
1793
+ )
1794
+
1795
+ expect(registry.getRemoteConfig('test-org', 'remote-wf')?.deploymentId).toBe('deploy-old')
1796
+
1797
+ // Redeploy with same resource but new config
1798
+ registry.registerOrganization(
1799
+ 'test-org',
1800
+ { workflows: [remoteWorkflow] },
1801
+ createMockRemoteConfig({
1802
+ deploymentId: 'deploy-new',
1803
+ storagePath: 'test-org/deploy-new/bundle.js'
1804
+ })
1805
+ )
1806
+
1807
+ const config = registry.getRemoteConfig('test-org', 'remote-wf')
1808
+ expect(config?.deploymentId).toBe('deploy-new')
1809
+ expect(config?.storagePath).toBe('test-org/deploy-new/bundle.js')
1810
+ })
1811
+
1812
+ it('preserves the current remote deployment when a redeploy fails merged validation', () => {
1813
+ const staticWorkflow = createMockWorkflow('static-wf')
1814
+ const remoteWorkflow = createMockWorkflow('remote-wf')
1815
+ const replacementWorkflow = createMockWorkflow('remote-wf-v2')
1816
+
1817
+ const registry = new ResourceRegistry({
1818
+ 'test-org': {
1819
+ workflows: [staticWorkflow]
1820
+ }
1821
+ })
1822
+
1823
+ registry.registerOrganization(
1824
+ 'test-org',
1825
+ {
1826
+ workflows: [remoteWorkflow],
1827
+ relationships: {
1828
+ 'static-wf': {
1829
+ triggers: { workflows: ['remote-wf'] }
1830
+ }
1831
+ }
1832
+ },
1833
+ createMockRemoteConfig({ deploymentId: 'deploy-old' })
1834
+ )
1835
+
1836
+ expect(() => {
1837
+ registry.registerOrganization(
1838
+ 'test-org',
1839
+ { workflows: [replacementWorkflow] },
1840
+ createMockRemoteConfig({ deploymentId: 'deploy-new' })
1841
+ )
1842
+ }).toThrow("[test-org] Resource 'static-wf' triggers non-existent workflow: remote-wf")
1843
+
1844
+ const result = registry.listResourcesForOrganization('test-org')
1845
+ expect(result.workflows.find((w) => w.resourceId === 'remote-wf')).toBeDefined()
1846
+ expect(result.workflows.find((w) => w.resourceId === 'remote-wf-v2')).toBeUndefined()
1847
+ expect(registry.getRemoteConfig('test-org', 'remote-wf')?.deploymentId).toBe('deploy-old')
1848
+ })
1849
+ })
1850
+
1851
+ describe('environment filter with remote resources', () => {
1852
+ it('remote dev resources are filtered out when environment is prod', () => {
1853
+ const staticProdWorkflow = createMockWorkflow('static-prod', 'prod')
1854
+ const remoteDevWorkflow = createMockWorkflow('remote-dev', 'dev')
1855
+
1856
+ const registry = new ResourceRegistry({
1857
+ 'test-org': { workflows: [staticProdWorkflow] }
1858
+ })
1859
+ registry.registerOrganization('test-org', { workflows: [remoteDevWorkflow] }, createMockRemoteConfig())
1860
+
1861
+ const result = registry.listResourcesForOrganization('test-org', 'prod')
1862
+
1863
+ expect(result.workflows).toHaveLength(1)
1864
+ expect(result.workflows[0].resourceId).toBe('static-prod')
1865
+ expect(result.workflows.find((w) => w.resourceId === 'remote-dev')).toBeUndefined()
1866
+ })
1867
+
1868
+ it('remote prod resources are included when environment is prod', () => {
1869
+ const staticProdWorkflow = createMockWorkflow('static-prod', 'prod')
1870
+ const remoteProdWorkflow = createMockWorkflow('remote-prod', 'prod')
1871
+
1872
+ const registry = new ResourceRegistry({
1873
+ 'test-org': { workflows: [staticProdWorkflow] }
1874
+ })
1875
+ registry.registerOrganization('test-org', { workflows: [remoteProdWorkflow] }, createMockRemoteConfig())
1876
+
1877
+ const result = registry.listResourcesForOrganization('test-org', 'prod')
1878
+
1879
+ expect(result.workflows).toHaveLength(2)
1880
+ expect(result.workflows.find((w) => w.resourceId === 'static-prod')).toBeDefined()
1881
+ expect(result.workflows.find((w) => w.resourceId === 'remote-prod')).toBeDefined()
1882
+ expect(result.workflows.find((w) => w.resourceId === 'remote-prod')?.origin).toBe('remote')
1883
+ })
1884
+ })
1885
+ })
1886
+
1887
+ describe('Archived Resource Filtering', () => {
1888
+ it('excludes archived workflows from registerStaticResources', () => {
1889
+ const activeWorkflow = createMockWorkflow('active-wf')
1890
+ const archivedWorkflow: WorkflowDefinition = {
1891
+ ...createMockWorkflow('archived-wf'),
1892
+ config: {
1893
+ ...createMockWorkflow('archived-wf').config,
1894
+ archived: true
1895
+ }
1896
+ }
1897
+
1898
+ const registry = new ResourceRegistry({})
1899
+ registry.registerStaticResources('test-org', {
1900
+ workflows: [activeWorkflow, archivedWorkflow]
1901
+ })
1902
+
1903
+ const result = registry.listResourcesForOrganization('test-org')
1904
+
1905
+ expect(result.workflows).toHaveLength(1)
1906
+ expect(result.workflows[0].resourceId).toBe('active-wf')
1907
+ expect(result.total).toBe(1)
1908
+ })
1909
+
1910
+ it('excludes archived agents from registerStaticResources', () => {
1911
+ const activeAgent = createMockAgent('active-agent')
1912
+ const archivedAgent: AgentDefinition = {
1913
+ ...createMockAgent('archived-agent'),
1914
+ config: {
1915
+ ...createMockAgent('archived-agent').config,
1916
+ archived: true
1917
+ }
1918
+ }
1919
+
1920
+ const registry = new ResourceRegistry({})
1921
+ registry.registerStaticResources('test-org', {
1922
+ agents: [activeAgent, archivedAgent]
1923
+ })
1924
+
1925
+ const result = registry.listResourcesForOrganization('test-org')
1926
+
1927
+ expect(result.agents).toHaveLength(1)
1928
+ expect(result.agents[0].resourceId).toBe('active-agent')
1929
+ expect(result.total).toBe(1)
1930
+ })
1931
+
1932
+ it('includes resources without archived field (backwards compatibility)', () => {
1933
+ const workflow = createMockWorkflow('normal-wf')
1934
+ const agent = createMockAgent('normal-agent')
1935
+
1936
+ // Verify neither has archived set
1937
+ expect(workflow.config).not.toHaveProperty('archived')
1938
+ expect(agent.config).not.toHaveProperty('archived')
1939
+
1940
+ const registry = new ResourceRegistry({})
1941
+ registry.registerStaticResources('test-org', {
1942
+ workflows: [workflow],
1943
+ agents: [agent]
1944
+ })
1945
+
1946
+ const result = registry.listResourcesForOrganization('test-org')
1947
+
1948
+ expect(result.workflows).toHaveLength(1)
1949
+ expect(result.agents).toHaveLength(1)
1950
+ expect(result.total).toBe(2)
1951
+ })
1952
+
1953
+ it('excludes archived workflows from registerOrganization', () => {
1954
+ const createMockRemoteConfig = (): RemoteOrgConfig => ({
1955
+ storagePath: 'test-org/deploy-001/bundle.js',
1956
+ deploymentId: 'deploy-001'
1957
+ })
1958
+
1959
+ const activeWorkflow = createMockWorkflow('remote-active-wf')
1960
+ const archivedWorkflow: WorkflowDefinition = {
1961
+ ...createMockWorkflow('remote-archived-wf'),
1962
+ config: {
1963
+ ...createMockWorkflow('remote-archived-wf').config,
1964
+ archived: true
1965
+ }
1966
+ }
1967
+
1968
+ const registry = new ResourceRegistry({})
1969
+ registry.registerOrganization(
1970
+ 'test-org',
1971
+ { workflows: [activeWorkflow, archivedWorkflow] },
1972
+ createMockRemoteConfig()
1973
+ )
1974
+
1975
+ const result = registry.listResourcesForOrganization('test-org')
1976
+
1977
+ expect(result.workflows).toHaveLength(1)
1978
+ expect(result.workflows[0].resourceId).toBe('remote-active-wf')
1979
+ })
1980
+
1981
+ it('excludes archived agents from registerOrganization', () => {
1982
+ const createMockRemoteConfig = (): RemoteOrgConfig => ({
1983
+ storagePath: 'test-org/deploy-001/bundle.js',
1984
+ deploymentId: 'deploy-001'
1985
+ })
1986
+
1987
+ const activeAgent = createMockAgent('remote-active-agent')
1988
+ const archivedAgent: AgentDefinition = {
1989
+ ...createMockAgent('remote-archived-agent'),
1990
+ config: {
1991
+ ...createMockAgent('remote-archived-agent').config,
1992
+ archived: true
1993
+ }
1994
+ }
1995
+
1996
+ const registry = new ResourceRegistry({})
1997
+ registry.registerOrganization('test-org', { agents: [activeAgent, archivedAgent] }, createMockRemoteConfig())
1998
+
1999
+ const result = registry.listResourcesForOrganization('test-org')
2000
+
2001
+ expect(result.agents).toHaveLength(1)
2002
+ expect(result.agents[0].resourceId).toBe('remote-active-agent')
2003
+ })
2004
+ })
2005
+ })