digital-products 2.1.3 → 2.3.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/CHANGELOG.md +9 -0
- package/README.md +2 -0
- package/dist/api.js +7 -7
- package/dist/api.js.map +1 -1
- package/dist/app.js +6 -6
- package/dist/app.js.map +1 -1
- package/dist/client.d.ts +157 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +69 -0
- package/dist/client.js.map +1 -0
- package/dist/content.js +7 -7
- package/dist/content.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +6 -6
- package/dist/data.js.map +1 -1
- package/dist/dataset.js +5 -5
- package/dist/dataset.js.map +1 -1
- package/dist/index.d.ts +92 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +139 -15
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +17 -10
- package/dist/mcp.js.map +1 -1
- package/dist/product.js +2 -2
- package/dist/product.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +52 -16
- package/dist/sdk.js.map +1 -1
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +12 -8
- package/dist/site.js.map +1 -1
- package/dist/types.d.ts +830 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +495 -2
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts +205 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +356 -0
- package/dist/worker.js.map +1 -0
- package/package.json +29 -13
- package/src/api.ts +7 -7
- package/src/app.ts +6 -6
- package/src/client.ts +192 -0
- package/src/content.ts +7 -7
- package/src/data.ts +12 -7
- package/src/dataset.ts +5 -5
- package/src/index.ts +151 -15
- package/src/mcp.ts +18 -11
- package/src/product.ts +2 -2
- package/src/sdk.ts +54 -15
- package/src/site.ts +12 -8
- package/src/types.ts +821 -12
- package/src/worker.ts +525 -0
- package/test/product.test.ts +53 -198
- package/test/unified-types.test.ts +589 -0
- package/test/worker.test.ts +912 -0
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/.turbo/turbo-build.log +0 -5
- package/LICENSE +0 -21
- package/dist/features/define.d.ts +0 -63
- package/dist/features/define.d.ts.map +0 -1
- package/dist/features/define.js +0 -72
- package/dist/features/define.js.map +0 -1
- package/dist/features/flags.d.ts +0 -98
- package/dist/features/flags.d.ts.map +0 -1
- package/dist/features/flags.js +0 -145
- package/dist/features/flags.js.map +0 -1
- package/dist/features/toggles.d.ts +0 -75
- package/dist/features/toggles.d.ts.map +0 -1
- package/dist/features/toggles.js +0 -107
- package/dist/features/toggles.js.map +0 -1
- package/dist/tiers/define.d.ts +0 -63
- package/dist/tiers/define.d.ts.map +0 -1
- package/dist/tiers/define.js +0 -78
- package/dist/tiers/define.js.map +0 -1
- package/dist/tiers/entitlements.d.ts +0 -94
- package/dist/tiers/entitlements.d.ts.map +0 -1
- package/dist/tiers/entitlements.js +0 -94
- package/dist/tiers/entitlements.js.map +0 -1
- package/src/api.js +0 -128
- package/src/app.js +0 -106
- package/src/content.js +0 -77
- package/src/data.js +0 -106
- package/src/dataset.js +0 -49
- package/src/entities/ai.js +0 -858
- package/src/entities/content.js +0 -783
- package/src/entities/index.js +0 -88
- package/src/entities/interfaces.js +0 -929
- package/src/entities/lifecycle.js +0 -803
- package/src/entities/products.js +0 -797
- package/src/entities/web.js +0 -657
- package/src/features/define.ts +0 -130
- package/src/features/flags.ts +0 -247
- package/src/features/toggles.ts +0 -189
- package/src/index.js +0 -35
- package/src/mcp.js +0 -139
- package/src/pricing/billing.ts +0 -386
- package/src/pricing/plans.ts +0 -214
- package/src/product.js +0 -53
- package/src/registry.js +0 -31
- package/src/sdk.js +0 -127
- package/src/site.js +0 -112
- package/src/tiers/define.ts +0 -137
- package/src/tiers/entitlements.ts +0 -201
- package/src/types.js +0 -4
- package/test/analytics/events.test.ts +0 -319
- package/test/analytics/experiments.test.ts +0 -327
- package/test/features/define.test.ts +0 -187
- package/test/features/flags.test.ts +0 -259
- package/test/features/toggles.test.ts +0 -178
- package/test/lifecycle/stages.test.ts +0 -233
- package/test/lifecycle/transitions.test.ts +0 -207
- package/test/onboarding/flows.test.ts +0 -307
- package/test/pricing/billing.test.ts +0 -287
- package/test/pricing/plans.test.ts +0 -307
- package/test/roadmap/milestones.test.ts +0 -231
- package/test/roadmap/priorities.test.ts +0 -239
- package/test/tiers/define.test.ts +0 -192
- package/test/tiers/entitlements.test.ts +0 -220
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for A/B Experiments Integration
|
|
3
|
-
* Phase 1: RED - These tests should FAIL initially
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
ProductExperiment,
|
|
9
|
-
createProductExperiment,
|
|
10
|
-
ExperimentManager,
|
|
11
|
-
createExperimentManager,
|
|
12
|
-
ExperimentVariant,
|
|
13
|
-
ExperimentAssignment,
|
|
14
|
-
} from '../../src/analytics/experiments.js'
|
|
15
|
-
|
|
16
|
-
describe('Product Experiments', () => {
|
|
17
|
-
describe('ProductExperiment', () => {
|
|
18
|
-
it('creates an A/B experiment', () => {
|
|
19
|
-
const experiment = createProductExperiment({
|
|
20
|
-
id: 'checkout-flow',
|
|
21
|
-
name: 'Checkout Flow Test',
|
|
22
|
-
description: 'Testing new checkout flow',
|
|
23
|
-
variants: [
|
|
24
|
-
{ id: 'control', name: 'Original', weight: 50 },
|
|
25
|
-
{ id: 'treatment', name: 'New Flow', weight: 50 },
|
|
26
|
-
],
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
expect(experiment.id).toBe('checkout-flow')
|
|
30
|
-
expect(experiment.variants).toHaveLength(2)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('creates a multivariate experiment', () => {
|
|
34
|
-
const experiment = createProductExperiment({
|
|
35
|
-
id: 'cta-test',
|
|
36
|
-
name: 'CTA Button Test',
|
|
37
|
-
variants: [
|
|
38
|
-
{ id: 'a', name: 'Get Started', weight: 33 },
|
|
39
|
-
{ id: 'b', name: 'Try Free', weight: 33 },
|
|
40
|
-
{ id: 'c', name: 'Sign Up', weight: 34 },
|
|
41
|
-
],
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
expect(experiment.variants).toHaveLength(3)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('validates variant weights sum to 100', () => {
|
|
48
|
-
expect(() =>
|
|
49
|
-
createProductExperiment({
|
|
50
|
-
id: 'invalid',
|
|
51
|
-
name: 'Invalid',
|
|
52
|
-
variants: [
|
|
53
|
-
{ id: 'a', name: 'A', weight: 30 },
|
|
54
|
-
{ id: 'b', name: 'B', weight: 30 },
|
|
55
|
-
],
|
|
56
|
-
})
|
|
57
|
-
).toThrow('Variant weights must sum to 100')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('supports targeting rules', () => {
|
|
61
|
-
const experiment = createProductExperiment({
|
|
62
|
-
id: 'targeted',
|
|
63
|
-
name: 'Targeted Experiment',
|
|
64
|
-
variants: [
|
|
65
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
66
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
67
|
-
],
|
|
68
|
-
targeting: {
|
|
69
|
-
tiers: ['pro', 'enterprise'],
|
|
70
|
-
countries: ['US', 'CA', 'UK'],
|
|
71
|
-
},
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
expect(experiment.targeting?.tiers).toContain('pro')
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('supports date ranges', () => {
|
|
78
|
-
const experiment = createProductExperiment({
|
|
79
|
-
id: 'time-bound',
|
|
80
|
-
name: 'Time Bound Test',
|
|
81
|
-
variants: [
|
|
82
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
83
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
84
|
-
],
|
|
85
|
-
startDate: new Date('2024-01-01'),
|
|
86
|
-
endDate: new Date('2024-03-01'),
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
expect(experiment.startDate).toBeDefined()
|
|
90
|
-
expect(experiment.endDate).toBeDefined()
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('supports goal metrics', () => {
|
|
94
|
-
const experiment = createProductExperiment({
|
|
95
|
-
id: 'with-goals',
|
|
96
|
-
name: 'Conversion Test',
|
|
97
|
-
variants: [
|
|
98
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
99
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
100
|
-
],
|
|
101
|
-
goals: [
|
|
102
|
-
{ event: 'signup.completed', type: 'conversion' },
|
|
103
|
-
{ event: 'subscription.created', type: 'conversion' },
|
|
104
|
-
{ event: 'time.on.page', type: 'duration' },
|
|
105
|
-
],
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
expect(experiment.goals).toHaveLength(3)
|
|
109
|
-
})
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
describe('ExperimentManager', () => {
|
|
113
|
-
let manager: ExperimentManager
|
|
114
|
-
|
|
115
|
-
beforeEach(() => {
|
|
116
|
-
manager = createExperimentManager()
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('registers experiments', () => {
|
|
120
|
-
const experiment = createProductExperiment({
|
|
121
|
-
id: 'test-exp',
|
|
122
|
-
name: 'Test',
|
|
123
|
-
variants: [
|
|
124
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
125
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
126
|
-
],
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
manager.register(experiment)
|
|
130
|
-
expect(manager.get('test-exp')).toEqual(experiment)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('assigns users to variants', () => {
|
|
134
|
-
manager.register(
|
|
135
|
-
createProductExperiment({
|
|
136
|
-
id: 'assignment-test',
|
|
137
|
-
name: 'Assignment Test',
|
|
138
|
-
variants: [
|
|
139
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
140
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
141
|
-
],
|
|
142
|
-
})
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
const assignment = manager.assign('assignment-test', 'user-123')
|
|
146
|
-
expect(['control', 'treatment']).toContain(assignment.variantId)
|
|
147
|
-
expect(assignment.userId).toBe('user-123')
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('returns consistent assignments for same user', () => {
|
|
151
|
-
manager.register(
|
|
152
|
-
createProductExperiment({
|
|
153
|
-
id: 'sticky-test',
|
|
154
|
-
name: 'Sticky Test',
|
|
155
|
-
variants: [
|
|
156
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
157
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
158
|
-
],
|
|
159
|
-
})
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
const first = manager.assign('sticky-test', 'user-456')
|
|
163
|
-
const second = manager.assign('sticky-test', 'user-456')
|
|
164
|
-
const third = manager.assign('sticky-test', 'user-456')
|
|
165
|
-
|
|
166
|
-
expect(first.variantId).toBe(second.variantId)
|
|
167
|
-
expect(second.variantId).toBe(third.variantId)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('respects targeting rules', () => {
|
|
171
|
-
manager.register(
|
|
172
|
-
createProductExperiment({
|
|
173
|
-
id: 'targeted-exp',
|
|
174
|
-
name: 'Targeted',
|
|
175
|
-
variants: [
|
|
176
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
177
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
178
|
-
],
|
|
179
|
-
targeting: {
|
|
180
|
-
tiers: ['enterprise'],
|
|
181
|
-
},
|
|
182
|
-
})
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
const freeUserAssignment = manager.assign('targeted-exp', 'free-user', {
|
|
186
|
-
tier: 'free',
|
|
187
|
-
})
|
|
188
|
-
expect(freeUserAssignment.variantId).toBeNull() // Not eligible
|
|
189
|
-
|
|
190
|
-
const enterpriseAssignment = manager.assign('targeted-exp', 'enterprise-user', {
|
|
191
|
-
tier: 'enterprise',
|
|
192
|
-
})
|
|
193
|
-
expect(enterpriseAssignment.variantId).not.toBeNull()
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('respects date ranges', () => {
|
|
197
|
-
manager.register(
|
|
198
|
-
createProductExperiment({
|
|
199
|
-
id: 'date-exp',
|
|
200
|
-
name: 'Date Test',
|
|
201
|
-
variants: [
|
|
202
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
203
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
204
|
-
],
|
|
205
|
-
startDate: new Date('2020-01-01'),
|
|
206
|
-
endDate: new Date('2020-12-31'),
|
|
207
|
-
})
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
const assignment = manager.assign('date-exp', 'user-789')
|
|
211
|
-
expect(assignment.variantId).toBeNull() // Experiment ended
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('tracks experiment events', () => {
|
|
215
|
-
manager.register(
|
|
216
|
-
createProductExperiment({
|
|
217
|
-
id: 'tracked-exp',
|
|
218
|
-
name: 'Tracked',
|
|
219
|
-
variants: [
|
|
220
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
221
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
222
|
-
],
|
|
223
|
-
goals: [{ event: 'purchase.completed', type: 'conversion' }],
|
|
224
|
-
})
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
manager.assign('tracked-exp', 'user-track')
|
|
228
|
-
manager.trackEvent('tracked-exp', 'user-track', 'purchase.completed', {
|
|
229
|
-
amount: 99,
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
const results = manager.getResults('tracked-exp')
|
|
233
|
-
expect(results.events).toHaveLength(1)
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('calculates experiment results', () => {
|
|
237
|
-
manager.register(
|
|
238
|
-
createProductExperiment({
|
|
239
|
-
id: 'results-exp',
|
|
240
|
-
name: 'Results Test',
|
|
241
|
-
variants: [
|
|
242
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
243
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
244
|
-
],
|
|
245
|
-
goals: [{ event: 'conversion', type: 'conversion' }],
|
|
246
|
-
})
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
// Simulate some assignments and events
|
|
250
|
-
for (let i = 0; i < 100; i++) {
|
|
251
|
-
manager.assign('results-exp', `user-${i}`)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const results = manager.getResults('results-exp')
|
|
255
|
-
expect(results.totalAssignments).toBe(100)
|
|
256
|
-
expect(results.variantStats.control).toBeDefined()
|
|
257
|
-
expect(results.variantStats.treatment).toBeDefined()
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
it('supports experiment overrides', () => {
|
|
261
|
-
manager.register(
|
|
262
|
-
createProductExperiment({
|
|
263
|
-
id: 'override-exp',
|
|
264
|
-
name: 'Override Test',
|
|
265
|
-
variants: [
|
|
266
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
267
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
268
|
-
],
|
|
269
|
-
})
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
manager.setOverride('override-exp', 'treatment')
|
|
273
|
-
const assignment = manager.assign('override-exp', 'any-user')
|
|
274
|
-
expect(assignment.variantId).toBe('treatment')
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
it('pauses and resumes experiments', () => {
|
|
278
|
-
manager.register(
|
|
279
|
-
createProductExperiment({
|
|
280
|
-
id: 'pause-exp',
|
|
281
|
-
name: 'Pause Test',
|
|
282
|
-
variants: [
|
|
283
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
284
|
-
{ id: 'treatment', name: 'Treatment', weight: 50 },
|
|
285
|
-
],
|
|
286
|
-
})
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
manager.pause('pause-exp')
|
|
290
|
-
const paused = manager.assign('pause-exp', 'user-1')
|
|
291
|
-
expect(paused.variantId).toBeNull()
|
|
292
|
-
|
|
293
|
-
manager.resume('pause-exp')
|
|
294
|
-
const resumed = manager.assign('pause-exp', 'user-2')
|
|
295
|
-
expect(resumed.variantId).not.toBeNull()
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
it('lists active experiments', () => {
|
|
299
|
-
manager.register(
|
|
300
|
-
createProductExperiment({
|
|
301
|
-
id: 'active-1',
|
|
302
|
-
name: 'Active 1',
|
|
303
|
-
variants: [
|
|
304
|
-
{ id: 'a', name: 'A', weight: 50 },
|
|
305
|
-
{ id: 'b', name: 'B', weight: 50 },
|
|
306
|
-
],
|
|
307
|
-
})
|
|
308
|
-
)
|
|
309
|
-
manager.register(
|
|
310
|
-
createProductExperiment({
|
|
311
|
-
id: 'active-2',
|
|
312
|
-
name: 'Active 2',
|
|
313
|
-
variants: [
|
|
314
|
-
{ id: 'a', name: 'A', weight: 50 },
|
|
315
|
-
{ id: 'b', name: 'B', weight: 50 },
|
|
316
|
-
],
|
|
317
|
-
})
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
manager.pause('active-2')
|
|
321
|
-
|
|
322
|
-
const active = manager.listActive()
|
|
323
|
-
expect(active).toHaveLength(1)
|
|
324
|
-
expect(active[0].id).toBe('active-1')
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
})
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Feature definitions
|
|
3
|
-
* Phase 1: RED - These tests should FAIL initially
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
Feature,
|
|
9
|
-
defineFeature,
|
|
10
|
-
FeatureRegistry,
|
|
11
|
-
createFeatureRegistry,
|
|
12
|
-
} from '../../src/features/define.js'
|
|
13
|
-
|
|
14
|
-
describe('Feature Definition', () => {
|
|
15
|
-
describe('Feature()', () => {
|
|
16
|
-
it('creates a feature with required properties', () => {
|
|
17
|
-
const feature = Feature({
|
|
18
|
-
id: 'dark-mode',
|
|
19
|
-
name: 'Dark Mode',
|
|
20
|
-
description: 'Enable dark mode UI',
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
expect(feature.id).toBe('dark-mode')
|
|
24
|
-
expect(feature.name).toBe('Dark Mode')
|
|
25
|
-
expect(feature.description).toBe('Enable dark mode UI')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('supports feature categories', () => {
|
|
29
|
-
const feature = Feature({
|
|
30
|
-
id: 'analytics',
|
|
31
|
-
name: 'Analytics Dashboard',
|
|
32
|
-
description: 'View analytics',
|
|
33
|
-
category: 'reporting',
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
expect(feature.category).toBe('reporting')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('supports dependencies on other features', () => {
|
|
40
|
-
const feature = Feature({
|
|
41
|
-
id: 'advanced-charts',
|
|
42
|
-
name: 'Advanced Charts',
|
|
43
|
-
description: 'Premium chart types',
|
|
44
|
-
dependencies: ['analytics', 'data-export'],
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
expect(feature.dependencies).toEqual(['analytics', 'data-export'])
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('supports feature metadata', () => {
|
|
51
|
-
const feature = Feature({
|
|
52
|
-
id: 'ai-assistant',
|
|
53
|
-
name: 'AI Assistant',
|
|
54
|
-
description: 'AI-powered help',
|
|
55
|
-
metadata: {
|
|
56
|
-
releaseDate: '2024-01-15',
|
|
57
|
-
team: 'ai-core',
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
expect(feature.metadata?.releaseDate).toBe('2024-01-15')
|
|
62
|
-
expect(feature.metadata?.team).toBe('ai-core')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('defaults status to active', () => {
|
|
66
|
-
const feature = Feature({
|
|
67
|
-
id: 'test',
|
|
68
|
-
name: 'Test',
|
|
69
|
-
description: 'Test feature',
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
expect(feature.status).toBe('active')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('supports custom status', () => {
|
|
76
|
-
const feature = Feature({
|
|
77
|
-
id: 'beta',
|
|
78
|
-
name: 'Beta Feature',
|
|
79
|
-
description: 'In beta',
|
|
80
|
-
status: 'beta',
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
expect(feature.status).toBe('beta')
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('defineFeature()', () => {
|
|
88
|
-
it('creates and validates a feature definition', () => {
|
|
89
|
-
const feature = defineFeature({
|
|
90
|
-
id: 'notifications',
|
|
91
|
-
name: 'Notifications',
|
|
92
|
-
description: 'Push notifications',
|
|
93
|
-
version: '1.0.0',
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
expect(feature.id).toBe('notifications')
|
|
97
|
-
expect(feature.version).toBe('1.0.0')
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('throws for invalid feature id', () => {
|
|
101
|
-
expect(() =>
|
|
102
|
-
defineFeature({
|
|
103
|
-
id: '',
|
|
104
|
-
name: 'Invalid',
|
|
105
|
-
description: 'No ID',
|
|
106
|
-
})
|
|
107
|
-
).toThrow('Feature ID is required')
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
describe('FeatureRegistry', () => {
|
|
112
|
-
it('registers and retrieves features', () => {
|
|
113
|
-
const registry = createFeatureRegistry()
|
|
114
|
-
|
|
115
|
-
const feature = Feature({
|
|
116
|
-
id: 'storage',
|
|
117
|
-
name: 'Cloud Storage',
|
|
118
|
-
description: 'Store files',
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
registry.register(feature)
|
|
122
|
-
const retrieved = registry.get('storage')
|
|
123
|
-
|
|
124
|
-
expect(retrieved).toEqual(feature)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('lists all features', () => {
|
|
128
|
-
const registry = createFeatureRegistry()
|
|
129
|
-
|
|
130
|
-
registry.register(Feature({ id: 'f1', name: 'F1', description: 'D1' }))
|
|
131
|
-
registry.register(Feature({ id: 'f2', name: 'F2', description: 'D2' }))
|
|
132
|
-
|
|
133
|
-
expect(registry.list()).toHaveLength(2)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('filters features by category', () => {
|
|
137
|
-
const registry = createFeatureRegistry()
|
|
138
|
-
|
|
139
|
-
registry.register(
|
|
140
|
-
Feature({
|
|
141
|
-
id: 'chart',
|
|
142
|
-
name: 'Charts',
|
|
143
|
-
description: 'Charts',
|
|
144
|
-
category: 'reporting',
|
|
145
|
-
})
|
|
146
|
-
)
|
|
147
|
-
registry.register(
|
|
148
|
-
Feature({
|
|
149
|
-
id: 'auth',
|
|
150
|
-
name: 'Auth',
|
|
151
|
-
description: 'Auth',
|
|
152
|
-
category: 'security',
|
|
153
|
-
})
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
const reporting = registry.listByCategory('reporting')
|
|
157
|
-
expect(reporting).toHaveLength(1)
|
|
158
|
-
expect(reporting[0].id).toBe('chart')
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('returns feature dependency graph', () => {
|
|
162
|
-
const registry = createFeatureRegistry()
|
|
163
|
-
|
|
164
|
-
registry.register(Feature({ id: 'base', name: 'Base', description: 'Base' }))
|
|
165
|
-
registry.register(
|
|
166
|
-
Feature({
|
|
167
|
-
id: 'advanced',
|
|
168
|
-
name: 'Advanced',
|
|
169
|
-
description: 'Advanced',
|
|
170
|
-
dependencies: ['base'],
|
|
171
|
-
})
|
|
172
|
-
)
|
|
173
|
-
registry.register(
|
|
174
|
-
Feature({
|
|
175
|
-
id: 'premium',
|
|
176
|
-
name: 'Premium',
|
|
177
|
-
description: 'Premium',
|
|
178
|
-
dependencies: ['advanced'],
|
|
179
|
-
})
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
const deps = registry.getDependencyGraph('premium')
|
|
183
|
-
expect(deps).toContain('base')
|
|
184
|
-
expect(deps).toContain('advanced')
|
|
185
|
-
})
|
|
186
|
-
})
|
|
187
|
-
})
|