business-as-code 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 (235) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/dist/dollar.d.ts.map +1 -1
  5. package/dist/dollar.js +2 -2
  6. package/dist/dollar.js.map +1 -1
  7. package/dist/entities/organization.d.ts +4 -0
  8. package/dist/entities/organization.d.ts.map +1 -1
  9. package/dist/entities/organization.js +27 -18
  10. package/dist/entities/organization.js.map +1 -1
  11. package/dist/entities/planning.d.ts +87 -0
  12. package/dist/finance/account.d.ts +44 -0
  13. package/dist/finance/account.d.ts.map +1 -0
  14. package/dist/finance/account.js +6 -0
  15. package/dist/finance/account.js.map +1 -0
  16. package/dist/finance/authority.d.ts +78 -0
  17. package/dist/finance/authority.d.ts.map +1 -0
  18. package/dist/finance/authority.js +27 -0
  19. package/dist/finance/authority.js.map +1 -0
  20. package/dist/finance/card.d.ts +36 -0
  21. package/dist/finance/card.d.ts.map +1 -0
  22. package/dist/finance/card.js +6 -0
  23. package/dist/finance/card.js.map +1 -0
  24. package/dist/finance/identity.d.ts +30 -0
  25. package/dist/finance/identity.d.ts.map +1 -0
  26. package/dist/finance/identity.js +8 -0
  27. package/dist/finance/identity.js.map +1 -0
  28. package/dist/finance/index.d.ts +36 -0
  29. package/dist/finance/index.d.ts.map +1 -0
  30. package/dist/finance/index.js +22 -0
  31. package/dist/finance/index.js.map +1 -0
  32. package/dist/finance/ledger.d.ts +24 -0
  33. package/dist/finance/ledger.d.ts.map +1 -0
  34. package/dist/finance/ledger.js +8 -0
  35. package/dist/finance/ledger.js.map +1 -0
  36. package/dist/finance/merchant.d.ts +129 -0
  37. package/dist/finance/merchant.d.ts.map +1 -0
  38. package/dist/finance/merchant.js +21 -0
  39. package/dist/finance/merchant.js.map +1 -0
  40. package/dist/finance/outcome-contract.d.ts +139 -0
  41. package/dist/finance/outcome-contract.d.ts.map +1 -0
  42. package/dist/finance/outcome-contract.js +27 -0
  43. package/dist/finance/outcome-contract.js.map +1 -0
  44. package/dist/finance/port.d.ts +121 -0
  45. package/dist/finance/port.d.ts.map +1 -0
  46. package/dist/finance/port.js +10 -0
  47. package/dist/finance/port.js.map +1 -0
  48. package/dist/finance/pricing.d.ts +154 -0
  49. package/dist/finance/pricing.d.ts.map +1 -0
  50. package/dist/finance/pricing.js +79 -0
  51. package/dist/finance/pricing.js.map +1 -0
  52. package/dist/finance/proof-predicate.d.ts +92 -0
  53. package/dist/finance/proof-predicate.d.ts.map +1 -0
  54. package/dist/finance/proof-predicate.js +80 -0
  55. package/dist/finance/proof-predicate.js.map +1 -0
  56. package/dist/finance/refund.d.ts +44 -0
  57. package/dist/finance/refund.d.ts.map +1 -0
  58. package/dist/finance/refund.js +41 -0
  59. package/dist/finance/refund.js.map +1 -0
  60. package/dist/finance/sla.d.ts +25 -0
  61. package/dist/finance/sla.d.ts.map +1 -0
  62. package/dist/finance/sla.js +7 -0
  63. package/dist/finance/sla.js.map +1 -0
  64. package/dist/finance/types.d.ts +79 -0
  65. package/dist/finance/types.d.ts.map +1 -0
  66. package/dist/finance/types.js +8 -0
  67. package/dist/{canvas → finance}/types.js.map +1 -1
  68. package/dist/goals.d.ts +19 -0
  69. package/dist/goals.d.ts.map +1 -1
  70. package/dist/goals.js +81 -12
  71. package/dist/goals.js.map +1 -1
  72. package/dist/index.d.ts +12 -8
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +19 -7
  75. package/dist/index.js.map +1 -1
  76. package/dist/kpis.d.ts +19 -0
  77. package/dist/kpis.d.ts.map +1 -1
  78. package/dist/kpis.js +71 -6
  79. package/dist/kpis.js.map +1 -1
  80. package/dist/metrics.d.ts.map +1 -1
  81. package/dist/metrics.js +29 -24
  82. package/dist/metrics.js.map +1 -1
  83. package/dist/okrs.d.ts +34 -0
  84. package/dist/okrs.d.ts.map +1 -1
  85. package/dist/okrs.js +135 -13
  86. package/dist/okrs.js.map +1 -1
  87. package/dist/organization.d.ts.map +1 -1
  88. package/dist/organization.js +11 -11
  89. package/dist/organization.js.map +1 -1
  90. package/dist/process.d.ts.map +1 -1
  91. package/dist/process.js +13 -12
  92. package/dist/process.js.map +1 -1
  93. package/dist/product.d.ts.map +1 -1
  94. package/dist/product.js +9 -9
  95. package/dist/product.js.map +1 -1
  96. package/dist/queries.d.ts.map +1 -1
  97. package/dist/queries.js +194 -32
  98. package/dist/queries.js.map +1 -1
  99. package/dist/roles.d.ts +25 -31
  100. package/dist/roles.d.ts.map +1 -1
  101. package/dist/roles.js +37 -10
  102. package/dist/roles.js.map +1 -1
  103. package/dist/workflow.d.ts.map +1 -1
  104. package/dist/workflow.js +13 -12
  105. package/dist/workflow.js.map +1 -1
  106. package/package.json +20 -13
  107. package/src/dollar.ts +5 -2
  108. package/src/entities/organization.ts +31 -18
  109. package/src/finance/account.ts +48 -0
  110. package/src/finance/authority.ts +42 -0
  111. package/src/finance/card.ts +38 -0
  112. package/src/finance/identity.ts +31 -0
  113. package/src/finance/index.ts +117 -0
  114. package/src/finance/ledger.ts +26 -0
  115. package/src/finance/merchant.ts +127 -0
  116. package/src/finance/outcome-contract.ts +157 -0
  117. package/src/finance/port.ts +144 -0
  118. package/src/finance/pricing.ts +197 -0
  119. package/src/finance/proof-predicate.ts +106 -0
  120. package/src/finance/refund.ts +52 -0
  121. package/src/finance/sla.ts +33 -0
  122. package/src/finance/types.ts +75 -0
  123. package/src/goals.ts +78 -12
  124. package/src/index.ts +48 -18
  125. package/src/kpis.ts +62 -8
  126. package/src/metrics.ts +92 -79
  127. package/src/okrs.ts +120 -20
  128. package/src/organization.ts +12 -15
  129. package/src/process.ts +11 -12
  130. package/src/product.ts +8 -9
  131. package/src/queries.ts +238 -75
  132. package/src/roles.ts +62 -61
  133. package/src/workflow.ts +22 -15
  134. package/test/business.test.ts +282 -0
  135. package/test/dollar.test.ts +270 -0
  136. package/test/entities.test.ts +628 -0
  137. package/test/financials.test.ts +539 -0
  138. package/test/goals.test.ts +451 -0
  139. package/{src → test}/index.test.ts +1 -1
  140. package/test/kpis.test.ts +440 -0
  141. package/test/metrics.test.ts +744 -0
  142. package/test/okrs.test.ts +741 -0
  143. package/test/organization.test.ts +548 -0
  144. package/test/process.test.ts +503 -0
  145. package/test/product.test.ts +430 -0
  146. package/test/queries.test.ts +556 -0
  147. package/test/roles.test.ts +546 -0
  148. package/test/service.test.ts +450 -0
  149. package/test/types.test.ts +1141 -0
  150. package/test/vision.test.ts +214 -0
  151. package/test/workflow.test.ts +501 -0
  152. package/vitest.config.ts +47 -0
  153. package/LICENSE +0 -21
  154. package/dist/canvas/activities.d.ts +0 -19
  155. package/dist/canvas/activities.d.ts.map +0 -1
  156. package/dist/canvas/activities.js +0 -20
  157. package/dist/canvas/activities.js.map +0 -1
  158. package/dist/canvas/channels.d.ts +0 -20
  159. package/dist/canvas/channels.d.ts.map +0 -1
  160. package/dist/canvas/channels.js +0 -21
  161. package/dist/canvas/channels.js.map +0 -1
  162. package/dist/canvas/relationships.d.ts +0 -20
  163. package/dist/canvas/relationships.d.ts.map +0 -1
  164. package/dist/canvas/relationships.js +0 -21
  165. package/dist/canvas/relationships.js.map +0 -1
  166. package/dist/canvas/resources.d.ts +0 -20
  167. package/dist/canvas/resources.d.ts.map +0 -1
  168. package/dist/canvas/resources.js +0 -30
  169. package/dist/canvas/resources.js.map +0 -1
  170. package/dist/canvas/revenue.d.ts +0 -22
  171. package/dist/canvas/revenue.d.ts.map +0 -1
  172. package/dist/canvas/revenue.js +0 -30
  173. package/dist/canvas/revenue.js.map +0 -1
  174. package/dist/canvas/segments.d.ts +0 -20
  175. package/dist/canvas/segments.d.ts.map +0 -1
  176. package/dist/canvas/segments.js +0 -28
  177. package/dist/canvas/segments.js.map +0 -1
  178. package/dist/canvas/types.d.ts +0 -232
  179. package/dist/canvas/types.d.ts.map +0 -1
  180. package/dist/canvas/types.js +0 -8
  181. package/dist/canvas/value.d.ts +0 -20
  182. package/dist/canvas/value.d.ts.map +0 -1
  183. package/dist/canvas/value.js +0 -21
  184. package/dist/canvas/value.js.map +0 -1
  185. package/src/business.js +0 -108
  186. package/src/canvas/activities.ts +0 -32
  187. package/src/canvas/canvas.ts +0 -482
  188. package/src/canvas/channels.ts +0 -34
  189. package/src/canvas/costs.ts +0 -43
  190. package/src/canvas/economics.ts +0 -99
  191. package/src/canvas/index.ts +0 -206
  192. package/src/canvas/partnerships.ts +0 -34
  193. package/src/canvas/projections.ts +0 -141
  194. package/src/canvas/relationships.ts +0 -34
  195. package/src/canvas/resources.ts +0 -43
  196. package/src/canvas/revenue.ts +0 -56
  197. package/src/canvas/segments.ts +0 -42
  198. package/src/canvas/types.ts +0 -363
  199. package/src/canvas/value.ts +0 -34
  200. package/src/dollar.js +0 -106
  201. package/src/entities/assets.js +0 -322
  202. package/src/entities/business.js +0 -369
  203. package/src/entities/communication.js +0 -254
  204. package/src/entities/customers.js +0 -988
  205. package/src/entities/financials.js +0 -931
  206. package/src/entities/goals.js +0 -799
  207. package/src/entities/index.js +0 -197
  208. package/src/entities/legal.js +0 -300
  209. package/src/entities/market.js +0 -300
  210. package/src/entities/marketing.js +0 -1156
  211. package/src/entities/offerings.js +0 -726
  212. package/src/entities/operations.js +0 -786
  213. package/src/entities/organization.js +0 -806
  214. package/src/entities/partnerships.js +0 -299
  215. package/src/entities/planning.js +0 -270
  216. package/src/entities/projects.js +0 -348
  217. package/src/entities/risk.js +0 -292
  218. package/src/entities/sales.js +0 -1247
  219. package/src/financials.js +0 -296
  220. package/src/goals.js +0 -214
  221. package/src/index.js +0 -131
  222. package/src/index.test.js +0 -274
  223. package/src/kpis.js +0 -231
  224. package/src/metrics.js +0 -324
  225. package/src/okrs.js +0 -268
  226. package/src/organization.js +0 -172
  227. package/src/process.js +0 -240
  228. package/src/product.js +0 -144
  229. package/src/queries.js +0 -414
  230. package/src/roles.js +0 -254
  231. package/src/service.js +0 -139
  232. package/src/types.js +0 -4
  233. package/src/vision.js +0 -67
  234. package/src/workflow.js +0 -246
  235. package/tests/canvas.test.ts +0 -842
