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.
Files changed (122) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +2 -0
  4. package/dist/api.js +7 -7
  5. package/dist/api.js.map +1 -1
  6. package/dist/app.js +6 -6
  7. package/dist/app.js.map +1 -1
  8. package/dist/client.d.ts +157 -0
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +69 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/content.js +7 -7
  13. package/dist/content.js.map +1 -1
  14. package/dist/data.d.ts.map +1 -1
  15. package/dist/data.js +6 -6
  16. package/dist/data.js.map +1 -1
  17. package/dist/dataset.js +5 -5
  18. package/dist/dataset.js.map +1 -1
  19. package/dist/index.d.ts +92 -13
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +139 -15
  22. package/dist/index.js.map +1 -1
  23. package/dist/mcp.d.ts +1 -1
  24. package/dist/mcp.d.ts.map +1 -1
  25. package/dist/mcp.js +17 -10
  26. package/dist/mcp.js.map +1 -1
  27. package/dist/product.js +2 -2
  28. package/dist/product.js.map +1 -1
  29. package/dist/sdk.d.ts.map +1 -1
  30. package/dist/sdk.js +52 -16
  31. package/dist/sdk.js.map +1 -1
  32. package/dist/site.d.ts.map +1 -1
  33. package/dist/site.js +12 -8
  34. package/dist/site.js.map +1 -1
  35. package/dist/types.d.ts +830 -12
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js +495 -2
  38. package/dist/types.js.map +1 -1
  39. package/dist/worker.d.ts +205 -0
  40. package/dist/worker.d.ts.map +1 -0
  41. package/dist/worker.js +356 -0
  42. package/dist/worker.js.map +1 -0
  43. package/package.json +29 -13
  44. package/src/api.ts +7 -7
  45. package/src/app.ts +6 -6
  46. package/src/client.ts +192 -0
  47. package/src/content.ts +7 -7
  48. package/src/data.ts +12 -7
  49. package/src/dataset.ts +5 -5
  50. package/src/index.ts +151 -15
  51. package/src/mcp.ts +18 -11
  52. package/src/product.ts +2 -2
  53. package/src/sdk.ts +54 -15
  54. package/src/site.ts +12 -8
  55. package/src/types.ts +821 -12
  56. package/src/worker.ts +525 -0
  57. package/test/product.test.ts +53 -198
  58. package/test/unified-types.test.ts +589 -0
  59. package/test/worker.test.ts +912 -0
  60. package/vitest.config.ts +42 -0
  61. package/wrangler.jsonc +36 -0
  62. package/LICENSE +0 -21
  63. package/dist/features/define.d.ts +0 -63
  64. package/dist/features/define.d.ts.map +0 -1
  65. package/dist/features/define.js +0 -72
  66. package/dist/features/define.js.map +0 -1
  67. package/dist/features/flags.d.ts +0 -98
  68. package/dist/features/flags.d.ts.map +0 -1
  69. package/dist/features/flags.js +0 -145
  70. package/dist/features/flags.js.map +0 -1
  71. package/dist/features/toggles.d.ts +0 -75
  72. package/dist/features/toggles.d.ts.map +0 -1
  73. package/dist/features/toggles.js +0 -107
  74. package/dist/features/toggles.js.map +0 -1
  75. package/dist/tiers/define.d.ts +0 -63
  76. package/dist/tiers/define.d.ts.map +0 -1
  77. package/dist/tiers/define.js +0 -78
  78. package/dist/tiers/define.js.map +0 -1
  79. package/dist/tiers/entitlements.d.ts +0 -94
  80. package/dist/tiers/entitlements.d.ts.map +0 -1
  81. package/dist/tiers/entitlements.js +0 -94
  82. package/dist/tiers/entitlements.js.map +0 -1
  83. package/src/api.js +0 -128
  84. package/src/app.js +0 -106
  85. package/src/content.js +0 -77
  86. package/src/data.js +0 -106
  87. package/src/dataset.js +0 -49
  88. package/src/entities/ai.js +0 -858
  89. package/src/entities/content.js +0 -783
  90. package/src/entities/index.js +0 -88
  91. package/src/entities/interfaces.js +0 -929
  92. package/src/entities/lifecycle.js +0 -803
  93. package/src/entities/products.js +0 -797
  94. package/src/entities/web.js +0 -657
  95. package/src/features/define.ts +0 -130
  96. package/src/features/flags.ts +0 -247
  97. package/src/features/toggles.ts +0 -189
  98. package/src/index.js +0 -35
  99. package/src/mcp.js +0 -139
  100. package/src/pricing/billing.ts +0 -386
  101. package/src/pricing/plans.ts +0 -214
  102. package/src/product.js +0 -53
  103. package/src/registry.js +0 -31
  104. package/src/sdk.js +0 -127
  105. package/src/site.js +0 -112
  106. package/src/tiers/define.ts +0 -137
  107. package/src/tiers/entitlements.ts +0 -201
  108. package/src/types.js +0 -4
  109. package/test/analytics/events.test.ts +0 -319
  110. package/test/analytics/experiments.test.ts +0 -327
  111. package/test/features/define.test.ts +0 -187
  112. package/test/features/flags.test.ts +0 -259
  113. package/test/features/toggles.test.ts +0 -178
  114. package/test/lifecycle/stages.test.ts +0 -233
  115. package/test/lifecycle/transitions.test.ts +0 -207
  116. package/test/onboarding/flows.test.ts +0 -307
  117. package/test/pricing/billing.test.ts +0 -287
  118. package/test/pricing/plans.test.ts +0 -307
  119. package/test/roadmap/milestones.test.ts +0 -231
  120. package/test/roadmap/priorities.test.ts +0 -239
  121. package/test/tiers/define.test.ts +0 -192
  122. 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
- })