@elevasis/core 0.36.0 → 0.38.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/auth/index.d.ts +84 -1
- package/dist/index.d.ts +359 -5
- package/dist/index.js +41 -8
- package/dist/knowledge/index.d.ts +86 -1
- package/dist/organization-model/index.d.ts +359 -5
- package/dist/organization-model/index.js +41 -8
- package/dist/test-utils/index.d.ts +84 -1
- package/dist/test-utils/index.js +38 -6
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +423 -338
- package/src/_gen/__tests__/__snapshots__/system-interface-capabilities.md.snap +47 -0
- package/src/_gen/__tests__/scaffold-contracts.test.ts +47 -8
- package/src/business/acquisition/api-schemas.test.ts +13 -1
- package/src/business/acquisition/ontology-validation.ts +13 -22
- package/src/organization-model/__tests__/domains/navigation-topbar.test.ts +282 -0
- package/src/organization-model/__tests__/domains/systems.test.ts +34 -1
- package/src/organization-model/__tests__/schema.test.ts +47 -0
- package/src/organization-model/defaults.ts +2 -1
- package/src/organization-model/domains/navigation.ts +176 -139
- package/src/organization-model/domains/systems.ts +22 -9
- package/src/organization-model/icons.ts +1 -0
- package/src/organization-model/published.ts +8 -6
- package/src/organization-model/types.ts +5 -1
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/__tests__/validation.test.ts +1404 -1318
- package/src/platform/registry/index.ts +90 -88
- package/src/platform/registry/types.ts +443 -425
- package/src/platform/registry/validation.ts +60 -1
- package/src/reference/_generated/contracts.md +423 -338
- package/src/reference/_generated/system-interface-capabilities.md +47 -0
- package/src/reference/glossary.md +14 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!-- @generated by scripts/monorepo/generate-scaffold-contracts.js -- DO NOT EDIT -->
|
|
2
|
+
<!-- Regenerate: pnpm scaffold:sync -->
|
|
3
|
+
|
|
4
|
+
# System Interface Capabilities
|
|
5
|
+
|
|
6
|
+
This catalog is generated from `SYSTEM_INTERFACE_PROFILES` and the derived-readiness checks in `packages/core/src/business/acquisition/ontology-validation.ts`.
|
|
7
|
+
|
|
8
|
+
System Interface profiles are a closed platform adoption handshake. Tenant custom Systems should extend behavior through workflows/operations plus ontology, resources, and topology instead of declaring custom `apiInterface.readinessProfile` values.
|
|
9
|
+
|
|
10
|
+
| Profile | Required System path | Interface key |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| `sales.lead-gen.api` | `sales.lead-gen` | `api` |
|
|
13
|
+
| `sales.crm.api` | `sales.crm` | `api` |
|
|
14
|
+
| `sales.lead-gen.crm-handoff` | `sales.lead-gen` | `crm-handoff` |
|
|
15
|
+
|
|
16
|
+
## `sales.lead-gen.api`
|
|
17
|
+
|
|
18
|
+
- Required System path: `sales.lead-gen`
|
|
19
|
+
- Interface key: `api`
|
|
20
|
+
- Derived-readiness requirements:
|
|
21
|
+
- System Interface marker must be active and scope at least one active `sales.lead-gen` Resource.
|
|
22
|
+
- Object types: `sales.lead-gen:object/list`, `sales.lead-gen:object/company`, `sales.lead-gen:object/contact`.
|
|
23
|
+
- Scoped Resource `ontology.reads` bindings for each required object type.
|
|
24
|
+
- Catalog types with entries: `sales.lead-gen:catalog/build-template`, `sales.lead-gen:catalog/company-stage`, `sales.lead-gen:catalog/contact-stage`, and derived `sales.lead-gen:catalog/lead-gen.stage-catalog`.
|
|
25
|
+
- Scoped Resource `ontology.usesCatalogs` bindings for each required catalog.
|
|
26
|
+
- A lead-gen template-step catalog owned by `sales.lead-gen` whose `appliesTo` value is `sales.lead-gen:object/list`, plus a scoped `ontology.usesCatalogs` binding for it.
|
|
27
|
+
|
|
28
|
+
## `sales.crm.api`
|
|
29
|
+
|
|
30
|
+
- Required System path: `sales.crm`
|
|
31
|
+
- Interface key: `api`
|
|
32
|
+
- Derived-readiness requirements:
|
|
33
|
+
- System Interface marker must be active and scope at least one active `sales.crm` Resource.
|
|
34
|
+
- Catalog type with entries: `sales.crm:catalog/crm.pipeline`.
|
|
35
|
+
- Scoped Resource `ontology.usesCatalogs` binding for `sales.crm:catalog/crm.pipeline`.
|
|
36
|
+
|
|
37
|
+
## `sales.lead-gen.crm-handoff`
|
|
38
|
+
|
|
39
|
+
- Required System path: `sales.lead-gen`
|
|
40
|
+
- Interface key: `crm-handoff`
|
|
41
|
+
- Derived-readiness requirements:
|
|
42
|
+
- Derived handoff readiness is evaluated for `sales.lead-gen/crm-handoff`; it is not authored as a separate custom tenant API surface.
|
|
43
|
+
- Lead-gen API readiness requirements must pass for `sales.lead-gen/api`.
|
|
44
|
+
- CRM pipeline catalog must exist with entries: `sales.crm:catalog/crm.pipeline`. Foreign ownership is allowed because the provider is `sales.crm/api`.
|
|
45
|
+
- Derived scoped resources are active `sales.lead-gen` Resources whose `ontology.usesCatalogs` includes `sales.crm:catalog/crm.pipeline`.
|
|
46
|
+
- Provider readiness must pass for `sales.crm/api`.
|
|
47
|
+
- Topology must include a scoped `systemInterfaceGrant` relationship from consumer `sales.lead-gen/crm-handoff` to provider `sales.crm/api`.
|
|
@@ -16,8 +16,17 @@ import { describe, it, expect } from 'vitest'
|
|
|
16
16
|
/** Monorepo root relative to packages/core/src/_gen/__tests__/ */
|
|
17
17
|
const ROOT = resolve(import.meta.dirname, '..', '..', '..', '..', '..')
|
|
18
18
|
|
|
19
|
-
const OUTPUT_PATH = resolve(ROOT, 'packages/core/src/reference/_generated/contracts.md')
|
|
20
|
-
const SNAPSHOT_PATH = resolve(import.meta.dirname, '__snapshots__', 'contracts.md.snap')
|
|
19
|
+
const OUTPUT_PATH = resolve(ROOT, 'packages/core/src/reference/_generated/contracts.md')
|
|
20
|
+
const SNAPSHOT_PATH = resolve(import.meta.dirname, '__snapshots__', 'contracts.md.snap')
|
|
21
|
+
const CAPABILITY_CATALOG_OUTPUT_PATH = resolve(
|
|
22
|
+
ROOT,
|
|
23
|
+
'packages/core/src/reference/_generated/system-interface-capabilities.md'
|
|
24
|
+
)
|
|
25
|
+
const CAPABILITY_CATALOG_SNAPSHOT_PATH = resolve(
|
|
26
|
+
import.meta.dirname,
|
|
27
|
+
'__snapshots__',
|
|
28
|
+
'system-interface-capabilities.md.snap'
|
|
29
|
+
)
|
|
21
30
|
|
|
22
31
|
function normalizeSnapshotContent(content: string) {
|
|
23
32
|
return content
|
|
@@ -29,7 +38,7 @@ function normalizeSnapshotContent(content: string) {
|
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
describe('scaffold-contracts generator', () => {
|
|
32
|
-
it('output file exists and has content', () => {
|
|
41
|
+
it('output file exists and has content', () => {
|
|
33
42
|
// The generator must have been run (either manually or by CI gen step).
|
|
34
43
|
// This test validates the committed artifact — it does NOT re-run the generator
|
|
35
44
|
// so the test suite stays fast and deterministic.
|
|
@@ -43,8 +52,22 @@ describe('scaffold-contracts generator', () => {
|
|
|
43
52
|
)
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
expect(content.length).toBeGreaterThan(0)
|
|
47
|
-
})
|
|
55
|
+
expect(content.length).toBeGreaterThan(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('capability catalog output file exists and has content', () => {
|
|
59
|
+
let content: string
|
|
60
|
+
try {
|
|
61
|
+
content = readFileSync(CAPABILITY_CATALOG_OUTPUT_PATH, 'utf8')
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Generated file not found: ${CAPABILITY_CATALOG_OUTPUT_PATH}\n` +
|
|
65
|
+
`Run "pnpm scaffold:generate" or "node scripts/monorepo/generate-scaffold-contracts.js" first.`
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(content.length).toBeGreaterThan(0)
|
|
70
|
+
})
|
|
48
71
|
|
|
49
72
|
it('output file matches stored snapshot', () => {
|
|
50
73
|
let content: string
|
|
@@ -59,6 +82,22 @@ describe('scaffold-contracts generator', () => {
|
|
|
59
82
|
|
|
60
83
|
const snapshot = readFileSync(SNAPSHOT_PATH, 'utf8')
|
|
61
84
|
|
|
62
|
-
expect(normalizeSnapshotContent(content)).toBe(normalizeSnapshotContent(snapshot))
|
|
63
|
-
})
|
|
64
|
-
|
|
85
|
+
expect(normalizeSnapshotContent(content)).toBe(normalizeSnapshotContent(snapshot))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('capability catalog output file matches stored snapshot', () => {
|
|
89
|
+
let content: string
|
|
90
|
+
try {
|
|
91
|
+
content = readFileSync(CAPABILITY_CATALOG_OUTPUT_PATH, 'utf8')
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Generated file not found: ${CAPABILITY_CATALOG_OUTPUT_PATH}\n` +
|
|
95
|
+
`Run "pnpm scaffold:generate" first to produce the artifact before snapshotting.`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const snapshot = readFileSync(CAPABILITY_CATALOG_SNAPSHOT_PATH, 'utf8')
|
|
100
|
+
|
|
101
|
+
expect(normalizeSnapshotContent(content)).toBe(normalizeSnapshotContent(snapshot))
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { type CrmPriorityRuleConfig, type StatefulPipelineDefinition } from '../../organization-model/domains/sales'
|
|
3
|
+
import { SYSTEM_INTERFACE_PROFILES } from '../../organization-model/domains/systems'
|
|
3
4
|
|
|
4
5
|
// Inline fixture for lead-gen pipeline stage/state validation tests.
|
|
5
6
|
// The canonical constants live in @repo/elevasis-core; @repo/core cannot depend on it.
|
|
@@ -503,6 +504,14 @@ describe('DealStageSchema', () => {
|
|
|
503
504
|
// ---------------------------------------------------------------------------
|
|
504
505
|
|
|
505
506
|
describe('business ontology validation contracts', () => {
|
|
507
|
+
it('derives compatibility interface constants from the canonical profile registry', () => {
|
|
508
|
+
expect(SYSTEM_INTERFACE_PROFILES).toEqual([
|
|
509
|
+
LEAD_GEN_API_INTERFACE,
|
|
510
|
+
CRM_API_INTERFACE,
|
|
511
|
+
LEAD_GEN_CRM_HANDOFF_INTERFACE
|
|
512
|
+
])
|
|
513
|
+
})
|
|
514
|
+
|
|
506
515
|
it('computes structured readiness for a ready lead-gen API interface without CRM', () => {
|
|
507
516
|
const model = buildMinimalLeadGenModel()
|
|
508
517
|
const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
|
|
@@ -655,12 +664,15 @@ describe('business ontology validation contracts', () => {
|
|
|
655
664
|
const model = buildMinimalLeadGenModel()
|
|
656
665
|
const apiInterface = model.systems.sales?.systems?.['lead-gen']?.apiInterface
|
|
657
666
|
if (apiInterface === undefined) throw new Error('Minimal fixture is missing lead-gen API interface')
|
|
658
|
-
apiInterface.readinessProfile = 'sales.lead-gen.unknown'
|
|
667
|
+
;(apiInterface as { readinessProfile?: string }).readinessProfile = 'sales.lead-gen.unknown'
|
|
659
668
|
|
|
660
669
|
const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
|
|
661
670
|
|
|
662
671
|
expect(readiness.ready).toBe(false)
|
|
663
672
|
expect(readiness.issues).toMatchObject([{ family: 'SYSTEM_INTERFACE_INVALID', code: 'unknown-readiness-profile' }])
|
|
673
|
+
expect(readiness.issues[0]?.message).toContain('Supported profiles: "sales.lead-gen.api", "sales.crm.api", "sales.lead-gen.crm-handoff"')
|
|
674
|
+
expect(readiness.issues[0]?.message).toContain('Custom Systems should not declare apiInterface')
|
|
675
|
+
expect(readiness.issues[0]?.message).toContain('workflows/operations plus ontology, resources, and topology')
|
|
664
676
|
})
|
|
665
677
|
|
|
666
678
|
it('lead-gen-to-CRM handoff validation fails when the scoped topology grant is absent', () => {
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
} from '../../organization-model/ontology'
|
|
11
11
|
import type { OrganizationModel } from '../../organization-model/types'
|
|
12
12
|
import type { ResourceEntry } from '../../organization-model/domains/resources'
|
|
13
|
+
import {
|
|
14
|
+
SYSTEM_INTERFACE_PROFILES,
|
|
15
|
+
SYSTEM_INTERFACE_READINESS_PROFILES
|
|
16
|
+
} from '../../organization-model/domains/systems'
|
|
13
17
|
import type { OmTopologyRelationship } from '../../organization-model/domains/topology'
|
|
14
18
|
import {
|
|
15
19
|
type LeadGenStageCatalogEntry,
|
|
@@ -18,23 +22,11 @@ import {
|
|
|
18
22
|
import { getSystem } from '../../organization-model/helpers'
|
|
19
23
|
import { getLeadGenStageCatalog } from '../../organization-model/migration-helpers'
|
|
20
24
|
|
|
21
|
-
export const LEAD_GEN_API_INTERFACE =
|
|
22
|
-
systemPath: 'sales.lead-gen',
|
|
23
|
-
interfaceKey: 'api',
|
|
24
|
-
readinessProfile: 'sales.lead-gen.api'
|
|
25
|
-
} as const
|
|
25
|
+
export const LEAD_GEN_API_INTERFACE = SYSTEM_INTERFACE_PROFILES[0]
|
|
26
26
|
|
|
27
|
-
export const CRM_API_INTERFACE =
|
|
28
|
-
systemPath: 'sales.crm',
|
|
29
|
-
interfaceKey: 'api',
|
|
30
|
-
readinessProfile: 'sales.crm.api'
|
|
31
|
-
} as const
|
|
27
|
+
export const CRM_API_INTERFACE = SYSTEM_INTERFACE_PROFILES[1]
|
|
32
28
|
|
|
33
|
-
export const LEAD_GEN_CRM_HANDOFF_INTERFACE =
|
|
34
|
-
systemPath: 'sales.lead-gen',
|
|
35
|
-
interfaceKey: 'crm-handoff',
|
|
36
|
-
readinessProfile: 'sales.lead-gen.crm-handoff'
|
|
37
|
-
} as const
|
|
29
|
+
export const LEAD_GEN_CRM_HANDOFF_INTERFACE = SYSTEM_INTERFACE_PROFILES[2]
|
|
38
30
|
|
|
39
31
|
const LEAD_GEN_API_READINESS_LABEL = LEAD_GEN_API_INTERFACE.readinessProfile
|
|
40
32
|
const CRM_API_READINESS_LABEL = CRM_API_INTERFACE.readinessProfile
|
|
@@ -215,6 +207,10 @@ function formatInterfaceReadinessFailure(result: SystemInterfaceReadinessResult)
|
|
|
215
207
|
return `${identity} readiness failed${result.readinessProfile ? ` (${result.readinessProfile})` : ''}: ${issueSummary}`
|
|
216
208
|
}
|
|
217
209
|
|
|
210
|
+
function formatSupportedReadinessProfiles(): string {
|
|
211
|
+
return SYSTEM_INTERFACE_READINESS_PROFILES.map((profile) => `"${profile}"`).join(', ')
|
|
212
|
+
}
|
|
213
|
+
|
|
218
214
|
function throwIfInterfaceNotReady(result: SystemInterfaceReadinessResult): void {
|
|
219
215
|
if (result.ready) return
|
|
220
216
|
throw new SystemInterfaceReadinessError(result)
|
|
@@ -607,20 +603,15 @@ export function computeInterfaceReadiness(
|
|
|
607
603
|
)
|
|
608
604
|
}
|
|
609
605
|
|
|
610
|
-
const supportedProfiles = [
|
|
611
|
-
LEAD_GEN_API_INTERFACE.readinessProfile,
|
|
612
|
-
CRM_API_INTERFACE.readinessProfile,
|
|
613
|
-
LEAD_GEN_CRM_HANDOFF_INTERFACE.readinessProfile
|
|
614
|
-
] as const
|
|
615
606
|
const supportedProfile =
|
|
616
|
-
readinessProfile !== undefined &&
|
|
607
|
+
readinessProfile !== undefined && SYSTEM_INTERFACE_PROFILES.some((profile) => profile.readinessProfile === readinessProfile)
|
|
617
608
|
|
|
618
609
|
if (!supportedProfile) {
|
|
619
610
|
addReadinessIssue(
|
|
620
611
|
issues,
|
|
621
612
|
'SYSTEM_INTERFACE_INVALID',
|
|
622
613
|
'unknown-readiness-profile',
|
|
623
|
-
`System Interface "${formatInterfaceIdentity(request.systemPath, request.interfaceKey)}" references unknown readiness profile "${readinessProfile}".`,
|
|
614
|
+
`System Interface "${formatInterfaceIdentity(request.systemPath, request.interfaceKey)}" references unknown readiness profile "${readinessProfile}". Supported profiles: ${formatSupportedReadinessProfiles()}. Custom Systems should not declare apiInterface; route custom behavior through workflows/operations plus ontology, resources, and topology.`,
|
|
624
615
|
{ path: `${readinessMarkerPath(request)}.readinessProfile`, ref: readinessProfile }
|
|
625
616
|
)
|
|
626
617
|
return {
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
OrganizationModelNavigationSchema,
|
|
4
|
+
TopbarActionNodeSchema,
|
|
5
|
+
TopbarSectionSchema
|
|
6
|
+
} from '../../domains/navigation'
|
|
7
|
+
import { OrganizationModelSchema } from '../../schema'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
function makeMinimalSystems() {
|
|
14
|
+
return {
|
|
15
|
+
dashboard: {
|
|
16
|
+
id: 'dashboard',
|
|
17
|
+
order: 10,
|
|
18
|
+
label: 'Dashboard',
|
|
19
|
+
enabled: true,
|
|
20
|
+
lifecycle: 'active' as const,
|
|
21
|
+
path: '/'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeMinimalModel(overrides: Record<string, unknown> = {}) {
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
branding: { organizationName: 'Test', productName: 'Test OS', shortName: 'Test' },
|
|
30
|
+
systems: makeMinimalSystems(),
|
|
31
|
+
entities: {},
|
|
32
|
+
actions: {},
|
|
33
|
+
...overrides
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// TopbarActionNodeSchema
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe('TopbarActionNodeSchema — leaf node shape', () => {
|
|
42
|
+
it('accepts a minimal node (id + label + enabled default)', () => {
|
|
43
|
+
const result = TopbarActionNodeSchema.safeParse({
|
|
44
|
+
id: 'request',
|
|
45
|
+
label: 'Request a feature or report an issue'
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(result.success).toBe(true)
|
|
49
|
+
if (result.success) {
|
|
50
|
+
expect(result.data.enabled).toBe(true)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('accepts a fully populated node', () => {
|
|
55
|
+
const result = TopbarActionNodeSchema.safeParse({
|
|
56
|
+
id: 'request',
|
|
57
|
+
label: 'Request a feature or report an issue',
|
|
58
|
+
tooltip: 'Request a feature or report an issue',
|
|
59
|
+
icon: 'message-plus',
|
|
60
|
+
order: 10,
|
|
61
|
+
enabled: true,
|
|
62
|
+
devOnly: false,
|
|
63
|
+
requiresAdmin: false,
|
|
64
|
+
targets: { systems: ['monitoring'], actions: [] }
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
expect(result.success).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('defaults enabled to true when omitted', () => {
|
|
71
|
+
const result = TopbarActionNodeSchema.parse({ id: 'docs', label: 'Docs' })
|
|
72
|
+
|
|
73
|
+
expect(result.enabled).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('accepts enabled: false to mark an item as hidden', () => {
|
|
77
|
+
const result = TopbarActionNodeSchema.parse({ id: 'docs', label: 'Docs', enabled: false })
|
|
78
|
+
|
|
79
|
+
expect(result.enabled).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('accepts optional devOnly flag', () => {
|
|
83
|
+
const result = TopbarActionNodeSchema.parse({ id: 'devtools', label: 'Dev Tools', devOnly: true })
|
|
84
|
+
|
|
85
|
+
expect(result.devOnly).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('accepts optional requiresAdmin flag', () => {
|
|
89
|
+
const result = TopbarActionNodeSchema.parse({ id: 'admin-panel', label: 'Admin Panel', requiresAdmin: true })
|
|
90
|
+
|
|
91
|
+
expect(result.requiresAdmin).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('rejects a node missing id', () => {
|
|
95
|
+
const result = TopbarActionNodeSchema.safeParse({ label: 'No Id' })
|
|
96
|
+
|
|
97
|
+
expect(result.success).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('rejects a node missing label', () => {
|
|
101
|
+
const result = TopbarActionNodeSchema.safeParse({ id: 'no-label' })
|
|
102
|
+
|
|
103
|
+
expect(result.success).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does NOT have a path field (leaf, not a route)', () => {
|
|
107
|
+
const result = TopbarActionNodeSchema.parse({ id: 'request', label: 'Request' })
|
|
108
|
+
|
|
109
|
+
expect('path' in result).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('does NOT have a surfaceType field (closed enum untouched)', () => {
|
|
113
|
+
const result = TopbarActionNodeSchema.parse({ id: 'request', label: 'Request' })
|
|
114
|
+
|
|
115
|
+
expect('surfaceType' in result).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// TopbarSectionSchema
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
describe('TopbarSectionSchema — flat map of topbar action nodes', () => {
|
|
124
|
+
it('defaults to an empty record', () => {
|
|
125
|
+
const result = TopbarSectionSchema.parse(undefined)
|
|
126
|
+
|
|
127
|
+
expect(result).toEqual({})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('accepts a populated map', () => {
|
|
131
|
+
const result = TopbarSectionSchema.safeParse({
|
|
132
|
+
request: { id: 'request', label: 'Request' },
|
|
133
|
+
docs: { id: 'docs', label: 'Docs', icon: 'knowledge' }
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(result.success).toBe(true)
|
|
137
|
+
if (result.success) {
|
|
138
|
+
expect(Object.keys(result.data)).toEqual(['request', 'docs'])
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// OrganizationModelNavigationSchema — topbar field added
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
describe('OrganizationModelNavigationSchema — topbar region', () => {
|
|
148
|
+
it('defaults topbar to an empty record', () => {
|
|
149
|
+
const result = OrganizationModelNavigationSchema.parse({})
|
|
150
|
+
|
|
151
|
+
expect(result.topbar).toEqual({})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('retains existing sidebar defaults (regression: append only, sidebar untouched)', () => {
|
|
155
|
+
const result = OrganizationModelNavigationSchema.parse({})
|
|
156
|
+
|
|
157
|
+
expect(result.sidebar.primary).toEqual({})
|
|
158
|
+
expect(result.sidebar.bottom).toEqual({})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('accepts topbar nodes alongside sidebar', () => {
|
|
162
|
+
const result = OrganizationModelNavigationSchema.safeParse({
|
|
163
|
+
sidebar: { primary: {}, bottom: {} },
|
|
164
|
+
topbar: {
|
|
165
|
+
request: {
|
|
166
|
+
id: 'request',
|
|
167
|
+
label: 'Request a feature or report an issue',
|
|
168
|
+
tooltip: 'Request a feature or report an issue',
|
|
169
|
+
icon: 'message-plus',
|
|
170
|
+
order: 10,
|
|
171
|
+
enabled: true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(result.success).toBe(true)
|
|
177
|
+
if (result.success) {
|
|
178
|
+
expect(result.data.topbar['request']?.id).toBe('request')
|
|
179
|
+
expect(result.data.topbar['request']?.icon).toBe('message-plus')
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// OrganizationModelSchema — navigation.topbar propagates through full schema
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
describe('OrganizationModelSchema — navigation.topbar contract', () => {
|
|
189
|
+
it('accepts a model with topbar declared', () => {
|
|
190
|
+
const result = OrganizationModelSchema.safeParse(
|
|
191
|
+
makeMinimalModel({
|
|
192
|
+
navigation: {
|
|
193
|
+
topbar: {
|
|
194
|
+
request: {
|
|
195
|
+
id: 'request',
|
|
196
|
+
label: 'Request a feature or report an issue',
|
|
197
|
+
tooltip: 'Request a feature or report an issue',
|
|
198
|
+
icon: 'message-plus',
|
|
199
|
+
order: 10,
|
|
200
|
+
enabled: true
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(result.success).toBe(true)
|
|
208
|
+
if (result.success) {
|
|
209
|
+
expect(result.data.navigation.topbar['request']?.id).toBe('request')
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('defaults navigation.topbar to empty when navigation is omitted', () => {
|
|
214
|
+
const result = OrganizationModelSchema.safeParse(makeMinimalModel())
|
|
215
|
+
|
|
216
|
+
expect(result.success).toBe(true)
|
|
217
|
+
if (result.success) {
|
|
218
|
+
expect(result.data.navigation.topbar).toEqual({})
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('defaults navigation.topbar to empty when only sidebar is provided', () => {
|
|
223
|
+
const result = OrganizationModelSchema.safeParse(
|
|
224
|
+
makeMinimalModel({
|
|
225
|
+
navigation: {
|
|
226
|
+
sidebar: {
|
|
227
|
+
primary: {
|
|
228
|
+
dashboard: {
|
|
229
|
+
type: 'surface',
|
|
230
|
+
label: 'Dashboard',
|
|
231
|
+
path: '/',
|
|
232
|
+
surfaceType: 'dashboard',
|
|
233
|
+
order: 10
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
bottom: {}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
expect(result.success).toBe(true)
|
|
243
|
+
if (result.success) {
|
|
244
|
+
expect(result.data.navigation.topbar).toEqual({})
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('coexists with sidebar — both sections independently populated', () => {
|
|
249
|
+
const result = OrganizationModelSchema.safeParse(
|
|
250
|
+
makeMinimalModel({
|
|
251
|
+
navigation: {
|
|
252
|
+
sidebar: {
|
|
253
|
+
primary: {
|
|
254
|
+
dashboard: {
|
|
255
|
+
type: 'surface',
|
|
256
|
+
label: 'Dashboard',
|
|
257
|
+
path: '/',
|
|
258
|
+
surfaceType: 'dashboard',
|
|
259
|
+
order: 10
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
bottom: {}
|
|
263
|
+
},
|
|
264
|
+
topbar: {
|
|
265
|
+
request: {
|
|
266
|
+
id: 'request',
|
|
267
|
+
label: 'Request',
|
|
268
|
+
order: 10,
|
|
269
|
+
enabled: true
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
expect(result.success).toBe(true)
|
|
277
|
+
if (result.success) {
|
|
278
|
+
expect(result.data.navigation.sidebar.primary['dashboard']?.label).toBe('Dashboard')
|
|
279
|
+
expect(result.data.navigation.topbar['request']?.label).toBe('Request')
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
})
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_ORGANIZATION_MODEL_SYSTEMS,
|
|
4
|
+
SYSTEM_INTERFACE_PROFILES,
|
|
4
5
|
SystemEntrySchema,
|
|
5
6
|
SystemApiInterfaceSchema,
|
|
7
|
+
SystemInterfaceReadinessProfileSchema,
|
|
6
8
|
SystemInterfaceRefSchema,
|
|
7
9
|
SystemKindSchema,
|
|
8
10
|
SystemStatusSchema,
|
|
9
11
|
SystemsDomainSchema
|
|
10
12
|
} from '../../domains/systems'
|
|
11
|
-
import { resolveOrganizationModel } from '../../resolve'
|
|
13
|
+
import { resolveOrganizationModel } from '../../resolve'
|
|
14
|
+
import type {
|
|
15
|
+
OrganizationModelSystemApiInterface,
|
|
16
|
+
OrganizationModelSystemInterfaceReadinessProfile
|
|
17
|
+
} from '../../types'
|
|
12
18
|
|
|
13
19
|
const VALID_SYSTEM = {
|
|
14
20
|
id: 'sys.lead-gen',
|
|
@@ -60,6 +66,32 @@ describe('SystemEntrySchema - positive parse', () => {
|
|
|
60
66
|
}
|
|
61
67
|
})
|
|
62
68
|
|
|
69
|
+
it('accepts only registry-backed readiness profiles', () => {
|
|
70
|
+
const profileValues = SYSTEM_INTERFACE_PROFILES.map((profile) => profile.readinessProfile)
|
|
71
|
+
|
|
72
|
+
for (const readinessProfile of profileValues) {
|
|
73
|
+
expect(SystemInterfaceReadinessProfileSchema.safeParse(readinessProfile).success).toBe(true)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expect(SystemInterfaceReadinessProfileSchema.safeParse('sales.lead-gen.unknown').success).toBe(false)
|
|
77
|
+
expect(SystemApiInterfaceSchema.safeParse({ readinessProfile: 'sales.lead-gen.unknown' }).success).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('publishes a narrowed System Interface readiness profile type', () => {
|
|
81
|
+
const acceptedReadinessProfile: OrganizationModelSystemInterfaceReadinessProfile = 'sales.lead-gen.api'
|
|
82
|
+
const acceptedInterface: OrganizationModelSystemApiInterface = {
|
|
83
|
+
lifecycle: 'active',
|
|
84
|
+
readinessProfile: acceptedReadinessProfile,
|
|
85
|
+
resourceIds: []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// @ts-expect-error unsupported profiles are not part of the published readiness profile union.
|
|
89
|
+
const unsupportedReadinessProfile: OrganizationModelSystemInterfaceReadinessProfile = 'sales.lead-gen.unknown'
|
|
90
|
+
|
|
91
|
+
expect(acceptedInterface.readinessProfile).toBe('sales.lead-gen.api')
|
|
92
|
+
expect(unsupportedReadinessProfile).toBe('sales.lead-gen.unknown')
|
|
93
|
+
})
|
|
94
|
+
|
|
63
95
|
it('represents missing and disabled System Interfaces distinctly', () => {
|
|
64
96
|
const missing = SystemEntrySchema.parse(VALID_SYSTEM)
|
|
65
97
|
const disabled = SystemEntrySchema.parse({
|
|
@@ -131,6 +163,7 @@ describe('SystemEntrySchema - negative parse', () => {
|
|
|
131
163
|
expect(SystemApiInterfaceSchema.safeParse({ kind: 'api', lifecycle: 'active' }).success).toBe(false)
|
|
132
164
|
expect(SystemApiInterfaceSchema.safeParse({ lifecycle: 'paused' }).success).toBe(false)
|
|
133
165
|
expect(SystemApiInterfaceSchema.safeParse({ readinessProfile: 'Sales Lead Gen API' }).success).toBe(false)
|
|
166
|
+
expect(SystemApiInterfaceSchema.safeParse({ readinessProfile: 'sales.lead-gen.unknown' }).success).toBe(false)
|
|
134
167
|
expect(
|
|
135
168
|
SystemInterfaceRefSchema.safeParse({ systemPath: 'sales.lead-gen', interfaceKey: 'api' }).success
|
|
136
169
|
).toBe(true)
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
listResolvedOntologyRecords,
|
|
8
8
|
parseOntologyId
|
|
9
9
|
} from '../ontology'
|
|
10
|
+
import { SYSTEM_INTERFACE_PROFILES } from '../domains/systems'
|
|
10
11
|
import { resolveOrganizationModelWithResources } from '../resolve'
|
|
11
12
|
import { OrganizationModelSchema } from '../schema'
|
|
12
13
|
|
|
@@ -236,6 +237,52 @@ describe('retired contract authoring surfaces', () => {
|
|
|
236
237
|
})
|
|
237
238
|
|
|
238
239
|
describe('System Interface schema integration', () => {
|
|
240
|
+
it('accepts every registry-backed System Interface readiness profile', () => {
|
|
241
|
+
for (const profile of SYSTEM_INTERFACE_PROFILES) {
|
|
242
|
+
const result = OrganizationModelSchema.safeParse({
|
|
243
|
+
...makeMinimalModel({
|
|
244
|
+
sales: {
|
|
245
|
+
id: 'sales',
|
|
246
|
+
order: 10,
|
|
247
|
+
label: 'Sales',
|
|
248
|
+
lifecycle: 'active'
|
|
249
|
+
},
|
|
250
|
+
[profile.systemPath]: {
|
|
251
|
+
...makeSystem(profile.systemPath, `/${profile.systemPath.replaceAll('.', '/')}`),
|
|
252
|
+
apiInterface: {
|
|
253
|
+
lifecycle: 'active',
|
|
254
|
+
readinessProfile: profile.readinessProfile
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
expect(result.success).toBe(true)
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('rejects unsupported System Interface readiness profiles', () => {
|
|
265
|
+
const messages = getIssueMessages(
|
|
266
|
+
makeMinimalModel({
|
|
267
|
+
sales: {
|
|
268
|
+
id: 'sales',
|
|
269
|
+
order: 10,
|
|
270
|
+
label: 'Sales',
|
|
271
|
+
lifecycle: 'active'
|
|
272
|
+
},
|
|
273
|
+
'sales.lead-gen': {
|
|
274
|
+
...makeSystem('sales.lead-gen', '/lead-gen/lists'),
|
|
275
|
+
apiInterface: {
|
|
276
|
+
lifecycle: 'active',
|
|
277
|
+
readinessProfile: 'sales.lead-gen.unknown'
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
expect(messages.some((message) => message.includes('Invalid option'))).toBe(true)
|
|
284
|
+
})
|
|
285
|
+
|
|
239
286
|
it('accepts a System API Interface scoped to resources from the same System', () => {
|
|
240
287
|
const result = OrganizationModelSchema.safeParse({
|
|
241
288
|
...makeMinimalModel({
|