@@ -0,0 +1,741 @@
1
+ /**
2
+ * Tests for okrs.ts - Objectives and Key Results management
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import {
7
+ okrs,
8
+ okr,
9
+ calculateKeyResultProgress,
10
+ calculateOKRProgress,
11
+ calculateConfidence,
12
+ updateKeyResult,
13
+ isKeyResultOnTrack,
14
+ isOKROnTrack,
15
+ getKeyResultsOnTrack,
16
+ getKeyResultsAtRisk,
17
+ getOKRsByOwner,
18
+ getOKRsByPeriod,
19
+ getOKRsByStatus,
20
+ calculateSuccessRate,
21
+ formatKeyResult,
22
+ compareOKRPerformance,
23
+ validateOKRs,
24
+ } from '../src/okrs.js'
25
+ import type { OKRDefinition, KeyResult } from '../src/types.js'
26
+
27
+ describe('OKRs', () => {
28
+ describe('okrs()', () => {
29
+ it('should create multiple OKRs', () => {
30
+ const okrList = okrs([
31
+ {
32
+ objective: 'Achieve Product-Market Fit',
33
+ keyResults: [
34
+ { description: 'Increase NPS', metric: 'NPS', startValue: 40, targetValue: 60 },
35
+ ],
36
+ },
37
+ {
38
+ objective: 'Grow Revenue 50% Quarter over Quarter',
39
+ keyResults: [
40
+ { description: 'Increase MRR', metric: 'MRR', startValue: 100000, targetValue: 150000 },
41
+ ],
42
+ },
43
+ ])
44
+
45
+ expect(okrList).toHaveLength(2)
46
+ expect(okrList[0]?.objective).toBe('Achieve Product-Market Fit')
47
+ expect(okrList[1]?.objective).toBe('Grow Revenue 50% Quarter over Quarter')
48
+ })
49
+
50
+ it('should normalize OKR defaults', () => {
51
+ const okrList = okrs([
52
+ {
53
+ objective: 'Achieve Product-Market Fit',
54
+ keyResults: [
55
+ {
56
+ description: 'Increase NPS',
57
+ metric: 'NPS',
58
+ startValue: 40,
59
+ targetValue: 60,
60
+ currentValue: 50,
61
+ },
62
+ ],
63
+ },
64
+ ])
65
+
66
+ expect(okrList[0]?.status).toBe('not-started')
67
+ expect(okrList[0]?.metadata).toEqual({})
68
+ expect(okrList[0]?.keyResults?.[0]?.progress).toBe(50)
69
+ })
70
+
71
+ it('should throw error for OKR without objective', () => {
72
+ expect(() => okrs([{ objective: '' }])).toThrow('OKR objective is required')
73
+ })
74
+ })
75
+
76
+ describe('okr()', () => {
77
+ it('should create a single OKR', () => {
78
+ const singleOKR = okr({
79
+ objective: 'Launch MVP by end of quarter',
80
+ owner: 'Product Team',
81
+ period: 'Q2 2024',
82
+ keyResults: [
83
+ {
84
+ description: 'Complete core features',
85
+ metric: 'Features',
86
+ startValue: 0,
87
+ targetValue: 5,
88
+ },
89
+ ],
90
+ })
91
+
92
+ expect(singleOKR.objective).toBe('Launch MVP by end of quarter')
93
+ expect(singleOKR.owner).toBe('Product Team')
94
+ expect(singleOKR.period).toBe('Q2 2024')
95
+ })
96
+ })
97
+
98
+ describe('calculateKeyResultProgress()', () => {
99
+ it('should calculate progress percentage', () => {
100
+ const kr: KeyResult = {
101
+ description: 'Test',
102
+ metric: 'NPS',
103
+ startValue: 0,
104
+ targetValue: 100,
105
+ currentValue: 50,
106
+ }
107
+ expect(calculateKeyResultProgress(kr)).toBe(50)
108
+ })
109
+
110
+ it('should handle starting value greater than zero', () => {
111
+ const kr: KeyResult = {
112
+ description: 'Test',
113
+ metric: 'NPS',
114
+ startValue: 40,
115
+ targetValue: 60,
116
+ currentValue: 50,
117
+ }
118
+ expect(calculateKeyResultProgress(kr)).toBe(50)
119
+ })
120
+
121
+ it('should return 0 for missing currentValue', () => {
122
+ const kr: KeyResult = { description: 'Test', metric: 'NPS', startValue: 0, targetValue: 100 }
123
+ expect(calculateKeyResultProgress(kr)).toBe(0)
124
+ })
125
+
126
+ it('should return 0 for missing startValue', () => {
127
+ const kr: KeyResult = {
128
+ description: 'Test',
129
+ metric: 'NPS',
130
+ targetValue: 100,
131
+ currentValue: 50,
132
+ }
133
+ expect(calculateKeyResultProgress(kr)).toBe(0)
134
+ })
135
+
136
+ it('should return 100 when target equals start', () => {
137
+ const kr: KeyResult = {
138
+ description: 'Test',
139
+ metric: 'NPS',
140
+ startValue: 50,
141
+ targetValue: 50,
142
+ currentValue: 50,
143
+ }
144
+ expect(calculateKeyResultProgress(kr)).toBe(100)
145
+ })
146
+
147
+ it('should cap progress at 100', () => {
148
+ const kr: KeyResult = {
149
+ description: 'Test',
150
+ metric: 'NPS',
151
+ startValue: 0,
152
+ targetValue: 100,
153
+ currentValue: 150,
154
+ }
155
+ expect(calculateKeyResultProgress(kr)).toBe(100)
156
+ })
157
+
158
+ it('should cap progress at 0', () => {
159
+ const kr: KeyResult = {
160
+ description: 'Test',
161
+ metric: 'NPS',
162
+ startValue: 50,
163
+ targetValue: 100,
164
+ currentValue: 30,
165
+ }
166
+ expect(calculateKeyResultProgress(kr)).toBe(0)
167
+ })
168
+
169
+ it('should handle decreasing metrics (e.g., churn)', () => {
170
+ // For metrics where lower is better (churn: 8% -> 4%), we should still
171
+ // express them as start/target going down
172
+ const kr: KeyResult = {
173
+ description: 'Reduce churn',
174
+ metric: 'Churn',
175
+ startValue: 8,
176
+ targetValue: 4,
177
+ currentValue: 6,
178
+ }
179
+ expect(calculateKeyResultProgress(kr)).toBe(50)
180
+ })
181
+ })
182
+
183
+ describe('calculateOKRProgress()', () => {
184
+ it('should calculate average progress across key results', () => {
185
+ const testOKR: OKRDefinition = {
186
+ objective: 'Test Objective for Progress',
187
+ keyResults: [
188
+ {
189
+ description: 'KR 1',
190
+ metric: 'M1',
191
+ startValue: 0,
192
+ targetValue: 100,
193
+ currentValue: 50,
194
+ progress: 50,
195
+ },
196
+ {
197
+ description: 'KR 2',
198
+ metric: 'M2',
199
+ startValue: 0,
200
+ targetValue: 100,
201
+ currentValue: 80,
202
+ progress: 80,
203
+ },
204
+ ],
205
+ }
206
+
207
+ expect(calculateOKRProgress(testOKR)).toBe(65)
208
+ })
209
+
210
+ it('should return 0 for OKR without key results', () => {
211
+ const testOKR: OKRDefinition = { objective: 'Test Objective for Progress' }
212
+ expect(calculateOKRProgress(testOKR)).toBe(0)
213
+ })
214
+
215
+ it('should return 0 for empty key results', () => {
216
+ const testOKR: OKRDefinition = { objective: 'Test Objective for Progress', keyResults: [] }
217
+ expect(calculateOKRProgress(testOKR)).toBe(0)
218
+ })
219
+
220
+ it('should calculate progress from values when progress not set', () => {
221
+ const testOKR: OKRDefinition = {
222
+ objective: 'Test Objective for Progress',
223
+ keyResults: [
224
+ { description: 'KR 1', metric: 'M1', startValue: 0, targetValue: 100, currentValue: 50 },
225
+ { description: 'KR 2', metric: 'M2', startValue: 0, targetValue: 100, currentValue: 100 },
226
+ ],
227
+ }
228
+
229
+ expect(calculateOKRProgress(testOKR)).toBe(75)
230
+ })
231
+ })
232
+
233
+ describe('calculateConfidence()', () => {
234
+ it('should calculate confidence from key results', () => {
235
+ const keyResults: KeyResult[] = [
236
+ {
237
+ description: 'KR 1',
238
+ metric: 'M1',
239
+ startValue: 0,
240
+ targetValue: 100,
241
+ currentValue: 70,
242
+ progress: 70,
243
+ },
244
+ {
245
+ description: 'KR 2',
246
+ metric: 'M2',
247
+ startValue: 0,
248
+ targetValue: 100,
249
+ currentValue: 80,
250
+ progress: 80,
251
+ },
252
+ ]
253
+
254
+ // Average progress = 75, confidence = 75 - 10 = 65
255
+ expect(calculateConfidence(keyResults)).toBe(65)
256
+ })
257
+
258
+ it('should return 0 for empty key results', () => {
259
+ expect(calculateConfidence([])).toBe(0)
260
+ })
261
+
262
+ it('should cap confidence at 0', () => {
263
+ const keyResults: KeyResult[] = [{ description: 'KR 1', metric: 'M1', progress: 5 }]
264
+
265
+ expect(calculateConfidence(keyResults)).toBe(0)
266
+ })
267
+
268
+ it('should cap confidence at 100', () => {
269
+ const keyResults: KeyResult[] = [
270
+ { description: 'KR 1', metric: 'M1', progress: 100 },
271
+ { description: 'KR 2', metric: 'M2', progress: 100 },
272
+ ]
273
+
274
+ expect(calculateConfidence(keyResults)).toBe(90)
275
+ })
276
+ })
277
+
278
+ describe('updateKeyResult()', () => {
279
+ const testOKR: OKRDefinition = {
280
+ objective: 'Test Objective for Update',
281
+ keyResults: [
282
+ {
283
+ description: 'KR 1',
284
+ metric: 'M1',
285
+ startValue: 0,
286
+ targetValue: 100,
287
+ currentValue: 50,
288
+ progress: 50,
289
+ },
290
+ {
291
+ description: 'KR 2',
292
+ metric: 'M2',
293
+ startValue: 0,
294
+ targetValue: 100,
295
+ currentValue: 60,
296
+ progress: 60,
297
+ },
298
+ ],
299
+ }
300
+
301
+ it('should update key result current value', () => {
302
+ const updated = updateKeyResult(testOKR, 'KR 1', 75)
303
+ const kr = updated.keyResults?.find((k) => k.description === 'KR 1')
304
+
305
+ expect(kr?.currentValue).toBe(75)
306
+ expect(kr?.progress).toBe(75)
307
+ })
308
+
309
+ it('should not affect other key results', () => {
310
+ const updated = updateKeyResult(testOKR, 'KR 1', 75)
311
+ const kr2 = updated.keyResults?.find((k) => k.description === 'KR 2')
312
+
313
+ expect(kr2?.currentValue).toBe(60)
314
+ expect(kr2?.progress).toBe(60)
315
+ })
316
+
317
+ it('should recalculate OKR confidence', () => {
318
+ const updated = updateKeyResult(testOKR, 'KR 1', 100)
319
+
320
+ // Progress for KR1 = 100, KR2 = 60, avg = 80
321
+ // Confidence = 80 - 10 = 70
322
+ expect(updated.confidence).toBe(70)
323
+ })
324
+
325
+ it('should handle non-existent key result', () => {
326
+ const updated = updateKeyResult(testOKR, 'NonExistent', 75)
327
+ expect(updated.keyResults).toHaveLength(2)
328
+ })
329
+ })
330
+
331
+ describe('isKeyResultOnTrack()', () => {
332
+ it('should return true for progress >= 70', () => {
333
+ const kr: KeyResult = { description: 'Test', metric: 'M1', progress: 70 }
334
+ expect(isKeyResultOnTrack(kr)).toBe(true)
335
+ })
336
+
337
+ it('should return true for progress > 70', () => {
338
+ const kr: KeyResult = { description: 'Test', metric: 'M1', progress: 85 }
339
+ expect(isKeyResultOnTrack(kr)).toBe(true)
340
+ })
341
+
342
+ it('should return false for progress < 70', () => {
343
+ const kr: KeyResult = { description: 'Test', metric: 'M1', progress: 50 }
344
+ expect(isKeyResultOnTrack(kr)).toBe(false)
345
+ })
346
+
347
+ it('should calculate progress if not set', () => {
348
+ const kr: KeyResult = {
349
+ description: 'Test',
350
+ metric: 'M1',
351
+ startValue: 0,
352
+ targetValue: 100,
353
+ currentValue: 80,
354
+ }
355
+ expect(isKeyResultOnTrack(kr)).toBe(true)
356
+ })
357
+ })
358
+
359
+ describe('isOKROnTrack()', () => {
360
+ it('should return true for high progress and confidence', () => {
361
+ const testOKR: OKRDefinition = {
362
+ objective: 'Test Objective On Track',
363
+ keyResults: [
364
+ { description: 'KR 1', metric: 'M1', progress: 80 },
365
+ { description: 'KR 2', metric: 'M2', progress: 75 },
366
+ ],
367
+ confidence: 70,
368
+ }
369
+
370
+ expect(isOKROnTrack(testOKR)).toBe(true)
371
+ })
372
+
373
+ it('should return false for low progress', () => {
374
+ const testOKR: OKRDefinition = {
375
+ objective: 'Test Objective Low Progress',
376
+ keyResults: [
377
+ { description: 'KR 1', metric: 'M1', progress: 50 },
378
+ { description: 'KR 2', metric: 'M2', progress: 40 },
379
+ ],
380
+ confidence: 70,
381
+ }
382
+
383
+ expect(isOKROnTrack(testOKR)).toBe(false)
384
+ })
385
+
386
+ it('should return false for low confidence', () => {
387
+ const testOKR: OKRDefinition = {
388
+ objective: 'Test Objective Low Confidence',
389
+ keyResults: [
390
+ { description: 'KR 1', metric: 'M1', progress: 80 },
391
+ { description: 'KR 2', metric: 'M2', progress: 75 },
392
+ ],
393
+ confidence: 50,
394
+ }
395
+
396
+ expect(isOKROnTrack(testOKR)).toBe(false)
397
+ })
398
+ })
399
+
400
+ describe('getKeyResultsOnTrack()', () => {
401
+ const testOKR: OKRDefinition = {
402
+ objective: 'Test Objective with Mixed KRs',
403
+ keyResults: [
404
+ { description: 'High progress KR', metric: 'M1', progress: 80 },
405
+ { description: 'Low progress KR', metric: 'M2', progress: 50 },
406
+ { description: 'On track KR', metric: 'M3', progress: 70 },
407
+ ],
408
+ }
409
+
410
+ it('should return key results with progress >= 70', () => {
411
+ const onTrack = getKeyResultsOnTrack(testOKR)
412
+
413
+ expect(onTrack).toHaveLength(2)
414
+ expect(onTrack[0]?.description).toBe('High progress KR')
415
+ expect(onTrack[1]?.description).toBe('On track KR')
416
+ })
417
+
418
+ it('should return empty array for OKR without key results', () => {
419
+ const emptyOKR: OKRDefinition = { objective: 'Empty OKR for Filter' }
420
+ expect(getKeyResultsOnTrack(emptyOKR)).toEqual([])
421
+ })
422
+ })
423
+
424
+ describe('getKeyResultsAtRisk()', () => {
425
+ const testOKR: OKRDefinition = {
426
+ objective: 'Test Objective with At-Risk KRs',
427
+ keyResults: [
428
+ { description: 'High progress KR', metric: 'M1', progress: 80 },
429
+ { description: 'Low progress KR', metric: 'M2', progress: 50 },
430
+ { description: 'At risk KR', metric: 'M3', progress: 30 },
431
+ ],
432
+ }
433
+
434
+ it('should return key results with progress < 70', () => {
435
+ const atRisk = getKeyResultsAtRisk(testOKR)
436
+
437
+ expect(atRisk).toHaveLength(2)
438
+ expect(atRisk[0]?.description).toBe('Low progress KR')
439
+ expect(atRisk[1]?.description).toBe('At risk KR')
440
+ })
441
+
442
+ it('should return empty array for OKR without key results', () => {
443
+ const emptyOKR: OKRDefinition = { objective: 'Empty OKR for At-Risk' }
444
+ expect(getKeyResultsAtRisk(emptyOKR)).toEqual([])
445
+ })
446
+ })
447
+
448
+ describe('getOKRsByOwner()', () => {
449
+ const okrList: OKRDefinition[] = [
450
+ { objective: 'Engineering Objective One', owner: 'Engineering' },
451
+ { objective: 'Sales Objective One Item', owner: 'Sales' },
452
+ { objective: 'Engineering Objective Two', owner: 'Engineering' },
453
+ ]
454
+
455
+ it('should filter OKRs by owner', () => {
456
+ const engineering = getOKRsByOwner(okrList, 'Engineering')
457
+
458
+ expect(engineering).toHaveLength(2)
459
+ expect(engineering[0]?.objective).toBe('Engineering Objective One')
460
+ expect(engineering[1]?.objective).toBe('Engineering Objective Two')
461
+ })
462
+
463
+ it('should return empty array for non-existent owner', () => {
464
+ const marketing = getOKRsByOwner(okrList, 'Marketing')
465
+ expect(marketing).toHaveLength(0)
466
+ })
467
+ })
468
+
469
+ describe('getOKRsByPeriod()', () => {
470
+ const okrList: OKRDefinition[] = [
471
+ { objective: 'Q1 Objective Example', period: 'Q1 2024' },
472
+ { objective: 'Q2 Objective Example', period: 'Q2 2024' },
473
+ { objective: 'Q1 Objective Second', period: 'Q1 2024' },
474
+ ]
475
+
476
+ it('should filter OKRs by period', () => {
477
+ const q1 = getOKRsByPeriod(okrList, 'Q1 2024')
478
+
479
+ expect(q1).toHaveLength(2)
480
+ expect(q1[0]?.objective).toBe('Q1 Objective Example')
481
+ expect(q1[1]?.objective).toBe('Q1 Objective Second')
482
+ })
483
+
484
+ it('should return empty array for non-existent period', () => {
485
+ const q4 = getOKRsByPeriod(okrList, 'Q4 2024')
486
+ expect(q4).toHaveLength(0)
487
+ })
488
+ })
489
+
490
+ describe('getOKRsByStatus()', () => {
491
+ const okrList: OKRDefinition[] = [
492
+ { objective: 'On Track Objective Example', status: 'on-track' },
493
+ { objective: 'At Risk Objective Example', status: 'at-risk' },
494
+ { objective: 'On Track Objective Second', status: 'on-track' },
495
+ { objective: 'Completed Objective Done', status: 'completed' },
496
+ ]
497
+
498
+ it('should filter OKRs by status', () => {
499
+ const onTrack = getOKRsByStatus(okrList, 'on-track')
500
+
501
+ expect(onTrack).toHaveLength(2)
502
+ expect(onTrack[0]?.objective).toBe('On Track Objective Example')
503
+ expect(onTrack[1]?.objective).toBe('On Track Objective Second')
504
+ })
505
+
506
+ it('should return empty array for non-existent status', () => {
507
+ const notStarted = getOKRsByStatus(okrList, 'not-started')
508
+ expect(notStarted).toHaveLength(0)
509
+ })
510
+ })
511
+
512
+ describe('calculateSuccessRate()', () => {
513
+ it('should calculate average progress across OKRs', () => {
514
+ const okrList: OKRDefinition[] = [
515
+ {
516
+ objective: 'OKR 1 for Success Rate',
517
+ keyResults: [{ description: 'KR 1', metric: 'M1', progress: 80 }],
518
+ },
519
+ {
520
+ objective: 'OKR 2 for Success Rate',
521
+ keyResults: [{ description: 'KR 1', metric: 'M1', progress: 60 }],
522
+ },
523
+ ]
524
+
525
+ expect(calculateSuccessRate(okrList)).toBe(70)
526
+ })
527
+
528
+ it('should return 0 for empty OKR list', () => {
529
+ expect(calculateSuccessRate([])).toBe(0)
530
+ })
531
+ })
532
+
533
+ describe('formatKeyResult()', () => {
534
+ it('should format key result for display', () => {
535
+ const kr: KeyResult = {
536
+ description: 'Increase NPS',
537
+ metric: 'NPS',
538
+ startValue: 40,
539
+ targetValue: 60,
540
+ currentValue: 50,
541
+ unit: 'score',
542
+ progress: 50,
543
+ }
544
+
545
+ const formatted = formatKeyResult(kr)
546
+ expect(formatted).toBe('Increase NPS: 50/60 score (50%)')
547
+ })
548
+
549
+ it('should handle missing progress', () => {
550
+ const kr: KeyResult = {
551
+ description: 'Increase NPS',
552
+ metric: 'NPS',
553
+ startValue: 40,
554
+ targetValue: 60,
555
+ currentValue: 50,
556
+ unit: 'score',
557
+ }
558
+
559
+ const formatted = formatKeyResult(kr)
560
+ expect(formatted).toBe('Increase NPS: 50/60 score (50%)')
561
+ })
562
+
563
+ it('should handle missing unit', () => {
564
+ const kr: KeyResult = {
565
+ description: 'Increase NPS',
566
+ metric: 'NPS',
567
+ startValue: 40,
568
+ targetValue: 60,
569
+ currentValue: 50,
570
+ progress: 50,
571
+ }
572
+
573
+ const formatted = formatKeyResult(kr)
574
+ expect(formatted).toBe('Increase NPS: 50/60 (50%)')
575
+ })
576
+ })
577
+
578
+ describe('compareOKRPerformance()', () => {
579
+ it('should compare OKR performance between periods', () => {
580
+ const current: OKRDefinition = {
581
+ objective: 'Current Period Objective',
582
+ keyResults: [{ description: 'KR 1', metric: 'M1', progress: 80 }],
583
+ confidence: 70,
584
+ }
585
+
586
+ const previous: OKRDefinition = {
587
+ objective: 'Previous Period Objective',
588
+ keyResults: [{ description: 'KR 1', metric: 'M1', progress: 60 }],
589
+ confidence: 60,
590
+ }
591
+
592
+ const comparison = compareOKRPerformance(current, previous)
593
+
594
+ expect(comparison.progressDelta).toBe(20)
595
+ expect(comparison.confidenceDelta).toBe(10)
596
+ expect(comparison.improved).toBe(true)
597
+ })
598
+
599
+ it('should detect regression', () => {
600
+ const current: OKRDefinition = {
601
+ objective: 'Current Period Regression',
602
+ keyResults: [{ description: 'KR 1', metric: 'M1', progress: 50 }],
603
+ confidence: 40,
604
+ }
605
+
606
+ const previous: OKRDefinition = {
607
+ objective: 'Previous Period Regression',
608
+ keyResults: [{ description: 'KR 1', metric: 'M1', progress: 70 }],
609
+ confidence: 60,
610
+ }
611
+
612
+ const comparison = compareOKRPerformance(current, previous)
613
+
614
+ expect(comparison.progressDelta).toBe(-20)
615
+ expect(comparison.confidenceDelta).toBe(-20)
616
+ expect(comparison.improved).toBe(false)
617
+ })
618
+ })
619
+
620
+ describe('validateOKRs()', () => {
621
+ it('should validate valid OKRs', () => {
622
+ const okrList: OKRDefinition[] = [
623
+ {
624
+ objective: 'Valid Objective with Proper Length',
625
+ keyResults: [
626
+ { description: 'KR 1', metric: 'M1', progress: 50 },
627
+ { description: 'KR 2', metric: 'M2', progress: 60 },
628
+ ],
629
+ confidence: 50,
630
+ },
631
+ ]
632
+
633
+ const result = validateOKRs(okrList)
634
+ expect(result.valid).toBe(true)
635
+ expect(result.errors).toHaveLength(0)
636
+ })
637
+
638
+ it('should fail if objective is missing', () => {
639
+ const okrList: OKRDefinition[] = [{ objective: '' }]
640
+
641
+ const result = validateOKRs(okrList)
642
+ expect(result.valid).toBe(false)
643
+ expect(result.errors).toContain('OKR objective is required')
644
+ })
645
+
646
+ it('should fail if objective is too short', () => {
647
+ const okrList: OKRDefinition[] = [{ objective: 'Short' }]
648
+
649
+ const result = validateOKRs(okrList)
650
+ expect(result.valid).toBe(false)
651
+ expect(result.errors).toContain('OKR objective "Short" should be at least 10 characters')
652
+ })
653
+
654
+ it('should fail if confidence is out of range', () => {
655
+ const okrList: OKRDefinition[] = [
656
+ { objective: 'Valid Objective for Confidence Test', confidence: 150 },
657
+ ]
658
+
659
+ const result = validateOKRs(okrList)
660
+ expect(result.valid).toBe(false)
661
+ expect(result.errors).toContain(
662
+ 'OKR "Valid Objective for Confidence Test" confidence must be between 0 and 100'
663
+ )
664
+ })
665
+
666
+ it('should fail if key results array is empty', () => {
667
+ const okrList: OKRDefinition[] = [
668
+ { objective: 'Objective with Empty Key Results', keyResults: [] },
669
+ ]
670
+
671
+ const result = validateOKRs(okrList)
672
+ expect(result.valid).toBe(false)
673
+ expect(result.errors).toContain(
674
+ 'OKR "Objective with Empty Key Results" must have at least one key result'
675
+ )
676
+ })
677
+
678
+ it('should fail if too many key results', () => {
679
+ const okrList: OKRDefinition[] = [
680
+ {
681
+ objective: 'Objective with Too Many Key Results',
682
+ keyResults: [
683
+ { description: 'KR 1', metric: 'M1' },
684
+ { description: 'KR 2', metric: 'M2' },
685
+ { description: 'KR 3', metric: 'M3' },
686
+ { description: 'KR 4', metric: 'M4' },
687
+ { description: 'KR 5', metric: 'M5' },
688
+ { description: 'KR 6', metric: 'M6' },
689
+ ],
690
+ },
691
+ ]
692
+
693
+ const result = validateOKRs(okrList)
694
+ expect(result.valid).toBe(false)
695
+ expect(result.errors).toContain(
696
+ 'OKR "Objective with Too Many Key Results" should have no more than 5 key results'
697
+ )
698
+ })
699
+
700
+ it('should fail if key result description is missing', () => {
701
+ const okrList: OKRDefinition[] = [
702
+ {
703
+ objective: 'Objective with Invalid Key Result',
704
+ keyResults: [{ description: '', metric: 'M1' }],
705
+ },
706
+ ]
707
+
708
+ const result = validateOKRs(okrList)
709
+ expect(result.valid).toBe(false)
710
+ expect(result.errors).toContain(
711
+ 'Key result in OKR "Objective with Invalid Key Result" must have a description'
712
+ )
713
+ })
714
+
715
+ it('should fail if key result metric is missing', () => {
716
+ const okrList: OKRDefinition[] = [
717
+ {
718
+ objective: 'Objective with Missing Metric',
719
+ keyResults: [{ description: 'KR 1', metric: '' }],
720
+ },
721
+ ]
722
+
723
+ const result = validateOKRs(okrList)
724
+ expect(result.valid).toBe(false)
725
+ expect(result.errors).toContain('Key result "KR 1" must have a metric')
726
+ })
727
+
728
+ it('should fail if key result progress is out of range', () => {
729
+ const okrList: OKRDefinition[] = [
730
+ {
731
+ objective: 'Objective with Invalid Progress',
732
+ keyResults: [{ description: 'KR 1', metric: 'M1', progress: 150 }],
733
+ },
734
+ ]
735
+
736
+ const result = validateOKRs(okrList)
737
+ expect(result.valid).toBe(false)
738
+ expect(result.errors).toContain('Key result "KR 1" progress must be between 0 and 100')
739
+ })
740
+ })
741
+ })