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