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