@elevasis/core 0.2.1 → 0.4.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.
- package/dist/index.d.ts +63 -103
- package/dist/index.js +431 -111
- package/dist/organization-model/index.d.ts +63 -103
- package/dist/organization-model/index.js +431 -111
- package/package.json +1 -1
- package/src/README.md +1 -1
- package/src/__tests__/template-foundations-compatibility.test.ts +28 -36
- package/src/auth/multi-tenancy/types.ts +4 -11
- package/src/auth/multi-tenancy/users/api-schemas.ts +1 -1
- package/src/business/base-entities.test.ts +481 -0
- package/src/business/base-entities.ts +241 -0
- package/src/business/delivery/types.ts +1 -1
- package/src/business/index.ts +3 -0
- package/src/execution/index.ts +3 -6
- package/src/index.ts +1 -1
- package/src/organization-model/README.md +25 -26
- package/src/organization-model/__tests__/graph.test.ts +103 -71
- package/src/organization-model/__tests__/resolve.test.ts +22 -31
- package/src/organization-model/contracts.ts +3 -0
- package/src/organization-model/defaults.ts +59 -7
- package/src/organization-model/domains/features.ts +19 -54
- package/src/organization-model/domains/navigation.ts +266 -17
- package/src/organization-model/domains/shared.ts +1 -10
- package/src/organization-model/foundation.ts +97 -0
- package/src/organization-model/graph/build.ts +34 -67
- package/src/organization-model/graph/schema.ts +2 -4
- package/src/organization-model/graph/types.ts +3 -15
- package/src/organization-model/index.ts +2 -0
- package/src/organization-model/organization-graph.mdx +37 -28
- package/src/organization-model/organization-model.mdx +34 -36
- package/src/organization-model/published.ts +12 -3
- package/src/organization-model/schema.ts +38 -34
- package/src/organization-model/types.ts +5 -10
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/sse/events.ts +1 -34
- package/src/projects/api-schemas.ts +2 -1
- package/src/reference/_generated/contracts.md +10 -31
- package/src/reference/glossary.md +14 -18
- package/src/supabase/database.types.ts +0 -107
- package/src/test-utils/rls/RLSTestContext.ts +1 -31
- package/src/execution/calibration/__tests__/schemas.test.ts +0 -320
- package/src/execution/calibration/index.ts +0 -3
- package/src/execution/calibration/schemas.ts +0 -121
- package/src/execution/calibration/sse-events.ts +0 -125
- package/src/execution/calibration/types.ts +0 -190
|
@@ -25,7 +25,7 @@ describe('organization-graph', () => {
|
|
|
25
25
|
sourceId: 'operations',
|
|
26
26
|
label: 'Operations',
|
|
27
27
|
enabled: true,
|
|
28
|
-
|
|
28
|
+
featureId: 'operations'
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
const disabledFeature = graph.nodes.find((node) => node.id === 'feature:seo')
|
|
@@ -35,8 +35,10 @@ describe('organization-graph', () => {
|
|
|
35
35
|
enabled: false
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// Features now carry semantic data directly (no separate domain nodes)
|
|
39
|
+
const crmFeature = graph.nodes.find((node) => node.id === 'feature:crm')
|
|
40
|
+
expect(crmFeature).toMatchObject({
|
|
41
|
+
kind: 'feature',
|
|
40
42
|
sourceId: 'crm',
|
|
41
43
|
label: 'CRM'
|
|
42
44
|
})
|
|
@@ -64,24 +66,24 @@ describe('organization-graph', () => {
|
|
|
64
66
|
expect.objectContaining({
|
|
65
67
|
kind: 'contains',
|
|
66
68
|
sourceId: 'organization-model',
|
|
67
|
-
targetId: '
|
|
69
|
+
targetId: 'feature:crm'
|
|
68
70
|
}),
|
|
69
71
|
expect.objectContaining({
|
|
70
72
|
kind: 'exposes',
|
|
71
|
-
sourceId: 'feature:
|
|
73
|
+
sourceId: 'feature:crm',
|
|
72
74
|
targetId: 'surface:crm.pipeline'
|
|
73
75
|
}),
|
|
74
76
|
expect.objectContaining({
|
|
75
77
|
kind: 'references',
|
|
76
78
|
sourceId: 'surface:crm.pipeline',
|
|
77
|
-
targetId: '
|
|
79
|
+
targetId: 'feature:crm'
|
|
78
80
|
})
|
|
79
81
|
])
|
|
80
82
|
)
|
|
81
83
|
})
|
|
82
84
|
|
|
83
|
-
describe('
|
|
84
|
-
it('uses
|
|
85
|
+
describe('feature labels', () => {
|
|
86
|
+
it('uses the feature label directly from the unified features array', () => {
|
|
85
87
|
const model = resolveOrganizationModel(
|
|
86
88
|
defineOrganizationModel({
|
|
87
89
|
branding: { organizationName: 'Acme', productName: 'Acme OS', shortName: 'Acme' }
|
|
@@ -90,50 +92,75 @@ describe('organization-graph', () => {
|
|
|
90
92
|
|
|
91
93
|
const graph = buildOrganizationGraph({ organizationModel: model })
|
|
92
94
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
// Verify all default features have their labels from the unified feature definitions
|
|
96
|
+
const expectedLabels: Record<string, string> = {
|
|
97
|
+
crm: 'CRM',
|
|
98
|
+
'lead-gen': 'Lead Gen',
|
|
99
|
+
projects: 'Projects',
|
|
100
|
+
operations: 'Operations',
|
|
101
|
+
monitoring: 'Monitoring',
|
|
102
|
+
settings: 'Settings',
|
|
103
|
+
seo: 'SEO'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const [featureId, expectedLabel] of Object.entries(expectedLabels)) {
|
|
107
|
+
const node = graph.nodes.find((n) => n.id === `feature:${featureId}`)
|
|
108
|
+
expect(node, `feature node for '${featureId}' should exist`).toBeDefined()
|
|
109
|
+
expect(node?.label).toBe(expectedLabel)
|
|
102
110
|
}
|
|
103
111
|
})
|
|
104
112
|
|
|
105
|
-
it('uses the
|
|
113
|
+
it('uses the overridden label when a feature label is customized', () => {
|
|
106
114
|
const model = resolveOrganizationModel(
|
|
107
115
|
defineOrganizationModel({
|
|
108
116
|
branding: { organizationName: 'Acme', productName: 'Acme OS', shortName: 'Acme' },
|
|
109
|
-
features:
|
|
110
|
-
|
|
117
|
+
features: [
|
|
118
|
+
{
|
|
119
|
+
id: 'crm',
|
|
120
|
+
label: 'Deals',
|
|
121
|
+
enabled: true,
|
|
122
|
+
entityIds: [],
|
|
123
|
+
surfaceIds: ['crm.pipeline'],
|
|
124
|
+
resourceIds: [],
|
|
125
|
+
capabilityIds: []
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
navigation: {
|
|
129
|
+
defaultSurfaceId: 'crm.pipeline',
|
|
130
|
+
surfaces: [
|
|
131
|
+
{
|
|
132
|
+
id: 'crm.pipeline',
|
|
133
|
+
label: 'Pipeline',
|
|
134
|
+
path: '/crm/pipeline',
|
|
135
|
+
surfaceType: 'graph',
|
|
136
|
+
featureId: 'crm',
|
|
137
|
+
featureIds: ['crm'],
|
|
138
|
+
entityIds: [],
|
|
139
|
+
resourceIds: [],
|
|
140
|
+
capabilityIds: []
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
groups: [{ id: 'g', label: 'CRM', placement: 'primary', surfaceIds: ['crm.pipeline'] }]
|
|
111
144
|
}
|
|
112
145
|
})
|
|
113
146
|
)
|
|
114
147
|
|
|
115
148
|
const graph = buildOrganizationGraph({ organizationModel: model })
|
|
116
149
|
|
|
117
|
-
const node = graph.nodes.find((n) => n.id === 'feature:
|
|
118
|
-
expect(node?.label).toBe('
|
|
119
|
-
|
|
120
|
-
// Other features without explicit labels still fall back to titleCase.
|
|
121
|
-
const deliveryNode = graph.nodes.find((n) => n.id === 'feature:delivery')
|
|
122
|
-
expect(deliveryNode?.label).toBe('Delivery')
|
|
150
|
+
const node = graph.nodes.find((n) => n.id === 'feature:crm')
|
|
151
|
+
expect(node?.label).toBe('Deals')
|
|
123
152
|
})
|
|
124
153
|
})
|
|
125
154
|
|
|
126
155
|
describe('resource node upsert merging', () => {
|
|
127
156
|
it('keeps resourceMappings label and merges description from commandViewData', () => {
|
|
128
|
-
// Surface and domain are kept minimal (no cross-references) so that
|
|
129
|
-
// OrganizationModelSchema bidirectional integrity checks pass. The test
|
|
130
|
-
// is only exercising resource node merging, not surface/domain linkage.
|
|
131
157
|
const model = resolveOrganizationModel(
|
|
132
158
|
defineOrganizationModel({
|
|
133
|
-
|
|
159
|
+
features: [
|
|
134
160
|
{
|
|
135
161
|
id: 'operations',
|
|
136
162
|
label: 'Operations',
|
|
163
|
+
enabled: true,
|
|
137
164
|
color: 'violet',
|
|
138
165
|
entityIds: [],
|
|
139
166
|
surfaceIds: ['operations.command-view'],
|
|
@@ -149,8 +176,8 @@ describe('organization-graph', () => {
|
|
|
149
176
|
label: 'Command View',
|
|
150
177
|
path: '/operations/command-view',
|
|
151
178
|
surfaceType: 'graph',
|
|
152
|
-
|
|
153
|
-
|
|
179
|
+
featureId: 'operations',
|
|
180
|
+
featureIds: ['operations'],
|
|
154
181
|
entityIds: [],
|
|
155
182
|
resourceIds: ['workflow-order'],
|
|
156
183
|
capabilityIds: []
|
|
@@ -171,7 +198,7 @@ describe('organization-graph', () => {
|
|
|
171
198
|
label: 'Order Workflow',
|
|
172
199
|
resourceId: 'workflow-order',
|
|
173
200
|
resourceType: 'workflow',
|
|
174
|
-
|
|
201
|
+
featureIds: ['operations'],
|
|
175
202
|
entityIds: [],
|
|
176
203
|
surfaceIds: ['operations.command-view'],
|
|
177
204
|
capabilityIds: []
|
|
@@ -209,19 +236,18 @@ describe('organization-graph', () => {
|
|
|
209
236
|
expect(resourceNodes).toHaveLength(1)
|
|
210
237
|
|
|
211
238
|
const node = resourceNodes[0]
|
|
212
|
-
// Label from resourceMappings is a real label (not just the sourceId), so it is preserved.
|
|
213
239
|
expect(node.label).toBe('Order Workflow')
|
|
214
|
-
// Description was absent in resourceMappings, so it is filled in from commandViewData.
|
|
215
240
|
expect(node.description).toBe('Runtime description from commandViewData')
|
|
216
241
|
})
|
|
217
242
|
|
|
218
243
|
it('preserves existing non-default label even when commandViewData provides a different name', () => {
|
|
219
244
|
const model = resolveOrganizationModel(
|
|
220
245
|
defineOrganizationModel({
|
|
221
|
-
|
|
246
|
+
features: [
|
|
222
247
|
{
|
|
223
248
|
id: 'ops',
|
|
224
249
|
label: 'Ops',
|
|
250
|
+
enabled: true,
|
|
225
251
|
color: 'blue',
|
|
226
252
|
entityIds: [],
|
|
227
253
|
surfaceIds: ['ops.view'],
|
|
@@ -237,8 +263,8 @@ describe('organization-graph', () => {
|
|
|
237
263
|
label: 'Ops View',
|
|
238
264
|
path: '/ops/view',
|
|
239
265
|
surfaceType: 'graph',
|
|
240
|
-
|
|
241
|
-
|
|
266
|
+
featureId: 'operations',
|
|
267
|
+
featureIds: ['ops'],
|
|
242
268
|
entityIds: [],
|
|
243
269
|
resourceIds: ['res-a'],
|
|
244
270
|
capabilityIds: []
|
|
@@ -252,7 +278,7 @@ describe('organization-graph', () => {
|
|
|
252
278
|
label: 'My Custom Label',
|
|
253
279
|
resourceId: 'res-a',
|
|
254
280
|
resourceType: 'workflow',
|
|
255
|
-
|
|
281
|
+
featureIds: ['ops'],
|
|
256
282
|
entityIds: [],
|
|
257
283
|
surfaceIds: ['ops.view'],
|
|
258
284
|
capabilityIds: []
|
|
@@ -292,13 +318,14 @@ describe('organization-graph', () => {
|
|
|
292
318
|
})
|
|
293
319
|
|
|
294
320
|
describe('de-duplication of graph elements', () => {
|
|
295
|
-
it('creates only one
|
|
321
|
+
it('creates only one feature node when multiple surfaces reference the same feature', () => {
|
|
296
322
|
const model = resolveOrganizationModel(
|
|
297
323
|
defineOrganizationModel({
|
|
298
|
-
|
|
324
|
+
features: [
|
|
299
325
|
{
|
|
300
326
|
id: 'crm',
|
|
301
327
|
label: 'CRM',
|
|
328
|
+
enabled: true,
|
|
302
329
|
color: 'blue',
|
|
303
330
|
entityIds: [],
|
|
304
331
|
surfaceIds: ['crm.pipeline', 'crm.accounts'],
|
|
@@ -314,8 +341,8 @@ describe('organization-graph', () => {
|
|
|
314
341
|
label: 'Pipeline',
|
|
315
342
|
path: '/crm/pipeline',
|
|
316
343
|
surfaceType: 'graph',
|
|
317
|
-
|
|
318
|
-
|
|
344
|
+
featureId: 'crm',
|
|
345
|
+
featureIds: ['crm'],
|
|
319
346
|
entityIds: [],
|
|
320
347
|
resourceIds: [],
|
|
321
348
|
capabilityIds: []
|
|
@@ -325,8 +352,8 @@ describe('organization-graph', () => {
|
|
|
325
352
|
label: 'Accounts',
|
|
326
353
|
path: '/crm/accounts',
|
|
327
354
|
surfaceType: 'list',
|
|
328
|
-
|
|
329
|
-
|
|
355
|
+
featureId: 'crm',
|
|
356
|
+
featureIds: ['crm'],
|
|
330
357
|
entityIds: [],
|
|
331
358
|
resourceIds: [],
|
|
332
359
|
capabilityIds: []
|
|
@@ -339,17 +366,18 @@ describe('organization-graph', () => {
|
|
|
339
366
|
|
|
340
367
|
const graph = buildOrganizationGraph({ organizationModel: model })
|
|
341
368
|
|
|
342
|
-
const
|
|
343
|
-
expect(
|
|
369
|
+
const featureNodes = graph.nodes.filter((n) => n.id === 'feature:crm')
|
|
370
|
+
expect(featureNodes).toHaveLength(1)
|
|
344
371
|
})
|
|
345
372
|
|
|
346
|
-
it('creates only one entity node when the same entity appears in both a
|
|
373
|
+
it('creates only one entity node when the same entity appears in both a feature and a surface', () => {
|
|
347
374
|
const model = resolveOrganizationModel(
|
|
348
375
|
defineOrganizationModel({
|
|
349
|
-
|
|
376
|
+
features: [
|
|
350
377
|
{
|
|
351
378
|
id: 'crm',
|
|
352
379
|
label: 'CRM',
|
|
380
|
+
enabled: true,
|
|
353
381
|
color: 'blue',
|
|
354
382
|
entityIds: ['crm.deal'],
|
|
355
383
|
surfaceIds: ['crm.pipeline'],
|
|
@@ -365,8 +393,8 @@ describe('organization-graph', () => {
|
|
|
365
393
|
label: 'Pipeline',
|
|
366
394
|
path: '/crm/pipeline',
|
|
367
395
|
surfaceType: 'graph',
|
|
368
|
-
|
|
369
|
-
|
|
396
|
+
featureId: 'crm',
|
|
397
|
+
featureIds: ['crm'],
|
|
370
398
|
entityIds: ['crm.deal'],
|
|
371
399
|
resourceIds: [],
|
|
372
400
|
capabilityIds: []
|
|
@@ -386,10 +414,11 @@ describe('organization-graph', () => {
|
|
|
386
414
|
it('creates only one resource node when the same resource appears in resourceMappings and commandViewData', () => {
|
|
387
415
|
const model = resolveOrganizationModel(
|
|
388
416
|
defineOrganizationModel({
|
|
389
|
-
|
|
417
|
+
features: [
|
|
390
418
|
{
|
|
391
419
|
id: 'ops',
|
|
392
420
|
label: 'Ops',
|
|
421
|
+
enabled: true,
|
|
393
422
|
color: 'violet',
|
|
394
423
|
entityIds: [],
|
|
395
424
|
surfaceIds: ['ops.view'],
|
|
@@ -405,8 +434,8 @@ describe('organization-graph', () => {
|
|
|
405
434
|
label: 'Ops View',
|
|
406
435
|
path: '/ops/view',
|
|
407
436
|
surfaceType: 'graph',
|
|
408
|
-
|
|
409
|
-
|
|
437
|
+
featureId: 'operations',
|
|
438
|
+
featureIds: ['ops'],
|
|
410
439
|
entityIds: [],
|
|
411
440
|
resourceIds: ['wf-shared'],
|
|
412
441
|
capabilityIds: []
|
|
@@ -420,7 +449,7 @@ describe('organization-graph', () => {
|
|
|
420
449
|
label: 'Shared Workflow',
|
|
421
450
|
resourceId: 'wf-shared',
|
|
422
451
|
resourceType: 'workflow',
|
|
423
|
-
|
|
452
|
+
featureIds: ['ops'],
|
|
424
453
|
entityIds: [],
|
|
425
454
|
surfaceIds: ['ops.view'],
|
|
426
455
|
capabilityIds: []
|
|
@@ -463,10 +492,11 @@ describe('organization-graph', () => {
|
|
|
463
492
|
it('creates a stub resource node with label === resourceId for edges referencing unknown resources', () => {
|
|
464
493
|
const model = resolveOrganizationModel(
|
|
465
494
|
defineOrganizationModel({
|
|
466
|
-
|
|
495
|
+
features: [
|
|
467
496
|
{
|
|
468
497
|
id: 'ops',
|
|
469
498
|
label: 'Ops',
|
|
499
|
+
enabled: true,
|
|
470
500
|
color: 'violet',
|
|
471
501
|
entityIds: [],
|
|
472
502
|
surfaceIds: [],
|
|
@@ -482,8 +512,8 @@ describe('organization-graph', () => {
|
|
|
482
512
|
label: 'Ops View',
|
|
483
513
|
path: '/ops/view',
|
|
484
514
|
surfaceType: 'graph',
|
|
485
|
-
|
|
486
|
-
|
|
515
|
+
featureId: 'operations',
|
|
516
|
+
featureIds: [],
|
|
487
517
|
entityIds: [],
|
|
488
518
|
resourceIds: [],
|
|
489
519
|
capabilityIds: []
|
|
@@ -494,7 +524,6 @@ describe('organization-graph', () => {
|
|
|
494
524
|
})
|
|
495
525
|
)
|
|
496
526
|
|
|
497
|
-
// Both source and target in the edge are NOT listed in any workflow/agent/etc. array.
|
|
498
527
|
const graph = buildOrganizationGraph({
|
|
499
528
|
organizationModel: model,
|
|
500
529
|
commandViewData: {
|
|
@@ -529,10 +558,11 @@ describe('organization-graph', () => {
|
|
|
529
558
|
it('normalizes humanCheckpoints type to human_checkpoint on resource nodes', () => {
|
|
530
559
|
const model = resolveOrganizationModel(
|
|
531
560
|
defineOrganizationModel({
|
|
532
|
-
|
|
561
|
+
features: [
|
|
533
562
|
{
|
|
534
563
|
id: 'ops',
|
|
535
564
|
label: 'Ops',
|
|
565
|
+
enabled: true,
|
|
536
566
|
color: 'violet',
|
|
537
567
|
entityIds: [],
|
|
538
568
|
surfaceIds: [],
|
|
@@ -548,8 +578,8 @@ describe('organization-graph', () => {
|
|
|
548
578
|
label: 'Ops View',
|
|
549
579
|
path: '/ops/view',
|
|
550
580
|
surfaceType: 'graph',
|
|
551
|
-
|
|
552
|
-
|
|
581
|
+
featureId: 'operations',
|
|
582
|
+
featureIds: [],
|
|
553
583
|
entityIds: [],
|
|
554
584
|
resourceIds: [],
|
|
555
585
|
capabilityIds: []
|
|
@@ -591,10 +621,11 @@ describe('organization-graph', () => {
|
|
|
591
621
|
it('creates a references edge with label and relationshipType for commandViewData edges', () => {
|
|
592
622
|
const model = resolveOrganizationModel(
|
|
593
623
|
defineOrganizationModel({
|
|
594
|
-
|
|
624
|
+
features: [
|
|
595
625
|
{
|
|
596
626
|
id: 'ops',
|
|
597
627
|
label: 'Ops',
|
|
628
|
+
enabled: true,
|
|
598
629
|
color: 'violet',
|
|
599
630
|
entityIds: [],
|
|
600
631
|
surfaceIds: [],
|
|
@@ -610,8 +641,8 @@ describe('organization-graph', () => {
|
|
|
610
641
|
label: 'Ops View',
|
|
611
642
|
path: '/ops/view',
|
|
612
643
|
surfaceType: 'graph',
|
|
613
|
-
|
|
614
|
-
|
|
644
|
+
featureId: 'operations',
|
|
645
|
+
featureIds: [],
|
|
615
646
|
entityIds: [],
|
|
616
647
|
resourceIds: [],
|
|
617
648
|
capabilityIds: []
|
|
@@ -659,11 +690,12 @@ describe('organization-graph', () => {
|
|
|
659
690
|
it('bridges command view topology into resource nodes and labeled relationship edges', () => {
|
|
660
691
|
const model = resolveOrganizationModel(
|
|
661
692
|
defineOrganizationModel({
|
|
662
|
-
|
|
693
|
+
features: [
|
|
663
694
|
{
|
|
664
695
|
id: 'operations',
|
|
665
696
|
label: 'Operations',
|
|
666
697
|
description: 'Operational resources, topology, and orchestration visibility',
|
|
698
|
+
enabled: true,
|
|
667
699
|
color: 'violet',
|
|
668
700
|
entityIds: [],
|
|
669
701
|
surfaceIds: ['operations.command-view'],
|
|
@@ -679,8 +711,8 @@ describe('organization-graph', () => {
|
|
|
679
711
|
label: 'Command View',
|
|
680
712
|
path: '/operations/command-view',
|
|
681
713
|
surfaceType: 'graph',
|
|
682
|
-
|
|
683
|
-
|
|
714
|
+
featureId: 'operations',
|
|
715
|
+
featureIds: ['operations'],
|
|
684
716
|
entityIds: [],
|
|
685
717
|
resourceIds: ['workflow-order'],
|
|
686
718
|
capabilityIds: ['operations.command-view']
|
|
@@ -701,7 +733,7 @@ describe('organization-graph', () => {
|
|
|
701
733
|
label: 'Order Workflow Model',
|
|
702
734
|
resourceId: 'workflow-order',
|
|
703
735
|
resourceType: 'workflow',
|
|
704
|
-
|
|
736
|
+
featureIds: ['operations'],
|
|
705
737
|
entityIds: [],
|
|
706
738
|
surfaceIds: ['operations.command-view'],
|
|
707
739
|
capabilityIds: []
|
|
@@ -823,7 +855,7 @@ describe('organization-graph', () => {
|
|
|
823
855
|
expect.objectContaining({
|
|
824
856
|
kind: 'references',
|
|
825
857
|
sourceId: 'resource:agent-order',
|
|
826
|
-
targetId: '
|
|
858
|
+
targetId: 'feature:operations'
|
|
827
859
|
}),
|
|
828
860
|
expect.objectContaining({
|
|
829
861
|
kind: 'references',
|
|
@@ -849,7 +881,7 @@ describe('organization-graph', () => {
|
|
|
849
881
|
expect.objectContaining({
|
|
850
882
|
kind: 'maps_to',
|
|
851
883
|
sourceId: 'resource:workflow-order',
|
|
852
|
-
targetId: '
|
|
884
|
+
targetId: 'feature:operations'
|
|
853
885
|
}),
|
|
854
886
|
expect.objectContaining({
|
|
855
887
|
kind: 'maps_to',
|
|
@@ -17,16 +17,19 @@ describe('organization-model', () => {
|
|
|
17
17
|
expect(model.branding.organizationName).toBe('Acme')
|
|
18
18
|
expect(model.crm.pipelines[0]?.stages).toHaveLength(6)
|
|
19
19
|
expect(model.navigation.defaultSurfaceId).toBe('crm.pipeline')
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
const operationsFeature = model.features.find((f) => f.id === 'operations')
|
|
22
|
+
expect(operationsFeature?.enabled).toBe(true)
|
|
21
23
|
})
|
|
22
24
|
|
|
23
25
|
it('replaces arrays when a downstream override supplies a full slice', () => {
|
|
24
26
|
const model = resolveOrganizationModel({
|
|
25
|
-
|
|
27
|
+
features: [
|
|
26
28
|
{
|
|
27
29
|
id: 'crm',
|
|
28
30
|
label: 'CRM',
|
|
29
31
|
description: 'Custom CRM workspace',
|
|
32
|
+
enabled: true,
|
|
30
33
|
color: 'blue',
|
|
31
34
|
entityIds: [],
|
|
32
35
|
surfaceIds: ['custom.home'],
|
|
@@ -42,7 +45,7 @@ describe('organization-model', () => {
|
|
|
42
45
|
label: 'Home',
|
|
43
46
|
path: '/home',
|
|
44
47
|
surfaceType: 'page',
|
|
45
|
-
|
|
48
|
+
featureIds: ['crm'],
|
|
46
49
|
entityIds: [],
|
|
47
50
|
resourceIds: [],
|
|
48
51
|
capabilityIds: []
|
|
@@ -65,10 +68,10 @@ describe('organization-model', () => {
|
|
|
65
68
|
expect(model.branding.organizationName).toBe('Default Organization')
|
|
66
69
|
expect(model.branding.productName).toBe('Elevasis')
|
|
67
70
|
expect(model.branding.shortName).toBe('Elevasis')
|
|
68
|
-
expect(model.
|
|
71
|
+
expect(model.features).toHaveLength(7)
|
|
69
72
|
expect(model.navigation.defaultSurfaceId).toBe('crm.pipeline')
|
|
70
|
-
expect(model.navigation.surfaces).toHaveLength(
|
|
71
|
-
expect(model.navigation.groups).toHaveLength(
|
|
73
|
+
expect(model.navigation.surfaces).toHaveLength(22)
|
|
74
|
+
expect(model.navigation.groups).toHaveLength(4)
|
|
72
75
|
})
|
|
73
76
|
|
|
74
77
|
it('preserves sibling fields when overriding a nested property', () => {
|
|
@@ -84,15 +87,13 @@ describe('organization-model', () => {
|
|
|
84
87
|
})
|
|
85
88
|
|
|
86
89
|
it('replaces arrays entirely rather than merging', () => {
|
|
87
|
-
// Override domains with a single-item array that has no surfaceIds, so no
|
|
88
|
-
// cross-reference validation is needed. This proves the array is replaced (1 item)
|
|
89
|
-
// rather than merged/concatenated (would be 5 = 4 defaults + 1).
|
|
90
90
|
const model = resolveOrganizationModel({
|
|
91
|
-
|
|
91
|
+
features: [
|
|
92
92
|
{
|
|
93
93
|
id: 'crm',
|
|
94
94
|
label: 'CRM',
|
|
95
95
|
description: 'CRM only',
|
|
96
|
+
enabled: true,
|
|
96
97
|
color: 'blue',
|
|
97
98
|
entityIds: [],
|
|
98
99
|
surfaceIds: [],
|
|
@@ -108,7 +109,7 @@ describe('organization-model', () => {
|
|
|
108
109
|
label: 'Home',
|
|
109
110
|
path: '/home',
|
|
110
111
|
surfaceType: 'page',
|
|
111
|
-
|
|
112
|
+
featureIds: [],
|
|
112
113
|
entityIds: [],
|
|
113
114
|
resourceIds: [],
|
|
114
115
|
capabilityIds: []
|
|
@@ -118,8 +119,8 @@ describe('organization-model', () => {
|
|
|
118
119
|
}
|
|
119
120
|
})
|
|
120
121
|
|
|
121
|
-
expect(model.
|
|
122
|
-
expect(model.
|
|
122
|
+
expect(model.features).toHaveLength(1)
|
|
123
|
+
expect(model.features[0]?.id).toBe('crm')
|
|
123
124
|
})
|
|
124
125
|
|
|
125
126
|
it('ignores undefined values in the override', () => {
|
|
@@ -136,15 +137,13 @@ describe('organization-model', () => {
|
|
|
136
137
|
})
|
|
137
138
|
|
|
138
139
|
describe('navigation group normalization', () => {
|
|
139
|
-
// A self-consistent single-surface override: one domain pointing at one surface,
|
|
140
|
-
// one surface pointing back at that domain. Groups are intentionally NOT overridden
|
|
141
|
-
// so normalization fires.
|
|
142
140
|
const singleSurfaceOverride = {
|
|
143
|
-
|
|
141
|
+
features: [
|
|
144
142
|
{
|
|
145
143
|
id: 'crm',
|
|
146
144
|
label: 'CRM',
|
|
147
145
|
description: 'CRM workspace',
|
|
146
|
+
enabled: true,
|
|
148
147
|
color: 'blue',
|
|
149
148
|
entityIds: [],
|
|
150
149
|
surfaceIds: ['crm.pipeline'],
|
|
@@ -160,22 +159,17 @@ describe('organization-model', () => {
|
|
|
160
159
|
label: 'Pipeline',
|
|
161
160
|
path: '/crm/pipeline',
|
|
162
161
|
surfaceType: 'graph' as const,
|
|
163
|
-
|
|
162
|
+
featureIds: ['crm'],
|
|
164
163
|
entityIds: [],
|
|
165
164
|
resourceIds: [],
|
|
166
165
|
capabilityIds: []
|
|
167
166
|
}
|
|
168
167
|
]
|
|
169
|
-
// groups intentionally omitted
|
|
168
|
+
// groups intentionally omitted -- normalization must filter inherited groups
|
|
170
169
|
}
|
|
171
170
|
}
|
|
172
171
|
|
|
173
172
|
it('filters inherited groups to match overridden surfaces', () => {
|
|
174
|
-
// Only crm.pipeline is present. The default 'primary-workspace' group references
|
|
175
|
-
// crm.pipeline, lead-gen.lists, and projects.index. After normalization it should
|
|
176
|
-
// only contain crm.pipeline.
|
|
177
|
-
// The 'primary-operations' group references operations surfaces that are now gone,
|
|
178
|
-
// so it should be dropped entirely.
|
|
179
173
|
const model = resolveOrganizationModel(singleSurfaceOverride)
|
|
180
174
|
|
|
181
175
|
expect(model.navigation.surfaces).toHaveLength(1)
|
|
@@ -190,21 +184,19 @@ describe('organization-model', () => {
|
|
|
190
184
|
it('removes groups with no valid surface references', () => {
|
|
191
185
|
const model = resolveOrganizationModel(singleSurfaceOverride)
|
|
192
186
|
|
|
193
|
-
// The operations group
|
|
194
|
-
// references surfaces not present in the override — it must be removed entirely
|
|
187
|
+
// The operations group references surfaces not present in the override -- it must be removed
|
|
195
188
|
const operationsGroup = model.navigation.groups.find((g) => g.id === 'primary-operations')
|
|
196
189
|
expect(operationsGroup).toBeUndefined()
|
|
197
190
|
})
|
|
198
191
|
|
|
199
192
|
it('skips normalization when groups are explicitly overridden', () => {
|
|
200
|
-
// When groups are explicitly provided alongside surfaces, normalization is skipped.
|
|
201
|
-
// The group references only crm.pipeline (which is in the surface list).
|
|
202
193
|
const model = resolveOrganizationModel({
|
|
203
|
-
|
|
194
|
+
features: [
|
|
204
195
|
{
|
|
205
196
|
id: 'crm',
|
|
206
197
|
label: 'CRM',
|
|
207
198
|
description: 'CRM workspace',
|
|
199
|
+
enabled: true,
|
|
208
200
|
color: 'blue',
|
|
209
201
|
entityIds: [],
|
|
210
202
|
surfaceIds: ['crm.pipeline'],
|
|
@@ -220,7 +212,7 @@ describe('organization-model', () => {
|
|
|
220
212
|
label: 'Pipeline',
|
|
221
213
|
path: '/crm/pipeline',
|
|
222
214
|
surfaceType: 'graph',
|
|
223
|
-
|
|
215
|
+
featureIds: ['crm'],
|
|
224
216
|
entityIds: [],
|
|
225
217
|
resourceIds: [],
|
|
226
218
|
capabilityIds: []
|
|
@@ -237,7 +229,6 @@ describe('organization-model', () => {
|
|
|
237
229
|
}
|
|
238
230
|
})
|
|
239
231
|
|
|
240
|
-
// Groups come from the override exactly — no filtering applied
|
|
241
232
|
expect(model.navigation.groups).toHaveLength(1)
|
|
242
233
|
expect(model.navigation.groups[0]?.id).toBe('custom-group')
|
|
243
234
|
expect(model.navigation.groups[0]?.surfaceIds).toEqual(['crm.pipeline'])
|