@elevasis/core 0.5.0 → 0.7.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 +428 -42
- package/dist/index.js +596 -47
- package/dist/organization-model/index.d.ts +428 -42
- package/dist/organization-model/index.js +596 -47
- package/package.json +4 -3
- package/src/__tests__/template-foundations-compatibility.test.ts +2 -2
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1131 -0
- package/src/_gen/__tests__/scaffold-contracts.test.ts +53 -0
- package/src/_gen/scaffold-contracts.ts +45 -0
- package/src/business/acquisition/types.ts +2 -0
- package/src/commands/queue/types/task.ts +3 -3
- package/src/execution/engine/index.ts +8 -0
- package/src/execution/engine/tools/registry.ts +26 -24
- package/src/execution/engine/tools/tool-maps.ts +13 -9
- package/src/execution/engine/workflow/types.ts +2 -3
- package/src/index.ts +10 -0
- package/src/organization-model/README.md +16 -12
- package/src/organization-model/__tests__/defaults.test.ts +175 -0
- package/src/organization-model/__tests__/domains/customers.test.ts +295 -0
- package/src/organization-model/__tests__/domains/goals.test.ts +479 -0
- package/src/organization-model/__tests__/domains/identity.test.ts +279 -0
- package/src/organization-model/__tests__/domains/navigation.test.ts +212 -0
- package/src/organization-model/__tests__/domains/offerings.test.ts +419 -0
- package/src/organization-model/__tests__/domains/operations.test.ts +203 -0
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -0
- package/src/organization-model/__tests__/domains/roles.test.ts +347 -0
- package/src/organization-model/__tests__/domains/statuses.test.ts +243 -0
- package/src/organization-model/__tests__/foundation.test.ts +3 -3
- package/src/organization-model/__tests__/resolve.test.ts +447 -3
- package/src/organization-model/__tests__/schema.test.ts +407 -0
- package/src/organization-model/contracts.ts +5 -5
- package/src/organization-model/defaults.ts +39 -16
- package/src/organization-model/domains/customers.ts +75 -0
- package/src/organization-model/domains/goals.ts +80 -0
- package/src/organization-model/domains/identity.ts +94 -0
- package/src/organization-model/domains/navigation.ts +43 -4
- package/src/organization-model/domains/offerings.ts +66 -0
- package/src/organization-model/domains/operations.ts +85 -0
- package/src/organization-model/domains/{delivery.ts → projects.ts} +6 -6
- package/src/organization-model/domains/{lead-gen.ts → prospecting.ts} +5 -5
- package/src/organization-model/domains/roles.ts +55 -0
- package/src/organization-model/domains/sales.ts +94 -0
- package/src/organization-model/domains/shared.ts +30 -1
- package/src/organization-model/domains/statuses.ts +130 -0
- package/src/organization-model/index.ts +3 -3
- package/src/organization-model/organization-graph.mdx +1 -0
- package/src/organization-model/organization-model.mdx +84 -19
- package/src/organization-model/published.ts +53 -8
- package/src/organization-model/schema.ts +67 -7
- package/src/organization-model/types.ts +31 -7
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/types.ts +1 -1
- package/src/projects/api-schemas.ts +1 -0
- package/src/reference/_generated/contracts.md +116 -8
- package/src/reference/glossary.md +25 -4
- package/src/requests/__tests__/api-schemas.test.ts +277 -0
- package/src/requests/api-schemas.ts +83 -0
- package/src/requests/index.ts +1 -0
- package/src/scaffold-registry/__tests__/schema.test.ts +280 -0
- package/src/scaffold-registry/index.ts +194 -0
- package/src/scaffold-registry/schema.ts +144 -0
- package/src/supabase/database.types.ts +158 -6
- package/src/organization-model/domains/crm.ts +0 -46
- /package/src/business/{delivery → projects}/index.ts +0 -0
- /package/src/business/{delivery → projects}/types.ts +0 -0
- /package/src/business/{crm → sales}/api-schemas.ts +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
OfferingsDomainSchema,
|
|
4
|
+
ProductSchema,
|
|
5
|
+
PricingModelSchema,
|
|
6
|
+
DEFAULT_ORGANIZATION_MODEL_OFFERINGS
|
|
7
|
+
} from '../../domains/offerings'
|
|
8
|
+
import { resolveOrganizationModel } from '../../resolve'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Group 1: ProductSchema — positive parse
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
describe('ProductSchema — positive parse', () => {
|
|
15
|
+
it('accepts a fully-populated product', () => {
|
|
16
|
+
const result = ProductSchema.safeParse({
|
|
17
|
+
id: 'product-starter-plan',
|
|
18
|
+
name: 'Starter Plan',
|
|
19
|
+
description: 'Full-service automation for small teams.',
|
|
20
|
+
pricingModel: 'subscription',
|
|
21
|
+
price: 299,
|
|
22
|
+
currency: 'USD',
|
|
23
|
+
targetSegmentIds: ['seg-smb'],
|
|
24
|
+
deliveryFeatureId: 'crm'
|
|
25
|
+
})
|
|
26
|
+
expect(result.success).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('accepts a minimal product — only id required', () => {
|
|
30
|
+
const result = ProductSchema.safeParse({ id: 'product-minimal' })
|
|
31
|
+
expect(result.success).toBe(true)
|
|
32
|
+
if (result.success) {
|
|
33
|
+
expect(result.data.id).toBe('product-minimal')
|
|
34
|
+
expect(result.data.name).toBe('')
|
|
35
|
+
expect(result.data.description).toBe('')
|
|
36
|
+
expect(result.data.pricingModel).toBe('custom')
|
|
37
|
+
expect(result.data.price).toBe(0)
|
|
38
|
+
expect(result.data.currency).toBe('USD')
|
|
39
|
+
expect(result.data.targetSegmentIds).toEqual([])
|
|
40
|
+
expect(result.data.deliveryFeatureId).toBeUndefined()
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('trims whitespace from id and name', () => {
|
|
45
|
+
const result = ProductSchema.safeParse({
|
|
46
|
+
id: ' product-trim ',
|
|
47
|
+
name: ' Trimmed Name '
|
|
48
|
+
})
|
|
49
|
+
expect(result.success).toBe(true)
|
|
50
|
+
if (result.success) {
|
|
51
|
+
expect(result.data.id).toBe('product-trim')
|
|
52
|
+
expect(result.data.name).toBe('Trimmed Name')
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('accepts price of 0 (free tier)', () => {
|
|
57
|
+
const result = ProductSchema.safeParse({
|
|
58
|
+
id: 'product-free',
|
|
59
|
+
pricingModel: 'subscription',
|
|
60
|
+
price: 0
|
|
61
|
+
})
|
|
62
|
+
expect(result.success).toBe(true)
|
|
63
|
+
if (result.success) {
|
|
64
|
+
expect(result.data.price).toBe(0)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('accepts all four pricing model values', () => {
|
|
69
|
+
const models = ['one-time', 'subscription', 'usage-based', 'custom'] as const
|
|
70
|
+
for (const pricingModel of models) {
|
|
71
|
+
const result = ProductSchema.safeParse({ id: `product-${pricingModel}`, pricingModel })
|
|
72
|
+
expect(result.success).toBe(true)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Group 2: ProductSchema — default values
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
describe('ProductSchema — default values', () => {
|
|
82
|
+
it('pricingModel defaults to "custom"', () => {
|
|
83
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults' })
|
|
84
|
+
expect(result.success).toBe(true)
|
|
85
|
+
if (result.success) {
|
|
86
|
+
expect(result.data.pricingModel).toBe('custom')
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('price defaults to 0', () => {
|
|
91
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults' })
|
|
92
|
+
expect(result.success).toBe(true)
|
|
93
|
+
if (result.success) {
|
|
94
|
+
expect(result.data.price).toBe(0)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('currency defaults to "USD"', () => {
|
|
99
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults' })
|
|
100
|
+
expect(result.success).toBe(true)
|
|
101
|
+
if (result.success) {
|
|
102
|
+
expect(result.data.currency).toBe('USD')
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('targetSegmentIds defaults to empty array', () => {
|
|
107
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults' })
|
|
108
|
+
expect(result.success).toBe(true)
|
|
109
|
+
if (result.success) {
|
|
110
|
+
expect(result.data.targetSegmentIds).toEqual([])
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('deliveryFeatureId defaults to undefined (field is optional)', () => {
|
|
115
|
+
const result = ProductSchema.safeParse({ id: 'product-defaults' })
|
|
116
|
+
expect(result.success).toBe(true)
|
|
117
|
+
if (result.success) {
|
|
118
|
+
expect(result.data.deliveryFeatureId).toBeUndefined()
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Group 3: ProductSchema — negative parse (wrong types / constraints)
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
describe('ProductSchema — negative parse', () => {
|
|
128
|
+
it('rejects missing id', () => {
|
|
129
|
+
const result = ProductSchema.safeParse({ name: 'No ID Product' })
|
|
130
|
+
expect(result.success).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('rejects empty string id', () => {
|
|
134
|
+
const result = ProductSchema.safeParse({ id: '' })
|
|
135
|
+
expect(result.success).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('rejects whitespace-only id (trims to empty string)', () => {
|
|
139
|
+
const result = ProductSchema.safeParse({ id: ' ' })
|
|
140
|
+
expect(result.success).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('rejects an unknown pricingModel value', () => {
|
|
144
|
+
const result = ProductSchema.safeParse({ id: 'product-bad-model', pricingModel: 'freemium' })
|
|
145
|
+
expect(result.success).toBe(false)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('rejects price below 0', () => {
|
|
149
|
+
const result = ProductSchema.safeParse({ id: 'product-negative-price', price: -1 })
|
|
150
|
+
expect(result.success).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('rejects price as a string', () => {
|
|
154
|
+
const result = ProductSchema.safeParse({ id: 'product-string-price', price: '299' })
|
|
155
|
+
expect(result.success).toBe(false)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('rejects targetSegmentIds as a non-array value', () => {
|
|
159
|
+
const result = ProductSchema.safeParse({ id: 'product-bad-segments', targetSegmentIds: 'seg-a' })
|
|
160
|
+
expect(result.success).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Group 4: PricingModelSchema — enum coverage
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
describe('PricingModelSchema — enum', () => {
|
|
169
|
+
it('has exactly four values', () => {
|
|
170
|
+
const result = PricingModelSchema.safeParse('one-time')
|
|
171
|
+
expect(result.success).toBe(true)
|
|
172
|
+
// Verify the full enum options list matches expected set
|
|
173
|
+
expect(PricingModelSchema.options).toEqual(['one-time', 'subscription', 'usage-based', 'custom'])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('rejects an empty string', () => {
|
|
177
|
+
const result = PricingModelSchema.safeParse('')
|
|
178
|
+
expect(result.success).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('rejects a numeric value', () => {
|
|
182
|
+
const result = PricingModelSchema.safeParse(42)
|
|
183
|
+
expect(result.success).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Group 5: OfferingsDomainSchema — structural
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
describe('OfferingsDomainSchema — structural', () => {
|
|
192
|
+
it('accepts an empty products array', () => {
|
|
193
|
+
const result = OfferingsDomainSchema.safeParse({ products: [] })
|
|
194
|
+
expect(result.success).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('defaults products to empty array when key is omitted', () => {
|
|
198
|
+
const result = OfferingsDomainSchema.safeParse({})
|
|
199
|
+
expect(result.success).toBe(true)
|
|
200
|
+
if (result.success) {
|
|
201
|
+
expect(result.data.products).toEqual([])
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('accepts multiple valid products', () => {
|
|
206
|
+
const result = OfferingsDomainSchema.safeParse({
|
|
207
|
+
products: [
|
|
208
|
+
{ id: 'product-a', name: 'Product A', pricingModel: 'subscription' },
|
|
209
|
+
{ id: 'product-b', name: 'Product B', pricingModel: 'one-time', price: 999 }
|
|
210
|
+
]
|
|
211
|
+
})
|
|
212
|
+
expect(result.success).toBe(true)
|
|
213
|
+
if (result.success) {
|
|
214
|
+
expect(result.data.products).toHaveLength(2)
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('rejects products as a non-array value', () => {
|
|
219
|
+
const result = OfferingsDomainSchema.safeParse({ products: 'not an array' })
|
|
220
|
+
expect(result.success).toBe(false)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('DEFAULT_ORGANIZATION_MODEL_OFFERINGS constant matches schema parse of empty object', () => {
|
|
224
|
+
const result = OfferingsDomainSchema.safeParse({})
|
|
225
|
+
expect(result.success).toBe(true)
|
|
226
|
+
if (result.success) {
|
|
227
|
+
expect(result.data).toEqual(DEFAULT_ORGANIZATION_MODEL_OFFERINGS)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Group 6: Cross-ref validation via resolveOrganizationModel
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
describe('resolveOrganizationModel — offerings cross-ref (targetSegmentIds)', () => {
|
|
237
|
+
it('passes when targetSegmentIds references a declared customer segment', () => {
|
|
238
|
+
expect(() =>
|
|
239
|
+
resolveOrganizationModel({
|
|
240
|
+
customers: {
|
|
241
|
+
segments: [{ id: 'seg-agencies', name: 'SMB Agencies' }]
|
|
242
|
+
},
|
|
243
|
+
offerings: {
|
|
244
|
+
products: [
|
|
245
|
+
{
|
|
246
|
+
id: 'product-starter',
|
|
247
|
+
name: 'Starter Plan',
|
|
248
|
+
pricingModel: 'subscription',
|
|
249
|
+
targetSegmentIds: ['seg-agencies']
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
).not.toThrow()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('throws when targetSegmentIds references a non-existent segment', () => {
|
|
258
|
+
expect(() =>
|
|
259
|
+
resolveOrganizationModel({
|
|
260
|
+
customers: { segments: [] },
|
|
261
|
+
offerings: {
|
|
262
|
+
products: [
|
|
263
|
+
{
|
|
264
|
+
id: 'product-bad-ref',
|
|
265
|
+
targetSegmentIds: ['nonexistent-segment']
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
).toThrow()
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('throws with a message referencing the unknown segment id', () => {
|
|
274
|
+
let errorMessage = ''
|
|
275
|
+
try {
|
|
276
|
+
resolveOrganizationModel({
|
|
277
|
+
customers: { segments: [] },
|
|
278
|
+
offerings: {
|
|
279
|
+
products: [
|
|
280
|
+
{
|
|
281
|
+
id: 'product-path-check',
|
|
282
|
+
targetSegmentIds: ['ghost-segment']
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
} catch (e) {
|
|
288
|
+
errorMessage = String(e)
|
|
289
|
+
}
|
|
290
|
+
expect(errorMessage).toContain('ghost-segment')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('passes when targetSegmentIds is empty (no refs required)', () => {
|
|
294
|
+
expect(() =>
|
|
295
|
+
resolveOrganizationModel({
|
|
296
|
+
offerings: {
|
|
297
|
+
products: [{ id: 'product-no-targets', targetSegmentIds: [] }]
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
).not.toThrow()
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('passes when multiple products each reference a valid segment', () => {
|
|
304
|
+
expect(() =>
|
|
305
|
+
resolveOrganizationModel({
|
|
306
|
+
customers: {
|
|
307
|
+
segments: [
|
|
308
|
+
{ id: 'seg-a', name: 'Segment A' },
|
|
309
|
+
{ id: 'seg-b', name: 'Segment B' }
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
offerings: {
|
|
313
|
+
products: [
|
|
314
|
+
{ id: 'product-1', targetSegmentIds: ['seg-a'] },
|
|
315
|
+
{ id: 'product-2', targetSegmentIds: ['seg-a', 'seg-b'] }
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
).not.toThrow()
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Group 7: Cross-ref validation — deliveryFeatureId
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
describe('resolveOrganizationModel — offerings cross-ref (deliveryFeatureId)', () => {
|
|
328
|
+
it('passes when deliveryFeatureId references a declared feature', () => {
|
|
329
|
+
// 'crm' is in DEFAULT_ORGANIZATION_MODEL.features
|
|
330
|
+
expect(() =>
|
|
331
|
+
resolveOrganizationModel({
|
|
332
|
+
offerings: {
|
|
333
|
+
products: [
|
|
334
|
+
{
|
|
335
|
+
id: 'product-with-delivery',
|
|
336
|
+
deliveryFeatureId: 'crm'
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
).not.toThrow()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('throws when deliveryFeatureId references a non-existent feature', () => {
|
|
345
|
+
expect(() =>
|
|
346
|
+
resolveOrganizationModel({
|
|
347
|
+
offerings: {
|
|
348
|
+
products: [
|
|
349
|
+
{
|
|
350
|
+
id: 'product-bad-delivery',
|
|
351
|
+
deliveryFeatureId: 'nonexistent-feature'
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
).toThrow()
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('throws with a message referencing the unknown feature id', () => {
|
|
360
|
+
let errorMessage = ''
|
|
361
|
+
try {
|
|
362
|
+
resolveOrganizationModel({
|
|
363
|
+
offerings: {
|
|
364
|
+
products: [
|
|
365
|
+
{
|
|
366
|
+
id: 'product-delivery-path',
|
|
367
|
+
deliveryFeatureId: 'ghost-feature'
|
|
368
|
+
}
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
} catch (e) {
|
|
373
|
+
errorMessage = String(e)
|
|
374
|
+
}
|
|
375
|
+
expect(errorMessage).toContain('ghost-feature')
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('passes when deliveryFeatureId is absent (field is optional)', () => {
|
|
379
|
+
expect(() =>
|
|
380
|
+
resolveOrganizationModel({
|
|
381
|
+
offerings: {
|
|
382
|
+
products: [{ id: 'product-no-delivery' }]
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
).not.toThrow()
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Group 8: Integration — resolveOrganizationModel general
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
describe('resolveOrganizationModel — offerings domain integration', () => {
|
|
394
|
+
it('merges partial offerings override and preserves empty products default', () => {
|
|
395
|
+
const model = resolveOrganizationModel({ offerings: { products: [] } })
|
|
396
|
+
expect(model.offerings.products).toEqual([])
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('omitting offerings key entirely resolves to default empty products', () => {
|
|
400
|
+
const model = resolveOrganizationModel({})
|
|
401
|
+
expect(model.offerings).toBeDefined()
|
|
402
|
+
expect(model.offerings.products).toEqual([])
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('does not bleed offerings changes into other top-level domains', () => {
|
|
406
|
+
const model = resolveOrganizationModel({
|
|
407
|
+
customers: {
|
|
408
|
+
segments: [{ id: 'seg-isolation' }]
|
|
409
|
+
},
|
|
410
|
+
offerings: {
|
|
411
|
+
products: [{ id: 'product-isolation', targetSegmentIds: ['seg-isolation'] }]
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
expect(model.identity).toBeDefined()
|
|
415
|
+
expect(model.customers).toBeDefined()
|
|
416
|
+
expect(model.features).toBeDefined()
|
|
417
|
+
expect(model.navigation).toBeDefined()
|
|
418
|
+
})
|
|
419
|
+
})
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ORGANIZATION_MODEL_OPERATIONS,
|
|
4
|
+
OperationEntrySchema,
|
|
5
|
+
OperationsDomainSchema,
|
|
6
|
+
OperationSemanticClassSchema
|
|
7
|
+
} from '../../domains/operations'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Group 1: OperationEntrySchema — positive parse
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
describe('OperationEntrySchema — positive parse', () => {
|
|
14
|
+
it('accepts a fully-specified entry (id, label, semanticClass, featureId, supportedStatusSemanticClass)', () => {
|
|
15
|
+
const result = OperationEntrySchema.safeParse({
|
|
16
|
+
id: 'operations.queue',
|
|
17
|
+
label: 'HITL Queue',
|
|
18
|
+
semanticClass: 'queue',
|
|
19
|
+
featureId: 'operations',
|
|
20
|
+
supportedStatusSemanticClass: ['queue']
|
|
21
|
+
})
|
|
22
|
+
expect(result.success).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('accepts a minimal entry without optional fields', () => {
|
|
26
|
+
const result = OperationEntrySchema.safeParse({
|
|
27
|
+
id: 'operations.sessions',
|
|
28
|
+
label: 'Sessions',
|
|
29
|
+
semanticClass: 'sessions'
|
|
30
|
+
})
|
|
31
|
+
expect(result.success).toBe(true)
|
|
32
|
+
if (result.success) {
|
|
33
|
+
expect(result.data.featureId).toBeUndefined()
|
|
34
|
+
expect(result.data.supportedStatusSemanticClass).toBeUndefined()
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('trims whitespace from id and label', () => {
|
|
39
|
+
const result = OperationEntrySchema.safeParse({
|
|
40
|
+
id: ' trimmed-id ',
|
|
41
|
+
label: ' Trimmed Label ',
|
|
42
|
+
semanticClass: 'executions'
|
|
43
|
+
})
|
|
44
|
+
expect(result.success).toBe(true)
|
|
45
|
+
if (result.success) {
|
|
46
|
+
expect(result.data.id).toBe('trimmed-id')
|
|
47
|
+
expect(result.data.label).toBe('Trimmed Label')
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Group 2: OperationEntrySchema — negative parse
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
describe('OperationEntrySchema — negative parse', () => {
|
|
57
|
+
it('rejects a missing id field', () => {
|
|
58
|
+
const result = OperationEntrySchema.safeParse({
|
|
59
|
+
label: 'No ID',
|
|
60
|
+
semanticClass: 'queue'
|
|
61
|
+
})
|
|
62
|
+
expect(result.success).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('rejects a missing label field', () => {
|
|
66
|
+
const result = OperationEntrySchema.safeParse({
|
|
67
|
+
id: 'operations.queue',
|
|
68
|
+
semanticClass: 'queue'
|
|
69
|
+
})
|
|
70
|
+
expect(result.success).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('rejects a missing semanticClass field', () => {
|
|
74
|
+
const result = OperationEntrySchema.safeParse({
|
|
75
|
+
id: 'operations.queue',
|
|
76
|
+
label: 'HITL Queue'
|
|
77
|
+
})
|
|
78
|
+
expect(result.success).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('rejects an unknown semanticClass value', () => {
|
|
82
|
+
const result = OperationEntrySchema.safeParse({
|
|
83
|
+
id: 'some.entry',
|
|
84
|
+
label: 'Some Entry',
|
|
85
|
+
semanticClass: 'not-a-real-class'
|
|
86
|
+
})
|
|
87
|
+
expect(result.success).toBe(false)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('rejects a non-string id (number)', () => {
|
|
91
|
+
const result = OperationEntrySchema.safeParse({
|
|
92
|
+
id: 42,
|
|
93
|
+
label: 'Label',
|
|
94
|
+
semanticClass: 'queue'
|
|
95
|
+
})
|
|
96
|
+
expect(result.success).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('rejects an empty string label (min length 1 after trim)', () => {
|
|
100
|
+
const result = OperationEntrySchema.safeParse({
|
|
101
|
+
id: 'some.entry',
|
|
102
|
+
label: ' ',
|
|
103
|
+
semanticClass: 'queue'
|
|
104
|
+
})
|
|
105
|
+
expect(result.success).toBe(false)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Group 3: OperationsDomainSchema — structural tests
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
describe('OperationsDomainSchema — structural tests', () => {
|
|
114
|
+
it('accepts an entries-empty domain object', () => {
|
|
115
|
+
const result = OperationsDomainSchema.safeParse({ entries: [] })
|
|
116
|
+
expect(result.success).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('applies an empty-array default when entries is omitted', () => {
|
|
120
|
+
const result = OperationsDomainSchema.safeParse({})
|
|
121
|
+
expect(result.success).toBe(true)
|
|
122
|
+
if (result.success) {
|
|
123
|
+
expect(result.data.entries).toEqual([])
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Group 4: Semantic class enum — all 5 values declared
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
describe('OperationSemanticClassSchema — enum coverage', () => {
|
|
133
|
+
const expectedClasses = ['queue', 'executions', 'sessions', 'notifications', 'schedules'] as const
|
|
134
|
+
|
|
135
|
+
it('enum exposes exactly 5 semantic classes', () => {
|
|
136
|
+
expect(OperationSemanticClassSchema.options).toHaveLength(5)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it.each(expectedClasses)('"%s" is a valid semanticClass value', (cls) => {
|
|
140
|
+
const result = OperationSemanticClassSchema.safeParse(cls)
|
|
141
|
+
expect(result.success).toBe(true)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('every entry in the seed uses one of the 5 known semanticClass values', () => {
|
|
145
|
+
const valid = new Set<string>(OperationSemanticClassSchema.options)
|
|
146
|
+
for (const entry of DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries) {
|
|
147
|
+
expect(
|
|
148
|
+
valid.has(entry.semanticClass),
|
|
149
|
+
`Unexpected semanticClass "${entry.semanticClass}" on entry "${entry.id}"`
|
|
150
|
+
).toBe(true)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('all 5 semanticClass values have exactly one seed entry', () => {
|
|
155
|
+
const presentClasses = new Set(DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries.map((e) => e.semanticClass))
|
|
156
|
+
for (const cls of OperationSemanticClassSchema.options) {
|
|
157
|
+
expect(presentClasses.has(cls), `No seed entry found for semanticClass "${cls}"`).toBe(true)
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Group 5: Seed completeness
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
describe('DEFAULT_ORGANIZATION_MODEL_OPERATIONS seed — completeness', () => {
|
|
167
|
+
it('has exactly 5 entries (one per entity category)', () => {
|
|
168
|
+
expect(DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries).toHaveLength(5)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('all entry ids are unique', () => {
|
|
172
|
+
const ids = DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries.map((e) => e.id)
|
|
173
|
+
const uniqueIds = new Set(ids)
|
|
174
|
+
expect(uniqueIds.size).toBe(ids.length)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('every entry has a non-empty id, label, and semanticClass', () => {
|
|
178
|
+
for (const entry of DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries) {
|
|
179
|
+
expect(entry.id.length).toBeGreaterThan(0)
|
|
180
|
+
expect(entry.label.length).toBeGreaterThan(0)
|
|
181
|
+
expect(entry.semanticClass.length).toBeGreaterThan(0)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('queue entry references queue status semantic class', () => {
|
|
186
|
+
const queueEntry = DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries.find((e) => e.semanticClass === 'queue')
|
|
187
|
+
expect(queueEntry).toBeDefined()
|
|
188
|
+
expect(queueEntry?.supportedStatusSemanticClass).toContain('queue')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('executions entry references execution status semantic class', () => {
|
|
192
|
+
const execEntry = DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries.find((e) => e.semanticClass === 'executions')
|
|
193
|
+
expect(execEntry).toBeDefined()
|
|
194
|
+
expect(execEntry?.supportedStatusSemanticClass).toContain('execution')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('schedules entry references schedule and schedule.run status semantic classes', () => {
|
|
198
|
+
const schedEntry = DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries.find((e) => e.semanticClass === 'schedules')
|
|
199
|
+
expect(schedEntry).toBeDefined()
|
|
200
|
+
expect(schedEntry?.supportedStatusSemanticClass).toContain('schedule')
|
|
201
|
+
expect(schedEntry?.supportedStatusSemanticClass).toContain('schedule.run')
|
|
202
|
+
})
|
|
203
|
+
})
|