@elevasis/core 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (473) hide show
  1. package/package.json +1 -1
  2. package/src/README.md +41 -41
  3. package/src/__tests__/publish.test.ts +18 -18
  4. package/src/__tests__/{template-foundations-compatibility.test.ts → template-core-compatibility.test.ts} +99 -99
  5. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1135 -1131
  6. package/src/_gen/__tests__/scaffold-contracts.test.ts +53 -53
  7. package/src/_gen/scaffold-contracts.ts +45 -45
  8. package/src/auth/multi-tenancy/credentials/README.md +38 -38
  9. package/src/auth/multi-tenancy/credentials/index.ts +6 -6
  10. package/src/auth/multi-tenancy/credentials/server/encryption.ts +39 -39
  11. package/src/auth/multi-tenancy/credentials/server/service.ts +60 -60
  12. package/src/auth/multi-tenancy/index.ts +17 -17
  13. package/src/auth/multi-tenancy/invitations/api-schemas.ts +107 -107
  14. package/src/auth/multi-tenancy/invitations/index.ts +37 -37
  15. package/src/auth/multi-tenancy/invitations/invitation.ts +86 -86
  16. package/src/auth/multi-tenancy/invitations/server/index.ts +25 -25
  17. package/src/auth/multi-tenancy/invitations/server/transforms.ts +24 -24
  18. package/src/auth/multi-tenancy/invitations/server/workos.ts +24 -24
  19. package/src/auth/multi-tenancy/invitations/supabase.ts +50 -50
  20. package/src/auth/multi-tenancy/memberships/api-schemas.ts +126 -126
  21. package/src/auth/multi-tenancy/memberships/index.ts +21 -21
  22. package/src/auth/multi-tenancy/memberships/membership.ts +138 -138
  23. package/src/auth/multi-tenancy/memberships/server/index.ts +15 -15
  24. package/src/auth/multi-tenancy/memberships/server/transforms.ts +32 -32
  25. package/src/auth/multi-tenancy/memberships/server/workos.ts +21 -21
  26. package/src/auth/multi-tenancy/memberships/supabase.ts +46 -46
  27. package/src/auth/multi-tenancy/organizations/api-schemas.ts +128 -128
  28. package/src/auth/multi-tenancy/organizations/index.ts +23 -23
  29. package/src/auth/multi-tenancy/organizations/organization.ts +24 -24
  30. package/src/auth/multi-tenancy/organizations/server/index.ts +10 -10
  31. package/src/auth/multi-tenancy/organizations/server/transforms.ts +35 -35
  32. package/src/auth/multi-tenancy/organizations/server/workos.ts +20 -20
  33. package/src/auth/multi-tenancy/types.ts +83 -83
  34. package/src/auth/multi-tenancy/users/api-schemas.ts +194 -194
  35. package/src/auth/multi-tenancy/users/index.ts +27 -27
  36. package/src/auth/multi-tenancy/users/server/index.ts +19 -19
  37. package/src/auth/multi-tenancy/users/server/transforms.ts +21 -21
  38. package/src/auth/multi-tenancy/users/server/workos.ts +16 -16
  39. package/src/auth/multi-tenancy/users/user.ts +65 -65
  40. package/src/business/README.md +52 -52
  41. package/src/business/__tests__/entities-published.test.ts +33 -33
  42. package/src/business/acquisition/api-schemas.ts +759 -759
  43. package/src/business/acquisition/index.ts +109 -109
  44. package/src/business/acquisition/types.ts +402 -402
  45. package/src/business/base-entities.test.ts +481 -481
  46. package/src/business/base-entities.ts +241 -241
  47. package/src/business/entities-published.ts +24 -24
  48. package/src/business/index.ts +15 -15
  49. package/src/business/pdf/browser/pdfmake-browser.ts +229 -229
  50. package/src/business/pdf/index.ts +10 -10
  51. package/src/business/pdf/server/index.ts +21 -21
  52. package/src/business/pdf/server/themes/default.ts +8 -8
  53. package/src/business/pdf/server/themes/index.ts +9 -9
  54. package/src/business/pdf/server/themes/types.ts +8 -8
  55. package/src/business/pdf/types.ts +272 -272
  56. package/src/business/projects/index.ts +2 -2
  57. package/src/business/projects/sse-events.ts +21 -21
  58. package/src/business/projects/types.ts +89 -89
  59. package/src/business/sales/api-schemas.ts +75 -75
  60. package/src/business/seo/__tests__/linking.test.ts +549 -549
  61. package/src/business/seo/__tests__/types.test.ts +404 -404
  62. package/src/business/seo/index.ts +2 -2
  63. package/src/business/seo/linking.ts +281 -281
  64. package/src/business/seo/types.ts +199 -199
  65. package/src/commands/queue/index.ts +3 -3
  66. package/src/commands/queue/schemas.test.ts +593 -593
  67. package/src/commands/queue/schemas.ts +125 -125
  68. package/src/commands/queue/sse-events.ts +61 -61
  69. package/src/commands/queue/types/action.ts +52 -52
  70. package/src/commands/queue/types/checkpoint.ts +44 -44
  71. package/src/commands/queue/types/index.ts +7 -7
  72. package/src/commands/queue/types/task.ts +116 -116
  73. package/src/commands/queue/types.ts +14 -14
  74. package/src/content/distribution-metadata.ts +61 -61
  75. package/src/content/index.ts +10 -10
  76. package/src/deployments/index.ts +22 -22
  77. package/src/execution/core/__tests__/archived-logs.test.ts +72 -72
  78. package/src/execution/core/index.ts +11 -11
  79. package/src/execution/core/runner-types.ts +80 -80
  80. package/src/execution/core/server/environment.ts +31 -31
  81. package/src/execution/core/sse-executions.ts +119 -119
  82. package/src/execution/core/types.ts +29 -29
  83. package/src/execution/engine/__tests__/fixtures/test-agents.ts +4 -4
  84. package/src/execution/engine/__tests__/timeout.test.ts +565 -565
  85. package/src/execution/engine/agent/__tests__/errors.test.ts +508 -508
  86. package/src/execution/engine/agent/actions/__tests__/processor.test.ts +531 -531
  87. package/src/execution/engine/agent/actions/executor.ts +205 -205
  88. package/src/execution/engine/agent/actions/navigate-knowledge-executor.ts +230 -230
  89. package/src/execution/engine/agent/actions/processor.ts +116 -116
  90. package/src/execution/engine/agent/actions/types.ts +70 -70
  91. package/src/execution/engine/agent/core/agent.ts +810 -810
  92. package/src/execution/engine/agent/core/types.ts +155 -155
  93. package/src/execution/engine/agent/errors.ts +251 -251
  94. package/src/execution/engine/agent/index.ts +78 -78
  95. package/src/execution/engine/agent/knowledge-map/types.ts +106 -106
  96. package/src/execution/engine/agent/knowledge-map/utils.ts +101 -101
  97. package/src/execution/engine/agent/memory/__tests__/manager.test.ts +754 -754
  98. package/src/execution/engine/agent/memory/domains.ts +99 -99
  99. package/src/execution/engine/agent/memory/manager.ts +365 -365
  100. package/src/execution/engine/agent/memory/processor.ts +66 -66
  101. package/src/execution/engine/agent/memory/types.ts +90 -90
  102. package/src/execution/engine/agent/memory/utils.ts +134 -134
  103. package/src/execution/engine/agent/observability/logging.ts +467 -467
  104. package/src/execution/engine/agent/observability/types.ts +64 -64
  105. package/src/execution/engine/agent/reasoning/adapters/agent-adapter-helpers.ts +349 -349
  106. package/src/execution/engine/agent/reasoning/processor.ts +92 -92
  107. package/src/execution/engine/agent/reasoning/prompt-sections/base-actions.ts +134 -134
  108. package/src/execution/engine/agent/reasoning/prompt-sections/completion.ts +49 -49
  109. package/src/execution/engine/agent/reasoning/prompt-sections/knowledge-map.ts +93 -93
  110. package/src/execution/engine/agent/reasoning/prompt-sections/memory.ts +65 -65
  111. package/src/execution/engine/agent/reasoning/prompt-sections/tools.ts +44 -44
  112. package/src/execution/engine/agent/reasoning/request-builder.ts +169 -169
  113. package/src/execution/engine/agent/reasoning/types.ts +18 -18
  114. package/src/execution/engine/base/errors.ts +118 -118
  115. package/src/execution/engine/base/index.ts +2 -2
  116. package/src/execution/engine/base/logging.ts +31 -31
  117. package/src/execution/engine/base/serialization.ts +324 -324
  118. package/src/execution/engine/base/types.ts +126 -126
  119. package/src/execution/engine/base/utils.ts +41 -41
  120. package/src/execution/engine/index.ts +434 -434
  121. package/src/execution/engine/interface/index.ts +1 -1
  122. package/src/execution/engine/interface/types.ts +62 -62
  123. package/src/execution/engine/llm/__tests__/model-info.test.ts +50 -50
  124. package/src/execution/engine/llm/__tests__/model-validation.test.ts +321 -321
  125. package/src/execution/engine/llm/__tests__/response-schema-validator.test.ts +115 -115
  126. package/src/execution/engine/llm/adapters/__tests__/adapter-factory.test.ts +375 -375
  127. package/src/execution/engine/llm/adapters/__tests__/anthropic-adapter.test.ts +463 -463
  128. package/src/execution/engine/llm/adapters/__tests__/anthropic.integration.test.ts +177 -177
  129. package/src/execution/engine/llm/adapters/__tests__/google-adapter.test.ts +722 -722
  130. package/src/execution/engine/llm/adapters/__tests__/google.integration.test.ts +376 -376
  131. package/src/execution/engine/llm/adapters/__tests__/openai-adapter.test.ts +551 -551
  132. package/src/execution/engine/llm/adapters/__tests__/openrouter-adapter.test.ts +563 -563
  133. package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +105 -105
  134. package/src/execution/engine/llm/adapters/__tests__/universal-adapter.test.ts +537 -537
  135. package/src/execution/engine/llm/adapters/circuit-breaker.ts +147 -147
  136. package/src/execution/engine/llm/adapters/index.ts +17 -17
  137. package/src/execution/engine/llm/adapters/mock-adapter.ts +116 -116
  138. package/src/execution/engine/llm/adapters/server/adapter-factory.ts +130 -130
  139. package/src/execution/engine/llm/adapters/server/anthropic.ts +137 -137
  140. package/src/execution/engine/llm/adapters/server/google.ts +283 -283
  141. package/src/execution/engine/llm/adapters/server/index.ts +12 -12
  142. package/src/execution/engine/llm/adapters/server/openai.ts +206 -206
  143. package/src/execution/engine/llm/adapters/server/openrouter.ts +235 -235
  144. package/src/execution/engine/llm/adapters/universal-adapter.ts +230 -230
  145. package/src/execution/engine/llm/errors.ts +186 -186
  146. package/src/execution/engine/llm/model-info.ts +332 -332
  147. package/src/execution/engine/llm/response-schema-validator.ts +113 -113
  148. package/src/execution/engine/llm/types.ts +86 -86
  149. package/src/execution/engine/test-utils/index.ts +6 -6
  150. package/src/execution/engine/test-utils/mocks.ts +56 -56
  151. package/src/execution/engine/tools/integration/base-integration-adapter.ts +50 -50
  152. package/src/execution/engine/tools/integration/index.ts +53 -53
  153. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-adapter.ts +73 -73
  154. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-tools.ts +209 -209
  155. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-company-email/index.ts +82 -82
  156. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-decision-maker-email/index.ts +122 -122
  157. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-person-email/index.ts +89 -89
  158. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/verify-email/index.ts +84 -84
  159. package/src/execution/engine/tools/integration/server/adapters/anymailfinder/index.ts +16 -16
  160. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +293 -293
  161. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +100 -100
  162. package/src/execution/engine/tools/integration/server/adapters/apify/apify-tools.ts +217 -217
  163. package/src/execution/engine/tools/integration/server/adapters/apify/fetch/get-dataset-items/index.ts +92 -92
  164. package/src/execution/engine/tools/integration/server/adapters/apify/fetch/run-actor/index.ts +218 -218
  165. package/src/execution/engine/tools/integration/server/adapters/apify/fetch/start-actor/index.ts +87 -87
  166. package/src/execution/engine/tools/integration/server/adapters/apify/index.ts +11 -11
  167. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +361 -361
  168. package/src/execution/engine/tools/integration/server/adapters/attio/attio-adapter.ts +162 -162
  169. package/src/execution/engine/tools/integration/server/adapters/attio/attio-tools.ts +594 -594
  170. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-attribute/index.ts +214 -214
  171. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-note/index.ts +152 -152
  172. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-record/index.ts +141 -141
  173. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-note/index.ts +86 -86
  174. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-record/index.ts +105 -105
  175. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.ts +118 -118
  176. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-attributes/index.ts +165 -165
  177. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-notes/index.ts +96 -96
  178. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-objects/index.ts +104 -104
  179. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.ts +156 -156
  180. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-attribute/index.ts +220 -220
  181. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-record/index.ts +140 -140
  182. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/utils/types.ts +146 -146
  183. package/src/execution/engine/tools/integration/server/adapters/attio/index.ts +31 -31
  184. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +210 -210
  185. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +104 -104
  186. package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
  187. package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-adapter.ts +1189 -1189
  188. package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-tools.ts +641 -641
  189. package/src/execution/engine/tools/integration/server/adapters/google-sheets/index.ts +18 -18
  190. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/activate-campaign/index.ts +86 -86
  191. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/__tests__/index.test.ts +289 -289
  192. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/index.ts +154 -154
  193. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/__tests__/index.test.ts +325 -325
  194. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/index.ts +153 -153
  195. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-delete-leads/index.ts +84 -84
  196. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-campaign/index.ts +125 -125
  197. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-inbox-test/index.ts +107 -107
  198. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/delete-campaign/index.ts +85 -85
  199. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-account-health/index.ts +91 -91
  200. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign/index.ts +92 -92
  201. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/__tests__/index.test.ts +195 -195
  202. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/index.ts +113 -113
  203. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-daily-campaign-analytics/index.ts +104 -104
  204. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-emails/index.ts +155 -155
  205. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/__tests__/index.test.ts +196 -196
  206. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/index.ts +102 -102
  207. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/__tests__/index.test.ts +189 -189
  208. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/index.ts +87 -87
  209. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-leads/index.ts +112 -112
  210. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/patch-lead/index.ts +76 -76
  211. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/pause-campaign/index.ts +86 -86
  212. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/remove-from-subsequence/index.ts +98 -98
  213. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/send-reply/index.ts +126 -126
  214. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/__tests__/index.test.ts +193 -193
  215. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/index.ts +99 -99
  216. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/__tests__/index.test.ts +621 -621
  217. package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/index.ts +125 -125
  218. package/src/execution/engine/tools/integration/server/adapters/instantly/index.ts +29 -29
  219. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-adapter.ts +178 -178
  220. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1473 -1473
  221. package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/check-credits/index.ts +59 -59
  222. package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/verify-email/index.ts +102 -102
  223. package/src/execution/engine/tools/integration/server/adapters/millionverifier/index.ts +17 -17
  224. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-adapter.ts +80 -80
  225. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +102 -102
  226. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/get-email/index.ts +102 -102
  227. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +134 -134
  228. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +75 -75
  229. package/src/execution/engine/tools/integration/server/adapters/resend/index.ts +27 -27
  230. package/src/execution/engine/tools/integration/server/adapters/resend/resend-adapter.ts +108 -108
  231. package/src/execution/engine/tools/integration/server/adapters/resend/resend-tools.ts +132 -132
  232. package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/create-envelope/index.ts +274 -274
  233. package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/download-document/index.ts +230 -230
  234. package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/get-envelope/index.ts +133 -133
  235. package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/void-envelope/index.ts +90 -90
  236. package/src/execution/engine/tools/integration/server/adapters/stripe/fetch/utils/types.ts +210 -210
  237. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-adapter.ts +517 -517
  238. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +309 -309
  239. package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/domain-search/index.ts +133 -133
  240. package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-finder/index.ts +122 -122
  241. package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-verifier/index.ts +111 -111
  242. package/src/execution/engine/tools/integration/server/adapters/tomba/index.ts +11 -11
  243. package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-adapter.ts +78 -78
  244. package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-tools.ts +222 -222
  245. package/src/execution/engine/tools/integration/server/index.ts +61 -61
  246. package/src/execution/engine/tools/integration/service.ts +161 -161
  247. package/src/execution/engine/tools/integration/tool.ts +253 -253
  248. package/src/execution/engine/tools/integration/types/anymailfinder.ts +74 -74
  249. package/src/execution/engine/tools/integration/types/apify.ts +92 -92
  250. package/src/execution/engine/tools/integration/types/index.ts +19 -19
  251. package/src/execution/engine/tools/integration/types/instantly.ts +557 -557
  252. package/src/execution/engine/tools/integration/types/millionverifier.ts +56 -56
  253. package/src/execution/engine/tools/integration/types/stripe.ts +162 -162
  254. package/src/execution/engine/tools/integration/types/tomba.ts +94 -94
  255. package/src/execution/engine/tools/lead-service-types.ts +884 -884
  256. package/src/execution/engine/tools/llm/index.ts +11 -11
  257. package/src/execution/engine/tools/llm/server/index.ts +8 -8
  258. package/src/execution/engine/tools/llm/server/llm-call-tool.ts +118 -118
  259. package/src/execution/engine/tools/platform/__tests__/pdf.test.ts +441 -441
  260. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +248 -248
  261. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +319 -319
  262. package/src/execution/engine/tools/platform/acquisition/index.ts +43 -43
  263. package/src/execution/engine/tools/platform/acquisition/list-tools.ts +148 -148
  264. package/src/execution/engine/tools/platform/acquisition/types.ts +260 -260
  265. package/src/execution/engine/tools/platform/email/index.ts +122 -122
  266. package/src/execution/engine/tools/platform/email/types.ts +96 -96
  267. package/src/execution/engine/tools/platform/index.ts +157 -157
  268. package/src/execution/engine/tools/platform/notification.ts +81 -81
  269. package/src/execution/engine/tools/platform/pdf/index.ts +110 -110
  270. package/src/execution/engine/tools/platform/pdf/types.ts +77 -77
  271. package/src/execution/engine/tools/platform/scheduler.ts +87 -87
  272. package/src/execution/engine/tools/platform/storage/index.ts +370 -370
  273. package/src/execution/engine/tools/platform/types.ts +148 -148
  274. package/src/execution/engine/tools/registry.ts +700 -700
  275. package/src/execution/engine/tools/tool-maps.ts +786 -786
  276. package/src/execution/engine/tools/types.ts +233 -233
  277. package/src/execution/engine/workflow/__tests__/errors.test.ts +139 -139
  278. package/src/execution/engine/workflow/errors.ts +63 -63
  279. package/src/execution/engine/workflow/helpers/index.ts +11 -11
  280. package/src/execution/engine/workflow/helpers/server/index.ts +8 -8
  281. package/src/execution/engine/workflow/helpers/server/llm-call.ts +93 -93
  282. package/src/execution/engine/workflow/index.ts +19 -19
  283. package/src/execution/engine/workflow/log-truncate.ts +26 -26
  284. package/src/execution/engine/workflow/logging.ts +191 -191
  285. package/src/execution/engine/workflow/types.ts +182 -182
  286. package/src/execution/engine/workflow/utils.ts +280 -280
  287. package/src/execution/engine/workflow/workflow.ts +168 -168
  288. package/src/execution/index.ts +3 -3
  289. package/src/execution/scheduler/__tests__/api-schemas.test.ts +733 -733
  290. package/src/execution/scheduler/__tests__/utils.test.ts +1009 -1009
  291. package/src/execution/scheduler/api-schemas.ts +296 -296
  292. package/src/execution/scheduler/index.ts +50 -50
  293. package/src/execution/scheduler/schemas.ts +264 -264
  294. package/src/execution/scheduler/types.ts +111 -111
  295. package/src/execution/scheduler/utils.ts +364 -364
  296. package/src/forms/index.ts +7 -7
  297. package/src/forms/schemas.ts +69 -69
  298. package/src/forms/types.ts +70 -70
  299. package/src/index.ts +71 -60
  300. package/src/integrations/credentials/__tests__/schemas.test.ts +82 -82
  301. package/src/integrations/credentials/__tests__/utils.test.ts +144 -144
  302. package/src/integrations/credentials/api-schemas.ts +143 -143
  303. package/src/integrations/credentials/index.ts +32 -32
  304. package/src/integrations/credentials/schemas.ts +164 -164
  305. package/src/integrations/credentials/utils.ts +59 -59
  306. package/src/integrations/oauth/__tests__/provider-registry.test.ts +59 -59
  307. package/src/integrations/oauth/api-schemas.ts +92 -92
  308. package/src/integrations/oauth/index.ts +19 -19
  309. package/src/integrations/oauth/provider-registry.ts +61 -61
  310. package/src/integrations/oauth/server/__tests__/refresh-concurrent.test.ts +183 -183
  311. package/src/integrations/oauth/server/__tests__/refresh.test.ts +577 -577
  312. package/src/integrations/oauth/server/credentials.ts +39 -39
  313. package/src/integrations/oauth/server/refresh.ts +214 -214
  314. package/src/integrations/oauth/types.ts +34 -34
  315. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +318 -318
  316. package/src/integrations/webhook-endpoints/api-schemas.ts +102 -102
  317. package/src/integrations/webhook-endpoints/index.ts +28 -28
  318. package/src/integrations/webhook-endpoints/types.ts +51 -51
  319. package/src/operations/activities/api-schemas.ts +79 -79
  320. package/src/operations/activities/index.ts +9 -9
  321. package/src/operations/activities/sse-events.ts +30 -30
  322. package/src/operations/activities/types.ts +63 -63
  323. package/src/operations/debug-logs/client.ts +60 -60
  324. package/src/operations/debug-logs/debug-logger.ts +83 -83
  325. package/src/operations/debug-logs/index.ts +8 -8
  326. package/src/operations/debug-logs/server.ts +19 -19
  327. package/src/operations/debug-logs/types.ts +33 -33
  328. package/src/operations/index.ts +50 -50
  329. package/src/operations/notifications/api-schemas.ts +91 -91
  330. package/src/operations/notifications/index.ts +3 -3
  331. package/src/operations/notifications/sse-events.ts +21 -21
  332. package/src/operations/notifications/types.ts +47 -47
  333. package/src/operations/observability/__tests__/openrouter-cost-flow.test.ts +297 -297
  334. package/src/operations/observability/__tests__/utils.test.ts +54 -54
  335. package/src/operations/observability/ai-usage-collector.ts +64 -64
  336. package/src/operations/observability/index.ts +13 -13
  337. package/src/operations/observability/metrics-collector.ts +49 -49
  338. package/src/operations/observability/schemas.ts +39 -39
  339. package/src/operations/observability/types.ts +463 -463
  340. package/src/operations/observability/utils.ts +77 -77
  341. package/src/operations/sessions/__tests__/manager.test.ts +821 -821
  342. package/src/operations/sessions/index.ts +26 -26
  343. package/src/operations/sessions/server/manager.ts +90 -90
  344. package/src/operations/sessions/server/session.ts +180 -180
  345. package/src/operations/sessions/types.ts +98 -98
  346. package/src/operations/triggers/index.ts +12 -12
  347. package/src/operations/triggers/webhook/definitions/instantly-account-error.ts +44 -44
  348. package/src/operations/triggers/webhook/definitions/instantly-auto-reply-received.ts +51 -51
  349. package/src/operations/triggers/webhook/definitions/instantly-campaign-completed.ts +45 -45
  350. package/src/operations/triggers/webhook/definitions/instantly-email-bounced.ts +49 -49
  351. package/src/operations/triggers/webhook/definitions/instantly-lead-unsubscribed.ts +45 -45
  352. package/src/operations/triggers/webhook/definitions/instantly-reply-received.ts +54 -54
  353. package/src/operations/triggers/webhook/index.ts +35 -35
  354. package/src/operations/triggers/webhook/types.ts +74 -74
  355. package/src/organization-model/README.md +97 -97
  356. package/src/organization-model/__tests__/defaults.test.ts +175 -175
  357. package/src/organization-model/__tests__/domains/customers.test.ts +295 -295
  358. package/src/organization-model/__tests__/domains/goals.test.ts +479 -479
  359. package/src/organization-model/__tests__/domains/identity.test.ts +279 -279
  360. package/src/organization-model/__tests__/domains/navigation.test.ts +212 -212
  361. package/src/organization-model/__tests__/domains/offerings.test.ts +419 -419
  362. package/src/organization-model/__tests__/domains/operations.test.ts +203 -203
  363. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -362
  364. package/src/organization-model/__tests__/domains/roles.test.ts +347 -347
  365. package/src/organization-model/__tests__/domains/statuses.test.ts +243 -243
  366. package/src/organization-model/__tests__/foundation.test.ts +105 -105
  367. package/src/organization-model/__tests__/graph.test.ts +894 -894
  368. package/src/organization-model/__tests__/resolve.test.ts +690 -690
  369. package/src/organization-model/__tests__/schema.test.ts +407 -407
  370. package/src/organization-model/contracts.ts +14 -14
  371. package/src/organization-model/defaults.ts +148 -148
  372. package/src/organization-model/domains/branding.ts +22 -22
  373. package/src/organization-model/domains/customers.ts +75 -75
  374. package/src/organization-model/domains/features.ts +22 -22
  375. package/src/organization-model/domains/goals.ts +80 -80
  376. package/src/organization-model/domains/identity.ts +94 -94
  377. package/src/organization-model/domains/navigation.ts +391 -391
  378. package/src/organization-model/domains/offerings.ts +66 -66
  379. package/src/organization-model/domains/operations.ts +85 -85
  380. package/src/organization-model/domains/projects.ts +48 -48
  381. package/src/organization-model/domains/prospecting.ts +33 -33
  382. package/src/organization-model/domains/roles.ts +55 -55
  383. package/src/organization-model/domains/sales.ts +94 -94
  384. package/src/organization-model/domains/shared.ts +62 -62
  385. package/src/organization-model/domains/statuses.ts +130 -130
  386. package/src/organization-model/foundation.ts +97 -97
  387. package/src/organization-model/graph/build.ts +399 -399
  388. package/src/organization-model/graph/index.ts +4 -4
  389. package/src/organization-model/graph/schema.ts +48 -48
  390. package/src/organization-model/graph/types.ts +40 -40
  391. package/src/organization-model/index.ts +13 -13
  392. package/src/organization-model/organization-graph.mdx +272 -272
  393. package/src/organization-model/organization-model.mdx +320 -320
  394. package/src/organization-model/published.ts +85 -85
  395. package/src/organization-model/resolve.ts +66 -66
  396. package/src/organization-model/schema.ts +287 -287
  397. package/src/organization-model/types.ts +46 -46
  398. package/src/platform/api/index.ts +1 -1
  399. package/src/platform/api/types.ts +35 -35
  400. package/src/platform/constants/http.ts +37 -37
  401. package/src/platform/constants/index.ts +5 -5
  402. package/src/platform/constants/limits.ts +32 -32
  403. package/src/platform/constants/resilience.ts +51 -51
  404. package/src/platform/constants/timeouts.ts +20 -20
  405. package/src/platform/constants/versions.ts +3 -3
  406. package/src/platform/registry/__tests__/resource-registry-static.test.ts +347 -347
  407. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +1028 -1028
  408. package/src/platform/registry/__tests__/resource-registry.list-executable.test.ts +393 -393
  409. package/src/platform/registry/__tests__/resource-registry.test.ts +2005 -2005
  410. package/src/platform/registry/__tests__/serialization.test.ts +1127 -1127
  411. package/src/platform/registry/command-view.ts +180 -180
  412. package/src/platform/registry/domains.ts +165 -165
  413. package/src/platform/registry/index.ts +93 -93
  414. package/src/platform/registry/reserved.ts +24 -24
  415. package/src/platform/registry/resource-metadata.ts +59 -59
  416. package/src/platform/registry/resource-registry.command-queue-groups.test.ts +129 -129
  417. package/src/platform/registry/resource-registry.ts +876 -876
  418. package/src/platform/registry/serialization.ts +273 -273
  419. package/src/platform/registry/serialized-types.ts +231 -231
  420. package/src/platform/registry/stats-types.ts +66 -66
  421. package/src/platform/registry/types.ts +404 -404
  422. package/src/platform/registry/validation.ts +513 -513
  423. package/src/platform/resilience/__tests__/rate-limiter.test.ts +471 -471
  424. package/src/platform/resilience/circuit-breaker.ts +164 -164
  425. package/src/platform/resilience/errors.ts +68 -68
  426. package/src/platform/resilience/http-error-mapper.ts +129 -129
  427. package/src/platform/resilience/index.ts +93 -93
  428. package/src/platform/resilience/rate-limiter-types.ts +46 -46
  429. package/src/platform/resilience/rate-limiter.ts +140 -140
  430. package/src/platform/resilience/retry.ts +89 -89
  431. package/src/platform/resilience/timeout.ts +63 -63
  432. package/src/platform/sse/events.ts +37 -37
  433. package/src/platform/sse/index.ts +7 -7
  434. package/src/platform/utils/__tests__/validation.test.ts +1083 -1083
  435. package/src/platform/utils/currency.ts +96 -96
  436. package/src/platform/utils/debounce.ts +52 -52
  437. package/src/platform/utils/error.ts +41 -41
  438. package/src/platform/utils/hmac.test.ts +97 -97
  439. package/src/platform/utils/index.ts +32 -32
  440. package/src/platform/utils/server/betterstack-logger.ts +210 -210
  441. package/src/platform/utils/server/hmac.ts +44 -44
  442. package/src/platform/utils/server/unsubscribe.ts +111 -111
  443. package/src/platform/utils/token-counter.ts +96 -96
  444. package/src/platform/utils/validation.ts +425 -425
  445. package/src/projects/api-schemas.ts +268 -268
  446. package/src/published.ts +1 -1
  447. package/src/reference/_generated/contracts.md +607 -607
  448. package/src/reference/glossary.md +105 -105
  449. package/src/requests/__tests__/api-schemas.test.ts +277 -277
  450. package/src/requests/api-schemas.ts +83 -83
  451. package/src/requests/index.ts +1 -1
  452. package/src/scaffold-registry/__tests__/index.test.ts +17 -0
  453. package/src/scaffold-registry/__tests__/schema.test.ts +329 -230
  454. package/src/scaffold-registry/index.ts +205 -189
  455. package/src/scaffold-registry/schema.ts +196 -128
  456. package/src/server.ts +272 -272
  457. package/src/supabase/database.types.ts +2719 -2719
  458. package/src/supabase/helpers.ts +20 -20
  459. package/src/supabase/index.ts +52 -52
  460. package/src/supabase/server/client.ts +58 -58
  461. package/src/test-utils/README.md +38 -38
  462. package/src/test-utils/browser-mocks.ts +54 -54
  463. package/src/test-utils/fixtures/api-keys.ts +52 -52
  464. package/src/test-utils/fixtures/index.ts +4 -4
  465. package/src/test-utils/fixtures/memberships.ts +80 -80
  466. package/src/test-utils/fixtures/organizations.ts +69 -69
  467. package/src/test-utils/fixtures/users.ts +79 -79
  468. package/src/test-utils/index.ts +11 -11
  469. package/src/test-utils/mocks/index.ts +2 -2
  470. package/src/test-utils/mocks/supabase.ts +142 -142
  471. package/src/test-utils/mocks/workos.ts +108 -108
  472. package/src/test-utils/rls/RLSTestContext.ts +556 -556
  473. 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
+ })