business-as-code 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.
Files changed (260) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -0
  3. package/package.json +16 -13
  4. package/src/dollar.ts +5 -2
  5. package/src/entities/organization.ts +31 -18
  6. package/src/goals.ts +78 -12
  7. package/src/index.ts +48 -18
  8. package/src/kpis.ts +62 -8
  9. package/src/metrics.ts +92 -79
  10. package/src/okrs.ts +120 -20
  11. package/src/organization.ts +12 -15
  12. package/src/process.ts +11 -12
  13. package/src/product.ts +8 -9
  14. package/src/queries.ts +238 -75
  15. package/src/roles.ts +62 -61
  16. package/src/workflow.ts +22 -15
  17. package/test/business.test.ts +282 -0
  18. package/test/dollar.test.ts +270 -0
  19. package/test/entities.test.ts +628 -0
  20. package/test/financials.test.ts +539 -0
  21. package/test/goals.test.ts +451 -0
  22. package/{src → test}/index.test.ts +1 -1
  23. package/test/kpis.test.ts +440 -0
  24. package/test/metrics.test.ts +744 -0
  25. package/test/okrs.test.ts +741 -0
  26. package/test/organization.test.ts +548 -0
  27. package/test/process.test.ts +503 -0
  28. package/test/product.test.ts +430 -0
  29. package/test/queries.test.ts +556 -0
  30. package/test/roles.test.ts +546 -0
  31. package/test/service.test.ts +450 -0
  32. package/test/types.test.ts +1141 -0
  33. package/test/vision.test.ts +214 -0
  34. package/test/workflow.test.ts +501 -0
  35. package/vitest.config.ts +47 -0
  36. package/.turbo/turbo-build.log +0 -5
  37. package/LICENSE +0 -21
  38. package/dist/business.d.ts +0 -62
  39. package/dist/business.d.ts.map +0 -1
  40. package/dist/business.js +0 -109
  41. package/dist/business.js.map +0 -1
  42. package/dist/canvas/activities.d.ts +0 -19
  43. package/dist/canvas/activities.d.ts.map +0 -1
  44. package/dist/canvas/activities.js +0 -20
  45. package/dist/canvas/activities.js.map +0 -1
  46. package/dist/canvas/channels.d.ts +0 -20
  47. package/dist/canvas/channels.d.ts.map +0 -1
  48. package/dist/canvas/channels.js +0 -21
  49. package/dist/canvas/channels.js.map +0 -1
  50. package/dist/canvas/relationships.d.ts +0 -20
  51. package/dist/canvas/relationships.d.ts.map +0 -1
  52. package/dist/canvas/relationships.js +0 -21
  53. package/dist/canvas/relationships.js.map +0 -1
  54. package/dist/canvas/resources.d.ts +0 -20
  55. package/dist/canvas/resources.d.ts.map +0 -1
  56. package/dist/canvas/resources.js +0 -30
  57. package/dist/canvas/resources.js.map +0 -1
  58. package/dist/canvas/revenue.d.ts +0 -22
  59. package/dist/canvas/revenue.d.ts.map +0 -1
  60. package/dist/canvas/revenue.js +0 -30
  61. package/dist/canvas/revenue.js.map +0 -1
  62. package/dist/canvas/segments.d.ts +0 -20
  63. package/dist/canvas/segments.d.ts.map +0 -1
  64. package/dist/canvas/segments.js +0 -28
  65. package/dist/canvas/segments.js.map +0 -1
  66. package/dist/canvas/types.d.ts +0 -232
  67. package/dist/canvas/types.d.ts.map +0 -1
  68. package/dist/canvas/types.js +0 -8
  69. package/dist/canvas/types.js.map +0 -1
  70. package/dist/canvas/value.d.ts +0 -20
  71. package/dist/canvas/value.d.ts.map +0 -1
  72. package/dist/canvas/value.js +0 -21
  73. package/dist/canvas/value.js.map +0 -1
  74. package/dist/dollar.d.ts +0 -60
  75. package/dist/dollar.d.ts.map +0 -1
  76. package/dist/dollar.js +0 -107
  77. package/dist/dollar.js.map +0 -1
  78. package/dist/entities/assets.d.ts +0 -21
  79. package/dist/entities/assets.d.ts.map +0 -1
  80. package/dist/entities/assets.js +0 -323
  81. package/dist/entities/assets.js.map +0 -1
  82. package/dist/entities/business.d.ts +0 -36
  83. package/dist/entities/business.d.ts.map +0 -1
  84. package/dist/entities/business.js +0 -370
  85. package/dist/entities/business.js.map +0 -1
  86. package/dist/entities/communication.d.ts +0 -21
  87. package/dist/entities/communication.d.ts.map +0 -1
  88. package/dist/entities/communication.js +0 -255
  89. package/dist/entities/communication.js.map +0 -1
  90. package/dist/entities/customers.d.ts +0 -58
  91. package/dist/entities/customers.d.ts.map +0 -1
  92. package/dist/entities/customers.js +0 -989
  93. package/dist/entities/customers.js.map +0 -1
  94. package/dist/entities/financials.d.ts +0 -59
  95. package/dist/entities/financials.d.ts.map +0 -1
  96. package/dist/entities/financials.js +0 -932
  97. package/dist/entities/financials.js.map +0 -1
  98. package/dist/entities/goals.d.ts +0 -58
  99. package/dist/entities/goals.d.ts.map +0 -1
  100. package/dist/entities/goals.js +0 -800
  101. package/dist/entities/goals.js.map +0 -1
  102. package/dist/entities/index.d.ts +0 -299
  103. package/dist/entities/index.d.ts.map +0 -1
  104. package/dist/entities/index.js +0 -198
  105. package/dist/entities/index.js.map +0 -1
  106. package/dist/entities/legal.d.ts +0 -21
  107. package/dist/entities/legal.d.ts.map +0 -1
  108. package/dist/entities/legal.js +0 -301
  109. package/dist/entities/legal.js.map +0 -1
  110. package/dist/entities/market.d.ts +0 -21
  111. package/dist/entities/market.d.ts.map +0 -1
  112. package/dist/entities/market.js +0 -301
  113. package/dist/entities/market.js.map +0 -1
  114. package/dist/entities/marketing.d.ts +0 -67
  115. package/dist/entities/marketing.d.ts.map +0 -1
  116. package/dist/entities/marketing.js +0 -1157
  117. package/dist/entities/marketing.js.map +0 -1
  118. package/dist/entities/offerings.d.ts +0 -51
  119. package/dist/entities/offerings.d.ts.map +0 -1
  120. package/dist/entities/offerings.js +0 -727
  121. package/dist/entities/offerings.js.map +0 -1
  122. package/dist/entities/operations.d.ts +0 -58
  123. package/dist/entities/operations.d.ts.map +0 -1
  124. package/dist/entities/operations.js +0 -787
  125. package/dist/entities/operations.js.map +0 -1
  126. package/dist/entities/organization.d.ts +0 -57
  127. package/dist/entities/organization.d.ts.map +0 -1
  128. package/dist/entities/organization.js +0 -807
  129. package/dist/entities/organization.js.map +0 -1
  130. package/dist/entities/partnerships.d.ts +0 -21
  131. package/dist/entities/partnerships.d.ts.map +0 -1
  132. package/dist/entities/partnerships.js +0 -300
  133. package/dist/entities/partnerships.js.map +0 -1
  134. package/dist/entities/planning.d.ts +0 -0
  135. package/dist/entities/planning.d.ts.map +0 -1
  136. package/dist/entities/planning.js +0 -271
  137. package/dist/entities/planning.js.map +0 -1
  138. package/dist/entities/projects.d.ts +0 -25
  139. package/dist/entities/projects.d.ts.map +0 -1
  140. package/dist/entities/projects.js +0 -349
  141. package/dist/entities/projects.js.map +0 -1
  142. package/dist/entities/risk.d.ts +0 -21
  143. package/dist/entities/risk.d.ts.map +0 -1
  144. package/dist/entities/risk.js +0 -293
  145. package/dist/entities/risk.js.map +0 -1
  146. package/dist/entities/sales.d.ts +0 -72
  147. package/dist/entities/sales.d.ts.map +0 -1
  148. package/dist/entities/sales.js +0 -1248
  149. package/dist/entities/sales.js.map +0 -1
  150. package/dist/financials.d.ts +0 -130
  151. package/dist/financials.d.ts.map +0 -1
  152. package/dist/financials.js +0 -297
  153. package/dist/financials.js.map +0 -1
  154. package/dist/goals.d.ts +0 -87
  155. package/dist/goals.d.ts.map +0 -1
  156. package/dist/goals.js +0 -215
  157. package/dist/goals.js.map +0 -1
  158. package/dist/index.d.ts +0 -97
  159. package/dist/index.d.ts.map +0 -1
  160. package/dist/index.js +0 -132
  161. package/dist/index.js.map +0 -1
  162. package/dist/kpis.d.ts +0 -118
  163. package/dist/kpis.d.ts.map +0 -1
  164. package/dist/kpis.js +0 -232
  165. package/dist/kpis.js.map +0 -1
  166. package/dist/metrics.d.ts +0 -448
  167. package/dist/metrics.d.ts.map +0 -1
  168. package/dist/metrics.js +0 -325
  169. package/dist/metrics.js.map +0 -1
  170. package/dist/okrs.d.ts +0 -123
  171. package/dist/okrs.d.ts.map +0 -1
  172. package/dist/okrs.js +0 -269
  173. package/dist/okrs.js.map +0 -1
  174. package/dist/organization.d.ts +0 -585
  175. package/dist/organization.d.ts.map +0 -1
  176. package/dist/organization.js +0 -173
  177. package/dist/organization.js.map +0 -1
  178. package/dist/process.d.ts +0 -112
  179. package/dist/process.d.ts.map +0 -1
  180. package/dist/process.js +0 -241
  181. package/dist/process.js.map +0 -1
  182. package/dist/product.d.ts +0 -85
  183. package/dist/product.d.ts.map +0 -1
  184. package/dist/product.js +0 -145
  185. package/dist/product.js.map +0 -1
  186. package/dist/queries.d.ts +0 -304
  187. package/dist/queries.d.ts.map +0 -1
  188. package/dist/queries.js +0 -415
  189. package/dist/queries.js.map +0 -1
  190. package/dist/roles.d.ts +0 -340
  191. package/dist/roles.d.ts.map +0 -1
  192. package/dist/roles.js +0 -255
  193. package/dist/roles.js.map +0 -1
  194. package/dist/service.d.ts +0 -61
  195. package/dist/service.d.ts.map +0 -1
  196. package/dist/service.js +0 -140
  197. package/dist/service.js.map +0 -1
  198. package/dist/types.d.ts +0 -459
  199. package/dist/types.d.ts.map +0 -1
  200. package/dist/types.js +0 -5
  201. package/dist/types.js.map +0 -1
  202. package/dist/vision.d.ts +0 -38
  203. package/dist/vision.d.ts.map +0 -1
  204. package/dist/vision.js +0 -68
  205. package/dist/vision.js.map +0 -1
  206. package/dist/workflow.d.ts +0 -115
  207. package/dist/workflow.d.ts.map +0 -1
  208. package/dist/workflow.js +0 -247
  209. package/dist/workflow.js.map +0 -1
  210. package/src/business.js +0 -108
  211. package/src/canvas/activities.ts +0 -32
  212. package/src/canvas/canvas.ts +0 -482
  213. package/src/canvas/channels.ts +0 -34
  214. package/src/canvas/costs.ts +0 -43
  215. package/src/canvas/economics.ts +0 -99
  216. package/src/canvas/index.ts +0 -206
  217. package/src/canvas/partnerships.ts +0 -34
  218. package/src/canvas/projections.ts +0 -141
  219. package/src/canvas/relationships.ts +0 -34
  220. package/src/canvas/resources.ts +0 -43
  221. package/src/canvas/revenue.ts +0 -56
  222. package/src/canvas/segments.ts +0 -42
  223. package/src/canvas/types.ts +0 -363
  224. package/src/canvas/value.ts +0 -34
  225. package/src/dollar.js +0 -106
  226. package/src/entities/assets.js +0 -322
  227. package/src/entities/business.js +0 -369
  228. package/src/entities/communication.js +0 -254
  229. package/src/entities/customers.js +0 -988
  230. package/src/entities/financials.js +0 -931
  231. package/src/entities/goals.js +0 -799
  232. package/src/entities/index.js +0 -197
  233. package/src/entities/legal.js +0 -300
  234. package/src/entities/market.js +0 -300
  235. package/src/entities/marketing.js +0 -1156
  236. package/src/entities/offerings.js +0 -726
  237. package/src/entities/operations.js +0 -786
  238. package/src/entities/organization.js +0 -806
  239. package/src/entities/partnerships.js +0 -299
  240. package/src/entities/planning.js +0 -270
  241. package/src/entities/projects.js +0 -348
  242. package/src/entities/risk.js +0 -292
  243. package/src/entities/sales.js +0 -1247
  244. package/src/financials.js +0 -296
  245. package/src/goals.js +0 -214
  246. package/src/index.js +0 -131
  247. package/src/index.test.js +0 -274
  248. package/src/kpis.js +0 -231
  249. package/src/metrics.js +0 -324
  250. package/src/okrs.js +0 -268
  251. package/src/organization.js +0 -172
  252. package/src/process.js +0 -240
  253. package/src/product.js +0 -144
  254. package/src/queries.js +0 -414
  255. package/src/roles.js +0 -254
  256. package/src/service.js +0 -139
  257. package/src/types.js +0 -4
  258. package/src/vision.js +0 -67
  259. package/src/workflow.js +0 -246
  260. 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
+ })