digital-products 2.1.3 → 2.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/.turbo/turbo-build.log +4 -5
- package/CHANGELOG.md +17 -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/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,259 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Feature Flags
|
|
3
|
-
* Phase 1: RED - These tests should FAIL initially
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
FeatureFlag,
|
|
9
|
-
createFeatureFlag,
|
|
10
|
-
FeatureFlagManager,
|
|
11
|
-
createFeatureFlagManager,
|
|
12
|
-
FlagEvaluationContext,
|
|
13
|
-
} from '../../src/features/flags.js'
|
|
14
|
-
|
|
15
|
-
describe('Feature Flags', () => {
|
|
16
|
-
describe('FeatureFlag()', () => {
|
|
17
|
-
it('creates a simple boolean flag', () => {
|
|
18
|
-
const flag = FeatureFlag({
|
|
19
|
-
key: 'new-dashboard',
|
|
20
|
-
name: 'New Dashboard',
|
|
21
|
-
type: 'boolean',
|
|
22
|
-
defaultValue: false,
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
expect(flag.key).toBe('new-dashboard')
|
|
26
|
-
expect(flag.type).toBe('boolean')
|
|
27
|
-
expect(flag.defaultValue).toBe(false)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('creates a string flag with variants', () => {
|
|
31
|
-
const flag = FeatureFlag({
|
|
32
|
-
key: 'theme',
|
|
33
|
-
name: 'Theme',
|
|
34
|
-
type: 'string',
|
|
35
|
-
defaultValue: 'light',
|
|
36
|
-
variants: ['light', 'dark', 'system'],
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
expect(flag.variants).toEqual(['light', 'dark', 'system'])
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('creates a number flag with range', () => {
|
|
43
|
-
const flag = FeatureFlag({
|
|
44
|
-
key: 'rate-limit',
|
|
45
|
-
name: 'Rate Limit',
|
|
46
|
-
type: 'number',
|
|
47
|
-
defaultValue: 100,
|
|
48
|
-
min: 10,
|
|
49
|
-
max: 1000,
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
expect(flag.min).toBe(10)
|
|
53
|
-
expect(flag.max).toBe(1000)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('creates a JSON flag', () => {
|
|
57
|
-
const flag = FeatureFlag({
|
|
58
|
-
key: 'ui-config',
|
|
59
|
-
name: 'UI Config',
|
|
60
|
-
type: 'json',
|
|
61
|
-
defaultValue: { showBanner: true, bannerText: 'Welcome' },
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
expect(flag.defaultValue).toEqual({ showBanner: true, bannerText: 'Welcome' })
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('supports targeting rules', () => {
|
|
68
|
-
const flag = FeatureFlag({
|
|
69
|
-
key: 'beta-feature',
|
|
70
|
-
name: 'Beta Feature',
|
|
71
|
-
type: 'boolean',
|
|
72
|
-
defaultValue: false,
|
|
73
|
-
rules: [
|
|
74
|
-
{
|
|
75
|
-
condition: { attribute: 'plan', operator: 'equals', value: 'enterprise' },
|
|
76
|
-
value: true,
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
condition: { attribute: 'userId', operator: 'in', value: ['user-1', 'user-2'] },
|
|
80
|
-
value: true,
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
expect(flag.rules).toHaveLength(2)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('supports percentage rollout', () => {
|
|
89
|
-
const flag = FeatureFlag({
|
|
90
|
-
key: 'gradual-release',
|
|
91
|
-
name: 'Gradual Release',
|
|
92
|
-
type: 'boolean',
|
|
93
|
-
defaultValue: false,
|
|
94
|
-
rolloutPercentage: 25,
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
expect(flag.rolloutPercentage).toBe(25)
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
describe('FeatureFlagManager', () => {
|
|
102
|
-
let manager: FeatureFlagManager
|
|
103
|
-
|
|
104
|
-
beforeEach(() => {
|
|
105
|
-
manager = createFeatureFlagManager()
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('registers and retrieves flags', () => {
|
|
109
|
-
const flag = FeatureFlag({
|
|
110
|
-
key: 'test-flag',
|
|
111
|
-
name: 'Test',
|
|
112
|
-
type: 'boolean',
|
|
113
|
-
defaultValue: true,
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
manager.register(flag)
|
|
117
|
-
expect(manager.get('test-flag')).toEqual(flag)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('evaluates boolean flag with default', () => {
|
|
121
|
-
manager.register(
|
|
122
|
-
FeatureFlag({
|
|
123
|
-
key: 'simple',
|
|
124
|
-
name: 'Simple',
|
|
125
|
-
type: 'boolean',
|
|
126
|
-
defaultValue: false,
|
|
127
|
-
})
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
const result = manager.evaluate('simple', {})
|
|
131
|
-
expect(result).toBe(false)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('evaluates flag with targeting rules', () => {
|
|
135
|
-
manager.register(
|
|
136
|
-
FeatureFlag({
|
|
137
|
-
key: 'premium-only',
|
|
138
|
-
name: 'Premium Only',
|
|
139
|
-
type: 'boolean',
|
|
140
|
-
defaultValue: false,
|
|
141
|
-
rules: [
|
|
142
|
-
{
|
|
143
|
-
condition: { attribute: 'plan', operator: 'equals', value: 'premium' },
|
|
144
|
-
value: true,
|
|
145
|
-
},
|
|
146
|
-
],
|
|
147
|
-
})
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
const context: FlagEvaluationContext = {
|
|
151
|
-
userId: 'user-123',
|
|
152
|
-
attributes: { plan: 'premium' },
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
expect(manager.evaluate('premium-only', context)).toBe(true)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('evaluates flag with percentage rollout', () => {
|
|
159
|
-
manager.register(
|
|
160
|
-
FeatureFlag({
|
|
161
|
-
key: 'rollout',
|
|
162
|
-
name: 'Rollout',
|
|
163
|
-
type: 'boolean',
|
|
164
|
-
defaultValue: false,
|
|
165
|
-
rolloutPercentage: 100, // 100% should always be true
|
|
166
|
-
})
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
const result = manager.evaluate('rollout', { userId: 'any-user' })
|
|
170
|
-
expect(result).toBe(true)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('returns default for 0% rollout', () => {
|
|
174
|
-
manager.register(
|
|
175
|
-
FeatureFlag({
|
|
176
|
-
key: 'disabled',
|
|
177
|
-
name: 'Disabled',
|
|
178
|
-
type: 'boolean',
|
|
179
|
-
defaultValue: false,
|
|
180
|
-
rolloutPercentage: 0,
|
|
181
|
-
})
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
const result = manager.evaluate('disabled', { userId: 'any-user' })
|
|
185
|
-
expect(result).toBe(false)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('evaluates string flag variants', () => {
|
|
189
|
-
manager.register(
|
|
190
|
-
FeatureFlag({
|
|
191
|
-
key: 'theme',
|
|
192
|
-
name: 'Theme',
|
|
193
|
-
type: 'string',
|
|
194
|
-
defaultValue: 'light',
|
|
195
|
-
rules: [
|
|
196
|
-
{
|
|
197
|
-
condition: { attribute: 'darkModeEnabled', operator: 'equals', value: true },
|
|
198
|
-
value: 'dark',
|
|
199
|
-
},
|
|
200
|
-
],
|
|
201
|
-
})
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
const result = manager.evaluate('theme', {
|
|
205
|
-
attributes: { darkModeEnabled: true },
|
|
206
|
-
})
|
|
207
|
-
expect(result).toBe('dark')
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('tracks flag evaluations', () => {
|
|
211
|
-
manager.register(
|
|
212
|
-
FeatureFlag({
|
|
213
|
-
key: 'tracked',
|
|
214
|
-
name: 'Tracked',
|
|
215
|
-
type: 'boolean',
|
|
216
|
-
defaultValue: true,
|
|
217
|
-
})
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
manager.evaluate('tracked', { userId: 'user-1' })
|
|
221
|
-
manager.evaluate('tracked', { userId: 'user-2' })
|
|
222
|
-
|
|
223
|
-
const stats = manager.getEvaluationStats('tracked')
|
|
224
|
-
expect(stats.totalEvaluations).toBe(2)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('supports overrides for testing', () => {
|
|
228
|
-
manager.register(
|
|
229
|
-
FeatureFlag({
|
|
230
|
-
key: 'overridable',
|
|
231
|
-
name: 'Overridable',
|
|
232
|
-
type: 'boolean',
|
|
233
|
-
defaultValue: false,
|
|
234
|
-
})
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
manager.setOverride('overridable', true)
|
|
238
|
-
expect(manager.evaluate('overridable', {})).toBe(true)
|
|
239
|
-
|
|
240
|
-
manager.clearOverride('overridable')
|
|
241
|
-
expect(manager.evaluate('overridable', {})).toBe(false)
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
it('lists all flags', () => {
|
|
245
|
-
manager.register(FeatureFlag({ key: 'f1', name: 'F1', type: 'boolean', defaultValue: true }))
|
|
246
|
-
manager.register(FeatureFlag({ key: 'f2', name: 'F2', type: 'string', defaultValue: 'a' }))
|
|
247
|
-
|
|
248
|
-
expect(manager.list()).toHaveLength(2)
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('removes flags', () => {
|
|
252
|
-
manager.register(FeatureFlag({ key: 'temp', name: 'Temp', type: 'boolean', defaultValue: true }))
|
|
253
|
-
expect(manager.get('temp')).toBeDefined()
|
|
254
|
-
|
|
255
|
-
manager.remove('temp')
|
|
256
|
-
expect(manager.get('temp')).toBeUndefined()
|
|
257
|
-
})
|
|
258
|
-
})
|
|
259
|
-
})
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Feature Toggles
|
|
3
|
-
* Phase 1: RED - These tests should FAIL initially
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
FeatureToggle,
|
|
9
|
-
createToggle,
|
|
10
|
-
ToggleService,
|
|
11
|
-
createToggleService,
|
|
12
|
-
ToggleCategory,
|
|
13
|
-
} from '../../src/features/toggles.js'
|
|
14
|
-
|
|
15
|
-
describe('Feature Toggles', () => {
|
|
16
|
-
describe('FeatureToggle()', () => {
|
|
17
|
-
it('creates an ops toggle', () => {
|
|
18
|
-
const toggle = FeatureToggle({
|
|
19
|
-
name: 'kill-switch',
|
|
20
|
-
category: 'ops',
|
|
21
|
-
enabled: false,
|
|
22
|
-
description: 'Emergency kill switch',
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
expect(toggle.name).toBe('kill-switch')
|
|
26
|
-
expect(toggle.category).toBe('ops')
|
|
27
|
-
expect(toggle.enabled).toBe(false)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('creates a release toggle', () => {
|
|
31
|
-
const toggle = FeatureToggle({
|
|
32
|
-
name: 'new-checkout',
|
|
33
|
-
category: 'release',
|
|
34
|
-
enabled: false,
|
|
35
|
-
releaseDate: new Date('2024-03-01'),
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
expect(toggle.category).toBe('release')
|
|
39
|
-
expect(toggle.releaseDate).toEqual(new Date('2024-03-01'))
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('creates an experiment toggle', () => {
|
|
43
|
-
const toggle = FeatureToggle({
|
|
44
|
-
name: 'checkout-experiment',
|
|
45
|
-
category: 'experiment',
|
|
46
|
-
enabled: true,
|
|
47
|
-
experimentId: 'exp-checkout-flow',
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
expect(toggle.category).toBe('experiment')
|
|
51
|
-
expect(toggle.experimentId).toBe('exp-checkout-flow')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('creates a permission toggle', () => {
|
|
55
|
-
const toggle = FeatureToggle({
|
|
56
|
-
name: 'admin-panel',
|
|
57
|
-
category: 'permission',
|
|
58
|
-
enabled: true,
|
|
59
|
-
requiredRoles: ['admin', 'super-admin'],
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
expect(toggle.category).toBe('permission')
|
|
63
|
-
expect(toggle.requiredRoles).toEqual(['admin', 'super-admin'])
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('supports expiration date', () => {
|
|
67
|
-
const toggle = FeatureToggle({
|
|
68
|
-
name: 'promo-banner',
|
|
69
|
-
category: 'ops',
|
|
70
|
-
enabled: true,
|
|
71
|
-
expiresAt: new Date('2024-12-31'),
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
expect(toggle.expiresAt).toEqual(new Date('2024-12-31'))
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
describe('ToggleService', () => {
|
|
79
|
-
let service: ToggleService
|
|
80
|
-
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
service = createToggleService()
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('registers and checks toggles', () => {
|
|
86
|
-
service.register(
|
|
87
|
-
FeatureToggle({
|
|
88
|
-
name: 'feature-x',
|
|
89
|
-
category: 'release',
|
|
90
|
-
enabled: true,
|
|
91
|
-
})
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
expect(service.isEnabled('feature-x')).toBe(true)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('returns false for unknown toggle', () => {
|
|
98
|
-
expect(service.isEnabled('unknown')).toBe(false)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('enables and disables toggles', () => {
|
|
102
|
-
service.register(
|
|
103
|
-
FeatureToggle({
|
|
104
|
-
name: 'switchable',
|
|
105
|
-
category: 'ops',
|
|
106
|
-
enabled: false,
|
|
107
|
-
})
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
expect(service.isEnabled('switchable')).toBe(false)
|
|
111
|
-
service.enable('switchable')
|
|
112
|
-
expect(service.isEnabled('switchable')).toBe(true)
|
|
113
|
-
service.disable('switchable')
|
|
114
|
-
expect(service.isEnabled('switchable')).toBe(false)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('checks permission toggles with context', () => {
|
|
118
|
-
service.register(
|
|
119
|
-
FeatureToggle({
|
|
120
|
-
name: 'admin-only',
|
|
121
|
-
category: 'permission',
|
|
122
|
-
enabled: true,
|
|
123
|
-
requiredRoles: ['admin'],
|
|
124
|
-
})
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
expect(service.isEnabled('admin-only', { roles: ['user'] })).toBe(false)
|
|
128
|
-
expect(service.isEnabled('admin-only', { roles: ['admin'] })).toBe(true)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('respects expiration dates', () => {
|
|
132
|
-
service.register(
|
|
133
|
-
FeatureToggle({
|
|
134
|
-
name: 'expired',
|
|
135
|
-
category: 'ops',
|
|
136
|
-
enabled: true,
|
|
137
|
-
expiresAt: new Date('2020-01-01'), // Past date
|
|
138
|
-
})
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
expect(service.isEnabled('expired')).toBe(false)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('lists toggles by category', () => {
|
|
145
|
-
service.register(FeatureToggle({ name: 't1', category: 'ops', enabled: true }))
|
|
146
|
-
service.register(FeatureToggle({ name: 't2', category: 'release', enabled: true }))
|
|
147
|
-
service.register(FeatureToggle({ name: 't3', category: 'ops', enabled: false }))
|
|
148
|
-
|
|
149
|
-
const ops = service.listByCategory('ops')
|
|
150
|
-
expect(ops).toHaveLength(2)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('bulk enables/disables toggles', () => {
|
|
154
|
-
service.register(FeatureToggle({ name: 'b1', category: 'ops', enabled: false }))
|
|
155
|
-
service.register(FeatureToggle({ name: 'b2', category: 'ops', enabled: false }))
|
|
156
|
-
|
|
157
|
-
service.bulkEnable(['b1', 'b2'])
|
|
158
|
-
expect(service.isEnabled('b1')).toBe(true)
|
|
159
|
-
expect(service.isEnabled('b2')).toBe(true)
|
|
160
|
-
|
|
161
|
-
service.bulkDisable(['b1', 'b2'])
|
|
162
|
-
expect(service.isEnabled('b1')).toBe(false)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('exports and imports toggle state', () => {
|
|
166
|
-
service.register(FeatureToggle({ name: 'export1', category: 'ops', enabled: true }))
|
|
167
|
-
service.register(FeatureToggle({ name: 'export2', category: 'release', enabled: false }))
|
|
168
|
-
|
|
169
|
-
const exported = service.export()
|
|
170
|
-
expect(exported).toHaveLength(2)
|
|
171
|
-
|
|
172
|
-
const newService = createToggleService()
|
|
173
|
-
newService.import(exported)
|
|
174
|
-
expect(newService.isEnabled('export1')).toBe(true)
|
|
175
|
-
expect(newService.isEnabled('export2')).toBe(false)
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
})
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Product Lifecycle Stages
|
|
3
|
-
* Phase 1: RED - These tests should FAIL initially
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
LifecycleStage,
|
|
9
|
-
ProductLifecycle,
|
|
10
|
-
createProductLifecycle,
|
|
11
|
-
LifecycleManager,
|
|
12
|
-
createLifecycleManager,
|
|
13
|
-
LifecycleEvent,
|
|
14
|
-
} from '../../src/lifecycle/stages.js'
|
|
15
|
-
|
|
16
|
-
describe('Product Lifecycle Stages', () => {
|
|
17
|
-
describe('LifecycleStage', () => {
|
|
18
|
-
it('defines alpha stage', () => {
|
|
19
|
-
const stage: LifecycleStage = {
|
|
20
|
-
name: 'alpha',
|
|
21
|
-
displayName: 'Alpha',
|
|
22
|
-
description: 'Internal testing only',
|
|
23
|
-
order: 0,
|
|
24
|
-
isPublic: false,
|
|
25
|
-
supportLevel: 'none',
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
expect(stage.name).toBe('alpha')
|
|
29
|
-
expect(stage.isPublic).toBe(false)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('defines beta stage', () => {
|
|
33
|
-
const stage: LifecycleStage = {
|
|
34
|
-
name: 'beta',
|
|
35
|
-
displayName: 'Beta',
|
|
36
|
-
description: 'External beta testing',
|
|
37
|
-
order: 1,
|
|
38
|
-
isPublic: true,
|
|
39
|
-
supportLevel: 'limited',
|
|
40
|
-
warningMessage: 'This feature is in beta and may change',
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
expect(stage.name).toBe('beta')
|
|
44
|
-
expect(stage.supportLevel).toBe('limited')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('defines GA stage', () => {
|
|
48
|
-
const stage: LifecycleStage = {
|
|
49
|
-
name: 'ga',
|
|
50
|
-
displayName: 'Generally Available',
|
|
51
|
-
description: 'Production ready',
|
|
52
|
-
order: 2,
|
|
53
|
-
isPublic: true,
|
|
54
|
-
supportLevel: 'full',
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
expect(stage.supportLevel).toBe('full')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('defines deprecated stage', () => {
|
|
61
|
-
const stage: LifecycleStage = {
|
|
62
|
-
name: 'deprecated',
|
|
63
|
-
displayName: 'Deprecated',
|
|
64
|
-
description: 'No longer recommended',
|
|
65
|
-
order: 3,
|
|
66
|
-
isPublic: true,
|
|
67
|
-
supportLevel: 'limited',
|
|
68
|
-
deprecationDate: new Date('2024-06-01'),
|
|
69
|
-
migrationGuide: 'https://docs.example.com/migrate',
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
expect(stage.deprecationDate).toBeDefined()
|
|
73
|
-
expect(stage.migrationGuide).toBeDefined()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('defines sunset stage', () => {
|
|
77
|
-
const stage: LifecycleStage = {
|
|
78
|
-
name: 'sunset',
|
|
79
|
-
displayName: 'End of Life',
|
|
80
|
-
description: 'Will be removed',
|
|
81
|
-
order: 4,
|
|
82
|
-
isPublic: false,
|
|
83
|
-
supportLevel: 'none',
|
|
84
|
-
sunsetDate: new Date('2024-12-31'),
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
expect(stage.sunsetDate).toBeDefined()
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
describe('ProductLifecycle', () => {
|
|
92
|
-
let lifecycle: ProductLifecycle
|
|
93
|
-
|
|
94
|
-
beforeEach(() => {
|
|
95
|
-
lifecycle = createProductLifecycle({
|
|
96
|
-
productId: 'my-feature',
|
|
97
|
-
currentStage: 'beta',
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('creates lifecycle with initial stage', () => {
|
|
102
|
-
expect(lifecycle.productId).toBe('my-feature')
|
|
103
|
-
expect(lifecycle.currentStage).toBe('beta')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('tracks lifecycle history', () => {
|
|
107
|
-
expect(lifecycle.history).toHaveLength(1)
|
|
108
|
-
expect(lifecycle.history[0].stage).toBe('beta')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('advances to next stage', () => {
|
|
112
|
-
const updated = lifecycle.advance('ga')
|
|
113
|
-
expect(updated.currentStage).toBe('ga')
|
|
114
|
-
expect(updated.history).toHaveLength(2)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('rejects invalid transitions', () => {
|
|
118
|
-
expect(() => lifecycle.advance('alpha')).toThrow('Cannot transition backwards')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('supports custom transition validation', () => {
|
|
122
|
-
const strictLifecycle = createProductLifecycle({
|
|
123
|
-
productId: 'strict-feature',
|
|
124
|
-
currentStage: 'alpha',
|
|
125
|
-
validateTransition: (from, to) => {
|
|
126
|
-
// Only allow sequential transitions
|
|
127
|
-
const stages = ['alpha', 'beta', 'ga', 'deprecated', 'sunset']
|
|
128
|
-
const fromIdx = stages.indexOf(from)
|
|
129
|
-
const toIdx = stages.indexOf(to)
|
|
130
|
-
return toIdx === fromIdx + 1
|
|
131
|
-
},
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
expect(() => strictLifecycle.advance('ga')).toThrow()
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('includes metadata in history', () => {
|
|
138
|
-
const updated = lifecycle.advance('ga', {
|
|
139
|
-
reason: 'Passed all quality gates',
|
|
140
|
-
approvedBy: 'eng-lead',
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
expect(updated.history[1].metadata?.reason).toBe('Passed all quality gates')
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('calculates time in stage', () => {
|
|
147
|
-
const time = lifecycle.getTimeInCurrentStage()
|
|
148
|
-
expect(time).toBeGreaterThanOrEqual(0)
|
|
149
|
-
})
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
describe('LifecycleManager', () => {
|
|
153
|
-
let manager: LifecycleManager
|
|
154
|
-
|
|
155
|
-
beforeEach(() => {
|
|
156
|
-
manager = createLifecycleManager()
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('registers product lifecycle', () => {
|
|
160
|
-
manager.register({
|
|
161
|
-
productId: 'feature-a',
|
|
162
|
-
currentStage: 'beta',
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
expect(manager.get('feature-a')).toBeDefined()
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('advances product stage', () => {
|
|
169
|
-
manager.register({
|
|
170
|
-
productId: 'feature-b',
|
|
171
|
-
currentStage: 'beta',
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
manager.advanceStage('feature-b', 'ga')
|
|
175
|
-
expect(manager.get('feature-b')?.currentStage).toBe('ga')
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('lists products by stage', () => {
|
|
179
|
-
manager.register({ productId: 'p1', currentStage: 'alpha' })
|
|
180
|
-
manager.register({ productId: 'p2', currentStage: 'beta' })
|
|
181
|
-
manager.register({ productId: 'p3', currentStage: 'beta' })
|
|
182
|
-
manager.register({ productId: 'p4', currentStage: 'ga' })
|
|
183
|
-
|
|
184
|
-
const beta = manager.listByStage('beta')
|
|
185
|
-
expect(beta).toHaveLength(2)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('finds deprecated products', () => {
|
|
189
|
-
manager.register({ productId: 'old', currentStage: 'deprecated' })
|
|
190
|
-
manager.register({ productId: 'newer', currentStage: 'ga' })
|
|
191
|
-
|
|
192
|
-
const deprecated = manager.listByStage('deprecated')
|
|
193
|
-
expect(deprecated).toHaveLength(1)
|
|
194
|
-
expect(deprecated[0].productId).toBe('old')
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('emits stage change events', () => {
|
|
198
|
-
const events: LifecycleEvent[] = []
|
|
199
|
-
manager.on('stage.changed', (e) => events.push(e))
|
|
200
|
-
|
|
201
|
-
manager.register({ productId: 'event-test', currentStage: 'beta' })
|
|
202
|
-
manager.advanceStage('event-test', 'ga')
|
|
203
|
-
|
|
204
|
-
expect(events).toHaveLength(1)
|
|
205
|
-
expect(events[0].fromStage).toBe('beta')
|
|
206
|
-
expect(events[0].toStage).toBe('ga')
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('schedules future stage transitions', () => {
|
|
210
|
-
manager.register({ productId: 'scheduled', currentStage: 'ga' })
|
|
211
|
-
|
|
212
|
-
manager.scheduleTransition('scheduled', 'deprecated', {
|
|
213
|
-
date: new Date('2024-12-01'),
|
|
214
|
-
reason: 'Planned deprecation',
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
const scheduled = manager.getScheduledTransitions('scheduled')
|
|
218
|
-
expect(scheduled).toHaveLength(1)
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
it('gets lifecycle summary', () => {
|
|
222
|
-
manager.register({ productId: 's1', currentStage: 'alpha' })
|
|
223
|
-
manager.register({ productId: 's2', currentStage: 'beta' })
|
|
224
|
-
manager.register({ productId: 's3', currentStage: 'ga' })
|
|
225
|
-
manager.register({ productId: 's4', currentStage: 'ga' })
|
|
226
|
-
|
|
227
|
-
const summary = manager.getSummary()
|
|
228
|
-
expect(summary.alpha).toBe(1)
|
|
229
|
-
expect(summary.beta).toBe(1)
|
|
230
|
-
expect(summary.ga).toBe(2)
|
|
231
|
-
})
|
|
232
|
-
})
|
|
233
|
-
})
|