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,546 @@
1
+ /**
2
+ * Tests for roles.ts - Business roles and task assignment
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import {
7
+ StandardBusinessRoles,
8
+ createBusinessRole,
9
+ hasPermission,
10
+ canHandleTask,
11
+ canApproveRequest,
12
+ canDelegateTask,
13
+ findRoleForTask,
14
+ createTaskAssignment,
15
+ transitionTaskStatus,
16
+ } from '../src/roles.js'
17
+ import type {
18
+ BusinessRole,
19
+ TaskRoutingRule,
20
+ TaskAssignment,
21
+ AssigneeRef,
22
+ WorkflowRole,
23
+ } from '../src/roles.js'
24
+
25
+ describe('Roles', () => {
26
+ describe('StandardBusinessRoles', () => {
27
+ it('should have executive roles', () => {
28
+ expect(StandardBusinessRoles.ceo).toBeDefined()
29
+ expect(StandardBusinessRoles.cto).toBeDefined()
30
+ expect(StandardBusinessRoles.cfo).toBeDefined()
31
+ })
32
+
33
+ it('should have management roles', () => {
34
+ expect(StandardBusinessRoles.director).toBeDefined()
35
+ expect(StandardBusinessRoles.manager).toBeDefined()
36
+ expect(StandardBusinessRoles.lead).toBeDefined()
37
+ })
38
+
39
+ it('should have individual contributor roles', () => {
40
+ expect(StandardBusinessRoles.engineer).toBeDefined()
41
+ expect(StandardBusinessRoles.analyst).toBeDefined()
42
+ })
43
+
44
+ it('should have operations roles', () => {
45
+ expect(StandardBusinessRoles.agent).toBeDefined()
46
+ expect(StandardBusinessRoles.assistant).toBeDefined()
47
+ })
48
+
49
+ it('CEO should have manage all permissions', () => {
50
+ const ceo = StandardBusinessRoles.ceo
51
+ expect(ceo?.permissions?.['*']).toContain('manage')
52
+ })
53
+
54
+ it('CTO should have technology permissions', () => {
55
+ const cto = StandardBusinessRoles.cto
56
+ expect(cto?.permissions?.technology).toContain('manage')
57
+ expect(cto?.permissions?.repository).toContain('manage')
58
+ })
59
+
60
+ it('engineer should have repository permissions', () => {
61
+ const engineer = StandardBusinessRoles.engineer
62
+ expect(engineer?.permissions?.repository).toContain('read')
63
+ expect(engineer?.permissions?.repository).toContain('edit')
64
+ })
65
+
66
+ it('agent should be AI-first', () => {
67
+ const agent = StandardBusinessRoles.agent
68
+ expect(agent?.workerType).toBe('ai')
69
+ })
70
+
71
+ it('assistant should be AI-first', () => {
72
+ const assistant = StandardBusinessRoles.assistant
73
+ expect(assistant?.workerType).toBe('ai')
74
+ })
75
+ })
76
+
77
+ describe('createBusinessRole()', () => {
78
+ it('should create role from standard template', () => {
79
+ const role = createBusinessRole('role_cto_jane', 'cto')
80
+
81
+ expect(role.id).toBe('role_cto_jane')
82
+ expect(role.name).toBe('Chief Technology Officer')
83
+ expect(role.type).toBe('cto')
84
+ expect(role.level).toBe(10)
85
+ })
86
+
87
+ it('should allow overrides', () => {
88
+ const role = createBusinessRole('role_engineer_custom', 'engineer', {
89
+ name: 'Full Stack Engineer',
90
+ level: 4,
91
+ skills: ['TypeScript', 'React', 'Node.js'],
92
+ })
93
+
94
+ expect(role.name).toBe('Full Stack Engineer')
95
+ expect(role.level).toBe(4)
96
+ expect(role.skills).toContain('TypeScript')
97
+ })
98
+
99
+ it('should throw error for unknown template', () => {
100
+ expect(() =>
101
+ createBusinessRole('role_test', 'unknown_role' as keyof typeof StandardBusinessRoles)
102
+ ).toThrow('Unknown role template: unknown_role')
103
+ })
104
+ })
105
+
106
+ describe('hasPermission()', () => {
107
+ const role: BusinessRole = {
108
+ id: 'role_test',
109
+ name: 'Test Role',
110
+ type: 'engineer',
111
+ permissions: {
112
+ repository: ['read', 'edit', 'act:merge'],
113
+ project: ['read'],
114
+ document: ['manage'],
115
+ },
116
+ }
117
+
118
+ it('should return true for exact permission match', () => {
119
+ expect(hasPermission(role, 'repository', 'read')).toBe(true)
120
+ expect(hasPermission(role, 'repository', 'edit')).toBe(true)
121
+ })
122
+
123
+ it('should return true for action verb permissions', () => {
124
+ expect(hasPermission(role, 'repository', 'act:merge')).toBe(true)
125
+ })
126
+
127
+ it('should return true for manage permission (covers all)', () => {
128
+ expect(hasPermission(role, 'document', 'read')).toBe(true)
129
+ expect(hasPermission(role, 'document', 'edit')).toBe(true)
130
+ expect(hasPermission(role, 'document', 'delete')).toBe(true)
131
+ })
132
+
133
+ it('should return false for permission not granted', () => {
134
+ expect(hasPermission(role, 'repository', 'delete')).toBe(false)
135
+ expect(hasPermission(role, 'project', 'edit')).toBe(false)
136
+ })
137
+
138
+ it('should return false for non-existent resource type', () => {
139
+ expect(hasPermission(role, 'invoice', 'read')).toBe(false)
140
+ })
141
+
142
+ it('should return false for role without permissions', () => {
143
+ const emptyRole: BusinessRole = { id: 'empty', name: 'Empty', type: 'engineer' }
144
+ expect(hasPermission(emptyRole, 'repository', 'read')).toBe(false)
145
+ })
146
+
147
+ it('should handle wildcard permissions', () => {
148
+ const adminRole: BusinessRole = {
149
+ id: 'admin',
150
+ name: 'Admin',
151
+ type: 'ceo',
152
+ permissions: { '*': ['manage'] },
153
+ }
154
+
155
+ expect(hasPermission(adminRole, 'repository', 'read')).toBe(true)
156
+ expect(hasPermission(adminRole, 'anyResource', 'anyAction')).toBe(true)
157
+ })
158
+
159
+ it('should handle act:* wildcard', () => {
160
+ const roleWithActWildcard: BusinessRole = {
161
+ id: 'test',
162
+ name: 'Test',
163
+ type: 'lead',
164
+ permissions: {
165
+ repository: ['read', 'act:*'],
166
+ },
167
+ }
168
+
169
+ expect(hasPermission(roleWithActWildcard, 'repository', 'act:merge')).toBe(true)
170
+ expect(hasPermission(roleWithActWildcard, 'repository', 'act:deploy')).toBe(true)
171
+ })
172
+ })
173
+
174
+ describe('canHandleTask()', () => {
175
+ const role: BusinessRole = {
176
+ id: 'role_test',
177
+ name: 'Test Role',
178
+ type: 'engineer',
179
+ canHandle: ['coding', 'code-review', 'testing'],
180
+ }
181
+
182
+ it('should return true for task in canHandle list', () => {
183
+ expect(canHandleTask(role, 'coding')).toBe(true)
184
+ expect(canHandleTask(role, 'code-review')).toBe(true)
185
+ })
186
+
187
+ it('should return false for task not in canHandle list', () => {
188
+ expect(canHandleTask(role, 'architecture-review')).toBe(false)
189
+ })
190
+
191
+ it('should return false for role without canHandle', () => {
192
+ const emptyRole: BusinessRole = { id: 'empty', name: 'Empty', type: 'engineer' }
193
+ expect(canHandleTask(emptyRole, 'coding')).toBe(false)
194
+ })
195
+
196
+ it('should handle wildcard in canHandle', () => {
197
+ const wildcardRole: BusinessRole = {
198
+ id: 'admin',
199
+ name: 'Admin',
200
+ type: 'ceo',
201
+ canHandle: ['*'],
202
+ }
203
+
204
+ expect(canHandleTask(wildcardRole, 'anything')).toBe(true)
205
+ })
206
+ })
207
+
208
+ describe('canApproveRequest()', () => {
209
+ const role: BusinessRole = {
210
+ id: 'role_test',
211
+ name: 'Test Role',
212
+ type: 'manager',
213
+ canApprove: ['expense-under-5k', 'time-off', 'code-review'],
214
+ }
215
+
216
+ it('should return true for request in canApprove list', () => {
217
+ expect(canApproveRequest(role, 'expense-under-5k')).toBe(true)
218
+ expect(canApproveRequest(role, 'time-off')).toBe(true)
219
+ })
220
+
221
+ it('should return false for request not in canApprove list', () => {
222
+ expect(canApproveRequest(role, 'hiring')).toBe(false)
223
+ })
224
+
225
+ it('should return false for role without canApprove', () => {
226
+ const emptyRole: BusinessRole = { id: 'empty', name: 'Empty', type: 'engineer' }
227
+ expect(canApproveRequest(emptyRole, 'time-off')).toBe(false)
228
+ })
229
+
230
+ it('should handle wildcard in canApprove', () => {
231
+ const wildcardRole: BusinessRole = {
232
+ id: 'admin',
233
+ name: 'Admin',
234
+ type: 'ceo',
235
+ canApprove: ['*'],
236
+ }
237
+
238
+ expect(canApproveRequest(wildcardRole, 'anything')).toBe(true)
239
+ })
240
+ })
241
+
242
+ describe('canDelegateTask()', () => {
243
+ const role: BusinessRole = {
244
+ id: 'role_test',
245
+ name: 'Test Role',
246
+ type: 'lead',
247
+ canDelegate: ['code-review', 'testing'],
248
+ }
249
+
250
+ it('should return true for task in canDelegate list', () => {
251
+ expect(canDelegateTask(role, 'code-review')).toBe(true)
252
+ expect(canDelegateTask(role, 'testing')).toBe(true)
253
+ })
254
+
255
+ it('should return false for task not in canDelegate list', () => {
256
+ expect(canDelegateTask(role, 'architecture-review')).toBe(false)
257
+ })
258
+
259
+ it('should return false for role without canDelegate', () => {
260
+ const emptyRole: BusinessRole = { id: 'empty', name: 'Empty', type: 'engineer' }
261
+ expect(canDelegateTask(emptyRole, 'code-review')).toBe(false)
262
+ })
263
+
264
+ it('should handle wildcard in canDelegate', () => {
265
+ const wildcardRole: BusinessRole = {
266
+ id: 'admin',
267
+ name: 'Admin',
268
+ type: 'ceo',
269
+ canDelegate: ['*'],
270
+ }
271
+
272
+ expect(canDelegateTask(wildcardRole, 'anything')).toBe(true)
273
+ })
274
+ })
275
+
276
+ describe('findRoleForTask()', () => {
277
+ const rules: TaskRoutingRule[] = [
278
+ {
279
+ taskType: 'code-review',
280
+ requiredRole: 'engineer',
281
+ requiredLevel: 2,
282
+ preferWorkerType: 'human',
283
+ },
284
+ {
285
+ taskType: 'expense-approval',
286
+ requiredRole: 'manager',
287
+ amountThreshold: 1000,
288
+ },
289
+ {
290
+ taskType: 'expense-approval',
291
+ requiredRole: 'director',
292
+ escalateAbove: 5000,
293
+ escalateTo: 'director',
294
+ },
295
+ {
296
+ taskType: 'customer-inquiry',
297
+ requiredRole: 'agent',
298
+ preferWorkerType: 'ai',
299
+ fallbackTo: 'human',
300
+ },
301
+ ]
302
+
303
+ it('should find matching rule for task type', () => {
304
+ const rule = findRoleForTask('code-review', rules)
305
+
306
+ expect(rule).toBeDefined()
307
+ expect(rule?.requiredRole).toBe('engineer')
308
+ expect(rule?.requiredLevel).toBe(2)
309
+ })
310
+
311
+ it('should return undefined for non-existent task type', () => {
312
+ const rule = findRoleForTask('non-existent', rules)
313
+ expect(rule).toBeUndefined()
314
+ })
315
+
316
+ it('should return first matching rule when multiple exist', () => {
317
+ const rule = findRoleForTask('expense-approval', rules)
318
+
319
+ expect(rule).toBeDefined()
320
+ expect(rule?.requiredRole).toBe('manager')
321
+ })
322
+
323
+ it('should consider amount for escalation', () => {
324
+ const rule = findRoleForTask('expense-approval', rules, { amount: 10000 })
325
+
326
+ // Should escalate to director for amount > 5000
327
+ expect(rule).toBeDefined()
328
+ expect(rule?.requiredRole).toBe('director')
329
+ })
330
+ })
331
+
332
+ describe('createTaskAssignment()', () => {
333
+ const assignee: AssigneeRef = {
334
+ type: 'worker',
335
+ id: 'worker_alice',
336
+ name: 'Alice',
337
+ }
338
+
339
+ it('should create task assignment with defaults', () => {
340
+ const assignment = createTaskAssignment('task_123', 'code-review', assignee)
341
+
342
+ expect(assignment.id).toMatch(/^assign_/)
343
+ expect(assignment.taskId).toBe('task_123')
344
+ expect(assignment.taskType).toBe('code-review')
345
+ expect(assignment.assignee.id).toBe('worker_alice')
346
+ expect(assignment.status).toBe('assigned')
347
+ expect(assignment.priority).toBe('normal')
348
+ expect(assignment.assignedAt).toBeInstanceOf(Date)
349
+ })
350
+
351
+ it('should accept options', () => {
352
+ const assignment = createTaskAssignment('task_123', 'code-review', assignee, {
353
+ priority: 'high',
354
+ role: 'role_engineer',
355
+ description: 'Review PR #456',
356
+ context: { pullRequestId: 'pr_456' },
357
+ })
358
+
359
+ expect(assignment.priority).toBe('high')
360
+ expect(assignment.role).toBe('role_engineer')
361
+ expect(assignment.description).toBe('Review PR #456')
362
+ expect(assignment.context?.pullRequestId).toBe('pr_456')
363
+ })
364
+
365
+ it('should support team assignee', () => {
366
+ const teamAssignee: AssigneeRef = {
367
+ type: 'team',
368
+ id: 'team_platform',
369
+ name: 'Platform Team',
370
+ }
371
+
372
+ const assignment = createTaskAssignment('task_123', 'support', teamAssignee)
373
+
374
+ expect(assignment.assignee.type).toBe('team')
375
+ expect(assignment.assignee.id).toBe('team_platform')
376
+ })
377
+
378
+ it('should support role assignee', () => {
379
+ const roleAssignee: AssigneeRef = {
380
+ type: 'role',
381
+ id: 'role_engineer',
382
+ name: 'Engineer',
383
+ }
384
+
385
+ const assignment = createTaskAssignment('task_123', 'support', roleAssignee)
386
+
387
+ expect(assignment.assignee.type).toBe('role')
388
+ expect(assignment.assignee.id).toBe('role_engineer')
389
+ })
390
+ })
391
+
392
+ describe('transitionTaskStatus()', () => {
393
+ const baseAssignment: TaskAssignment = {
394
+ id: 'assign_123',
395
+ taskId: 'task_123',
396
+ taskType: 'code-review',
397
+ assignee: { type: 'worker', id: 'worker_alice' },
398
+ status: 'assigned',
399
+ assignedAt: new Date('2024-01-01'),
400
+ }
401
+
402
+ it('should transition to in_progress', () => {
403
+ const updated = transitionTaskStatus(baseAssignment, 'in_progress')
404
+
405
+ expect(updated.status).toBe('in_progress')
406
+ expect(updated.startedAt).toBeInstanceOf(Date)
407
+ })
408
+
409
+ it('should transition to completed', () => {
410
+ const inProgress: TaskAssignment = {
411
+ ...baseAssignment,
412
+ status: 'in_progress',
413
+ startedAt: new Date('2024-01-01'),
414
+ }
415
+
416
+ const updated = transitionTaskStatus(inProgress, 'completed', {
417
+ result: { approved: true },
418
+ notes: 'LGTM',
419
+ })
420
+
421
+ expect(updated.status).toBe('completed')
422
+ expect(updated.completedAt).toBeInstanceOf(Date)
423
+ expect(updated.result).toEqual({ approved: true })
424
+ expect(updated.notes).toBe('LGTM')
425
+ })
426
+
427
+ it('should transition to failed', () => {
428
+ const updated = transitionTaskStatus(baseAssignment, 'failed', {
429
+ notes: 'Task failed due to error',
430
+ })
431
+
432
+ expect(updated.status).toBe('failed')
433
+ expect(updated.completedAt).toBeInstanceOf(Date)
434
+ expect(updated.notes).toBe('Task failed due to error')
435
+ })
436
+
437
+ it('should preserve existing startedAt when completing', () => {
438
+ const inProgress: TaskAssignment = {
439
+ ...baseAssignment,
440
+ status: 'in_progress',
441
+ startedAt: new Date('2024-01-01T10:00:00'),
442
+ }
443
+
444
+ const updated = transitionTaskStatus(inProgress, 'completed')
445
+
446
+ expect(updated.startedAt?.getTime()).toBe(new Date('2024-01-01T10:00:00').getTime())
447
+ })
448
+
449
+ it('should handle blocked status', () => {
450
+ const updated = transitionTaskStatus(baseAssignment, 'blocked', {
451
+ notes: 'Waiting for dependencies',
452
+ })
453
+
454
+ expect(updated.status).toBe('blocked')
455
+ expect(updated.notes).toBe('Waiting for dependencies')
456
+ })
457
+
458
+ it('should handle cancelled status', () => {
459
+ const updated = transitionTaskStatus(baseAssignment, 'cancelled', {
460
+ notes: 'No longer needed',
461
+ })
462
+
463
+ expect(updated.status).toBe('cancelled')
464
+ expect(updated.notes).toBe('No longer needed')
465
+ })
466
+ })
467
+
468
+ describe('BusinessRole interface', () => {
469
+ it('should support full business role structure', () => {
470
+ const role: BusinessRole = {
471
+ id: 'role_senior_engineer',
472
+ name: 'Senior Software Engineer',
473
+ type: 'engineer',
474
+ department: 'Engineering',
475
+ description: 'Builds and maintains software systems',
476
+ responsibilities: [
477
+ 'Design and implement features',
478
+ 'Code review',
479
+ 'Mentor junior engineers',
480
+ ],
481
+ skills: ['TypeScript', 'React', 'Node.js', 'PostgreSQL'],
482
+ permissions: {
483
+ repository: ['read', 'edit', 'act:merge'],
484
+ project: ['read', 'edit'],
485
+ document: ['read'],
486
+ },
487
+ canHandle: ['coding', 'code-review', 'mentoring'],
488
+ canDelegate: ['testing'],
489
+ canApprove: ['code-review'],
490
+ escalateTo: 'role_lead',
491
+ reportsTo: 'role_lead',
492
+ workerType: 'hybrid',
493
+ level: 4,
494
+ compensationBand: 'L4',
495
+ metadata: { track: 'IC' },
496
+ }
497
+
498
+ expect(role.name).toBe('Senior Software Engineer')
499
+ expect(role.skills).toContain('TypeScript')
500
+ expect(role.canHandle).toContain('coding')
501
+ })
502
+ })
503
+
504
+ describe('TaskRoutingRule interface', () => {
505
+ it('should support full routing rule structure', () => {
506
+ const rule: TaskRoutingRule = {
507
+ taskType: 'complex-support-ticket',
508
+ requiredRole: 'agent',
509
+ requiredLevel: 2,
510
+ requiredSkills: ['technical-support', 'troubleshooting'],
511
+ requiredPermissions: ['ticket:read', 'ticket:respond'],
512
+ preferWorkerType: 'ai',
513
+ amountThreshold: 1000,
514
+ escalateAbove: 5000,
515
+ escalateTo: 'manager',
516
+ fallbackTo: 'human',
517
+ defaultPriority: 'high',
518
+ slaMinutes: 120,
519
+ conditions: { severity: 'critical' },
520
+ }
521
+
522
+ expect(rule.taskType).toBe('complex-support-ticket')
523
+ expect(rule.requiredSkills).toContain('technical-support')
524
+ expect(rule.slaMinutes).toBe(120)
525
+ })
526
+ })
527
+
528
+ describe('WorkflowRole interface', () => {
529
+ it('should support full workflow role structure', () => {
530
+ const workflowRole: WorkflowRole = {
531
+ name: 'Approver',
532
+ description: 'Reviews and approves requests',
533
+ canInitiate: false,
534
+ tasks: ['review', 'approve', 'reject'],
535
+ canView: ['details', 'history', 'comments'],
536
+ requiredBusinessRole: 'manager',
537
+ requiredPermissions: ['request:read', 'request:approve'],
538
+ minLevel: 3,
539
+ }
540
+
541
+ expect(workflowRole.name).toBe('Approver')
542
+ expect(workflowRole.tasks).toContain('approve')
543
+ expect(workflowRole.minLevel).toBe(3)
544
+ })
545
+ })
546
+ })