@things-factory/worklist 9.1.19 → 10.0.0-beta.2

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 (103) hide show
  1. package/dist-client/components/activity-thread-timeline.d.ts +1 -9
  2. package/dist-client/components/activity-thread-timeline.js +1 -3
  3. package/dist-client/components/activity-thread-timeline.js.map +1 -1
  4. package/dist-client/pages/activity/activity-list-page.d.ts +1 -7
  5. package/dist-client/pages/activity/activity-list-page.js +125 -234
  6. package/dist-client/pages/activity/activity-list-page.js.map +1 -1
  7. package/dist-client/pages/activity/activity-page.d.ts +1 -7
  8. package/dist-client/pages/activity/activity-page.js +51 -93
  9. package/dist-client/pages/activity/activity-page.js.map +1 -1
  10. package/dist-client/pages/activity/starter-list-page.d.ts +1 -7
  11. package/dist-client/pages/activity/starter-list-page.js +33 -62
  12. package/dist-client/pages/activity/starter-list-page.js.map +1 -1
  13. package/dist-client/pages/activity-approval/activity-approval-list-page.d.ts +1 -7
  14. package/dist-client/pages/activity-approval/activity-approval-list-page.js +50 -95
  15. package/dist-client/pages/activity-approval/activity-approval-list-page.js.map +1 -1
  16. package/dist-client/pages/activity-approval/activity-approval-page.d.ts +1 -7
  17. package/dist-client/pages/activity-approval/activity-approval-page.js +73 -119
  18. package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
  19. package/dist-client/pages/activity-instance/activity-instance-list-page.d.ts +0 -6
  20. package/dist-client/pages/activity-instance/activity-instance-list-page.js +63 -120
  21. package/dist-client/pages/activity-instance/activity-instance-list-page.js.map +1 -1
  22. package/dist-client/pages/activity-instance/activity-instance-search-page.d.ts +1 -7
  23. package/dist-client/pages/activity-instance/activity-instance-search-page.js +55 -101
  24. package/dist-client/pages/activity-instance/activity-instance-search-page.js.map +1 -1
  25. package/dist-client/pages/activity-instance/activity-instance-start-page.d.ts +1 -7
  26. package/dist-client/pages/activity-instance/activity-instance-start-page.js +65 -109
  27. package/dist-client/pages/activity-instance/activity-instance-start-page.js.map +1 -1
  28. package/dist-client/pages/activity-stats/activity-stats-list-page.d.ts +1 -7
  29. package/dist-client/pages/activity-stats/activity-stats-list-page.js +50 -95
  30. package/dist-client/pages/activity-stats/activity-stats-list-page.js.map +1 -1
  31. package/dist-client/pages/activity-store/activity-store-page.d.ts +1 -7
  32. package/dist-client/pages/activity-store/activity-store-page.js +2 -3
  33. package/dist-client/pages/activity-store/activity-store-page.js.map +1 -1
  34. package/dist-client/pages/activity-supervisor/reporter-list-page.d.ts +1 -7
  35. package/dist-client/pages/activity-supervisor/reporter-list-page.js +36 -66
  36. package/dist-client/pages/activity-supervisor/reporter-list-page.js.map +1 -1
  37. package/dist-client/pages/activity-template/activity-template-list-page.d.ts +1 -7
  38. package/dist-client/pages/activity-template/activity-template-list-page.js +70 -134
  39. package/dist-client/pages/activity-template/activity-template-list-page.js.map +1 -1
  40. package/dist-client/pages/activity-thread/activity-thread-list-page.d.ts +1 -7
  41. package/dist-client/pages/activity-thread/activity-thread-list-page.js +49 -93
  42. package/dist-client/pages/activity-thread/activity-thread-list-page.js.map +1 -1
  43. package/dist-client/pages/activity-thread/activity-thread-page.d.ts +1 -7
  44. package/dist-client/pages/activity-thread/activity-thread-page.js +80 -135
  45. package/dist-client/pages/activity-thread/activity-thread-page.js.map +1 -1
  46. package/dist-client/pages/activity-thread/activity-thread-view-page.d.ts +1 -7
  47. package/dist-client/pages/activity-thread/activity-thread-view-page.js +54 -80
  48. package/dist-client/pages/activity-thread/activity-thread-view-page.js.map +1 -1
  49. package/dist-client/pages/activity-thread/activity-thread-view.js +4 -0
  50. package/dist-client/pages/activity-thread/activity-thread-view.js.map +1 -1
  51. package/dist-client/pages/dashboard/dashboard-home.js +3 -5
  52. package/dist-client/pages/dashboard/dashboard-home.js.map +1 -1
  53. package/dist-client/pages/installable-activity/installable-activity-list-page.d.ts +0 -6
  54. package/dist-client/pages/installable-activity/installable-activity-list-page.js +68 -130
  55. package/dist-client/pages/installable-activity/installable-activity-list-page.js.map +1 -1
  56. package/dist-client/pages/todo/approval-done-list-page.d.ts +1 -7
  57. package/dist-client/pages/todo/approval-done-list-page.js +53 -100
  58. package/dist-client/pages/todo/approval-done-list-page.js.map +1 -1
  59. package/dist-client/pages/todo/approval-pending-list-page.d.ts +0 -6
  60. package/dist-client/pages/todo/approval-pending-list-page.js +63 -119
  61. package/dist-client/pages/todo/approval-pending-list-page.js.map +1 -1
  62. package/dist-client/pages/todo/done-list-calendar-page.d.ts +1 -7
  63. package/dist-client/pages/todo/done-list-calendar-page.js +2 -3
  64. package/dist-client/pages/todo/done-list-calendar-page.js.map +1 -1
  65. package/dist-client/pages/todo/done-list-page.d.ts +1 -7
  66. package/dist-client/pages/todo/done-list-page.js +56 -106
  67. package/dist-client/pages/todo/done-list-page.js.map +1 -1
  68. package/dist-client/pages/todo/draft-list-page.d.ts +1 -7
  69. package/dist-client/pages/todo/draft-list-page.js +49 -88
  70. package/dist-client/pages/todo/draft-list-page.js.map +1 -1
  71. package/dist-client/pages/todo/pickable-list-page.d.ts +1 -7
  72. package/dist-client/pages/todo/pickable-list-page.js +48 -91
  73. package/dist-client/pages/todo/pickable-list-page.js.map +1 -1
  74. package/dist-client/pages/todo/todo-list-page.d.ts +0 -6
  75. package/dist-client/pages/todo/todo-list-page.js +56 -106
  76. package/dist-client/pages/todo/todo-list-page.js.map +1 -1
  77. package/dist-client/pages/worklist-home.js +2 -3
  78. package/dist-client/pages/worklist-home.js.map +1 -1
  79. package/dist-client/route.d.ts +1 -1
  80. package/dist-client/templates/activity-approval-context-template.js +8 -12
  81. package/dist-client/templates/activity-approval-context-template.js.map +1 -1
  82. package/dist-client/templates/activity-instance-context-template.js +8 -12
  83. package/dist-client/templates/activity-instance-context-template.js.map +1 -1
  84. package/dist-client/templates/activity-thread-context-template.js +8 -12
  85. package/dist-client/templates/activity-thread-context-template.js.map +1 -1
  86. package/dist-client/tsconfig.tsbuildinfo +1 -1
  87. package/dist-server/controllers/activity-approval/approve.js +2 -2
  88. package/dist-server/controllers/activity-approval/approve.js.map +1 -1
  89. package/dist-server/controllers/activity-thread/submit.js +2 -2
  90. package/dist-server/controllers/activity-thread/submit.js.map +1 -1
  91. package/dist-server/service/index.d.ts +2 -2
  92. package/dist-server/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +16 -16
  94. package/spec/integration/approval-mixed-types.spec.ts +491 -0
  95. package/spec/integration/approval-role-based.spec.ts +389 -0
  96. package/spec/integration/instance-lifecycle.spec.ts +406 -0
  97. package/spec/integration/role-approval-edge-cases.spec.ts +581 -0
  98. package/spec/unit/controllers/activity-instance-issue.spec.ts +360 -0
  99. package/spec/unit/controllers/activity-thread-submit.spec.ts +384 -0
  100. package/spec/unit/role-approval-escalate-logic.spec.ts +499 -0
  101. package/spec/unit/role-approval-submit-logic.spec.ts +481 -0
  102. package/spec/unit/thread-state-helpers.spec.ts +253 -0
  103. package/translations/en.json +1 -1
@@ -0,0 +1,581 @@
1
+ /**
2
+ * ROLE Approval Edge Cases and Error Conditions Tests
3
+ * ROLE 승인 엣지 케이스 및 오류 조건 테스트
4
+ */
5
+
6
+ import { TestDatabase } from '../../../../test/test-database'
7
+ import { withTestTransaction } from '../../../../test/test-context'
8
+ import {
9
+ domainFactory,
10
+ userFactory,
11
+ roleFactory,
12
+ activityInstanceFactory,
13
+ activityThreadFactory,
14
+ activityApprovalFactory
15
+ } from '../../../../test/factories'
16
+
17
+ import {
18
+ ActivityInstanceStatus,
19
+ ActivityThreadStatus,
20
+ ActivityApprovalJudgment
21
+ } from '../../../../test/entities/schemas'
22
+
23
+ import {
24
+ createRoleApprovalLineItem,
25
+ createUserApprovalLineItem
26
+ } from '../../../../test/helpers/workflow-helpers'
27
+
28
+ describe('ROLE Approval Edge Cases and Error Conditions', () => {
29
+ let testDb: TestDatabase
30
+
31
+ beforeAll(async () => {
32
+ testDb = TestDatabase.getInstance()
33
+ })
34
+
35
+ describe('빈 승인라인', () => {
36
+ it('승인라인이 undefined이면 승인 프로세스를 건너뛰어야 한다', async () => {
37
+ await withTestTransaction(async (context) => {
38
+ const { tx } = context.state
39
+ const domain = await domainFactory.create({}, tx)
40
+ const user = await userFactory.create({}, tx)
41
+
42
+ // Given: 승인라인이 없는 Instance
43
+ const instance = await activityInstanceFactory.createWithActivity(
44
+ { state: ActivityInstanceStatus.Started },
45
+ undefined,
46
+ domain,
47
+ tx
48
+ )
49
+
50
+ // approvalLine 체크
51
+ const approvalLine = instance.approvalLine
52
+ const hasApprovalLine = approvalLine && approvalLine.length > 0
53
+ expect(hasApprovalLine).toBeFalsy()
54
+
55
+ // Then: Submit 시 즉시 Ended
56
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
57
+ { state: ActivityThreadStatus.Started },
58
+ instance,
59
+ user,
60
+ tx
61
+ )
62
+
63
+ thread.state = ActivityThreadStatus.Ended
64
+ thread.terminatedAt = new Date()
65
+ const ended = await tx.save('ActivityThread', thread) as any
66
+
67
+ expect(ended.state).toBe(ActivityThreadStatus.Ended)
68
+ })
69
+ })
70
+
71
+ it('승인라인이 빈 배열이면 승인 프로세스를 건너뛰어야 한다', async () => {
72
+ await withTestTransaction(async (context) => {
73
+ const { tx } = context.state
74
+ const domain = await domainFactory.create({}, tx)
75
+ const user = await userFactory.create({}, tx)
76
+
77
+ // Given: 빈 배열 승인라인
78
+ const instance = await activityInstanceFactory.createWithApprovalLine(
79
+ [],
80
+ { state: ActivityInstanceStatus.Started },
81
+ undefined,
82
+ domain,
83
+ tx
84
+ )
85
+
86
+ const approvalLine = instance.approvalLine
87
+ const hasApprovalLine = approvalLine && approvalLine.length > 0
88
+ expect(hasApprovalLine).toBe(false)
89
+
90
+ // Then: Submit 시 즉시 Ended
91
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
92
+ { state: ActivityThreadStatus.Started },
93
+ instance,
94
+ user,
95
+ tx
96
+ )
97
+
98
+ thread.state = ActivityThreadStatus.Ended
99
+ thread.terminatedAt = new Date()
100
+ const ended = await tx.save('ActivityThread', thread) as any
101
+
102
+ expect(ended.state).toBe(ActivityThreadStatus.Ended)
103
+ })
104
+ })
105
+ })
106
+
107
+ describe('존재하지 않는 Role', () => {
108
+ it('삭제된 roleId를 참조하면 Role을 찾을 수 없어야 한다', async () => {
109
+ await withTestTransaction(async (context) => {
110
+ const { tx } = context.state
111
+ const domain = await domainFactory.create({}, tx)
112
+
113
+ // Given: 존재하지 않는 roleId
114
+ const nonExistentRoleId = 'non-existent-role-id'
115
+ const invalidApprovalLine = [
116
+ { type: 'Role', value: nonExistentRoleId }
117
+ ]
118
+
119
+ // When: Role 조회
120
+ const role = await tx.findOne('Role', { where: { id: nonExistentRoleId } })
121
+
122
+ // Then: Role이 없음
123
+ expect(role).toBeNull()
124
+ })
125
+ })
126
+ })
127
+
128
+ describe('역할에 사용자가 없는 경우', () => {
129
+ it('사용자가 없는 Role로도 approval이 생성될 수 있어야 한다', async () => {
130
+ await withTestTransaction(async (context) => {
131
+ const { tx } = context.state
132
+ const domain = await domainFactory.create({}, tx)
133
+
134
+ // Given: 사용자가 할당되지 않은 빈 역할
135
+ const emptyRole = await roleFactory.create({ name: 'Empty Role', domain }, tx)
136
+
137
+ // When: ROLE 기반 approval 생성
138
+ const approval = await activityApprovalFactory.createWithRole(
139
+ { judgment: ActivityApprovalJudgment.Pending },
140
+ undefined,
141
+ emptyRole,
142
+ tx
143
+ )
144
+
145
+ // Then: approval은 생성됨 (나중에 역할에 사용자가 추가될 수 있음)
146
+ expect(approval).toBeDefined()
147
+ expect(approval.approverRole?.id).toBe(emptyRole.id)
148
+ expect(approval.approver).toBeUndefined()
149
+ })
150
+ })
151
+ })
152
+
153
+ describe('동일 approval 중복 처리 방지', () => {
154
+ it('이미 Approved된 approval을 다시 승인하면 거부되어야 한다', async () => {
155
+ await withTestTransaction(async (context) => {
156
+ const { tx } = context.state
157
+ const domain = await domainFactory.create({}, tx)
158
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
159
+
160
+ // Given: 이미 Approved 상태인 approval
161
+ const approval = await activityApprovalFactory.createWithRole(
162
+ {
163
+ judgment: ActivityApprovalJudgment.Approved,
164
+ terminatedAt: new Date()
165
+ },
166
+ undefined,
167
+ approverRole,
168
+ tx
169
+ )
170
+
171
+ // Then: Pending이 아니므로 재승인 불가
172
+ const canApprove = approval.judgment === ActivityApprovalJudgment.Pending
173
+ expect(canApprove).toBe(false)
174
+ })
175
+ })
176
+
177
+ it('이미 Rejected된 approval을 다시 승인하면 거부되어야 한다', async () => {
178
+ await withTestTransaction(async (context) => {
179
+ const { tx } = context.state
180
+ const domain = await domainFactory.create({}, tx)
181
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
182
+
183
+ // Given: 이미 Rejected 상태인 approval
184
+ const approval = await activityApprovalFactory.createWithRole(
185
+ {
186
+ judgment: ActivityApprovalJudgment.Rejected,
187
+ terminatedAt: new Date()
188
+ },
189
+ undefined,
190
+ approverRole,
191
+ tx
192
+ )
193
+
194
+ // Then: Pending이 아니므로 재승인 불가
195
+ const canApprove = approval.judgment === ActivityApprovalJudgment.Pending
196
+ expect(canApprove).toBe(false)
197
+ })
198
+ })
199
+
200
+ it('이미 Escalated된 approval을 다시 처리하면 거부되어야 한다', async () => {
201
+ await withTestTransaction(async (context) => {
202
+ const { tx } = context.state
203
+ const domain = await domainFactory.create({}, tx)
204
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
205
+
206
+ // Given: 이미 Escalated 상태인 approval
207
+ const approval = await activityApprovalFactory.createWithRole(
208
+ {
209
+ judgment: ActivityApprovalJudgment.Escalated,
210
+ terminatedAt: new Date()
211
+ },
212
+ undefined,
213
+ approverRole,
214
+ tx
215
+ )
216
+
217
+ // Then: Pending이 아니므로 재처리 불가
218
+ const canApprove = approval.judgment === ActivityApprovalJudgment.Pending
219
+ expect(canApprove).toBe(false)
220
+ })
221
+ })
222
+ })
223
+
224
+ describe('스레드 상태 검증', () => {
225
+ it('Unassigned 상태 스레드는 submit할 수 없어야 한다', async () => {
226
+ await withTestTransaction(async (context) => {
227
+ const { tx } = context.state
228
+ const domain = await domainFactory.create({}, tx)
229
+
230
+ // Given: Unassigned 상태 스레드 (assignee 없이 생성)
231
+ const instance = await activityInstanceFactory.createWithActivity(
232
+ { state: ActivityInstanceStatus.Issued },
233
+ undefined,
234
+ domain,
235
+ tx
236
+ )
237
+
238
+ const thread = await tx.save('ActivityThread', {
239
+ state: ActivityThreadStatus.Unassigned,
240
+ round: 1,
241
+ activityInstance: instance,
242
+ domain
243
+ }) as any
244
+
245
+ // Then: Submit 불가능한 상태
246
+ const canSubmit =
247
+ thread.state === ActivityThreadStatus.Assigned ||
248
+ thread.state === ActivityThreadStatus.Started
249
+ expect(canSubmit).toBe(false)
250
+ })
251
+ })
252
+
253
+ it('Submitted 상태 스레드는 다시 submit할 수 없어야 한다', async () => {
254
+ await withTestTransaction(async (context) => {
255
+ const { tx } = context.state
256
+ const domain = await domainFactory.create({}, tx)
257
+ const user = await userFactory.create({}, tx)
258
+
259
+ // Given: Submitted 상태 스레드
260
+ const instance = await activityInstanceFactory.createWithActivity(
261
+ { state: ActivityInstanceStatus.Started },
262
+ undefined,
263
+ domain,
264
+ tx
265
+ )
266
+
267
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
268
+ { state: ActivityThreadStatus.Submitted },
269
+ instance,
270
+ user,
271
+ tx
272
+ )
273
+
274
+ // Then: Submit 불가능한 상태
275
+ const canSubmit =
276
+ thread.state === ActivityThreadStatus.Assigned ||
277
+ thread.state === ActivityThreadStatus.Started
278
+ expect(canSubmit).toBe(false)
279
+ })
280
+ })
281
+ })
282
+
283
+ describe('order 경계 조건', () => {
284
+ it('order가 0이면 잘못된 상태여야 한다', async () => {
285
+ await withTestTransaction(async (context) => {
286
+ const { tx } = context.state
287
+ const domain = await domainFactory.create({}, tx)
288
+ const role = await roleFactory.create({ name: 'Approver', domain }, tx)
289
+
290
+ // order는 1부터 시작해야 함
291
+ const invalidOrder = 0
292
+ const validOrders = [1, 2, 3]
293
+
294
+ expect(validOrders.includes(invalidOrder)).toBe(false)
295
+ expect(invalidOrder).toBeLessThan(1)
296
+ })
297
+ })
298
+
299
+ it('order가 음수이면 잘못된 상태여야 한다', async () => {
300
+ await withTestTransaction(async (context) => {
301
+ const { tx } = context.state
302
+
303
+ const invalidOrder = -1
304
+ expect(invalidOrder).toBeLessThan(1)
305
+ })
306
+ })
307
+
308
+ it('order가 approvalLine 길이보다 크면 더 이상 에스컬레이션이 없어야 한다', async () => {
309
+ await withTestTransaction(async (context) => {
310
+ const { tx } = context.state
311
+ const domain = await domainFactory.create({}, tx)
312
+ const role = await roleFactory.create({ name: 'Approver', domain }, tx)
313
+
314
+ // Given: 단일 승인라인
315
+ const approvalLine = [createRoleApprovalLineItem(role)]
316
+
317
+ // order=1일 때: approvalLine.length(1) > order(1) = false
318
+ const order = 1
319
+ const hasNextLevel = approvalLine.length > order
320
+ expect(hasNextLevel).toBe(false)
321
+ })
322
+ })
323
+ })
324
+
325
+ describe('타입스탬프 설정', () => {
326
+ it('승인 완료 시 terminatedAt이 반드시 설정되어야 한다', async () => {
327
+ await withTestTransaction(async (context) => {
328
+ const { tx } = context.state
329
+ const domain = await domainFactory.create({}, tx)
330
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
331
+ const approver = await userFactory.createWithRole({}, approverRole, tx)
332
+
333
+ // Given: Pending approval
334
+ const approval = await activityApprovalFactory.createWithRole(
335
+ { judgment: ActivityApprovalJudgment.Pending },
336
+ undefined,
337
+ approverRole,
338
+ tx
339
+ )
340
+ expect(approval.terminatedAt).toBeFalsy() // null or undefined
341
+
342
+ // When: 승인
343
+ approval.judgment = ActivityApprovalJudgment.Approved
344
+ approval.approver = approver
345
+ approval.terminatedAt = new Date()
346
+ const approved = await tx.save('ActivityApproval', approval) as any
347
+
348
+ // Then
349
+ expect(approved.terminatedAt).toBeDefined()
350
+ expect(approved.terminatedAt).toBeInstanceOf(Date)
351
+ })
352
+ })
353
+
354
+ it('반려 시 terminatedAt이 반드시 설정되어야 한다', async () => {
355
+ await withTestTransaction(async (context) => {
356
+ const { tx } = context.state
357
+ const domain = await domainFactory.create({}, tx)
358
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
359
+ const approver = await userFactory.createWithRole({}, approverRole, tx)
360
+
361
+ // Given: Pending approval
362
+ const approval = await activityApprovalFactory.createWithRole(
363
+ { judgment: ActivityApprovalJudgment.Pending },
364
+ undefined,
365
+ approverRole,
366
+ tx
367
+ )
368
+
369
+ // When: 반려
370
+ approval.judgment = ActivityApprovalJudgment.Rejected
371
+ approval.approver = approver
372
+ approval.comment = 'Rejected reason'
373
+ approval.terminatedAt = new Date()
374
+ const rejected = await tx.save('ActivityApproval', approval) as any
375
+
376
+ // Then
377
+ expect(rejected.terminatedAt).toBeDefined()
378
+ expect(rejected.terminatedAt).toBeInstanceOf(Date)
379
+ })
380
+ })
381
+ })
382
+
383
+ describe('Domain 격리', () => {
384
+ it('다른 Domain의 Role로 approval을 생성할 수 없어야 한다 (개념적)', async () => {
385
+ await withTestTransaction(async (context) => {
386
+ const { tx } = context.state
387
+
388
+ // Given: 두 개의 다른 Domain
389
+ const domain1 = await domainFactory.create({ name: 'Domain 1' }, tx)
390
+ const domain2 = await domainFactory.create({ name: 'Domain 2' }, tx)
391
+
392
+ // Given: 각 도메인의 역할
393
+ const role1 = await roleFactory.create({ name: 'Role in Domain 1', domain: domain1 }, tx)
394
+ const role2 = await roleFactory.create({ name: 'Role in Domain 2', domain: domain2 }, tx)
395
+
396
+ // Then: 역할은 각 도메인에 속함
397
+ expect(role1.domain?.id).toBe(domain1.id)
398
+ expect(role2.domain?.id).toBe(domain2.id)
399
+
400
+ // 실제 구현에서는 Domain 격리 검증이 필요
401
+ expect(role1.domain?.id).not.toBe(domain2.id)
402
+ expect(role2.domain?.id).not.toBe(domain1.id)
403
+ })
404
+ })
405
+ })
406
+
407
+ describe('Round 증가 규칙', () => {
408
+ it('Rejected 후 재시작 시 round가 증가해야 한다', async () => {
409
+ await withTestTransaction(async (context) => {
410
+ const { tx } = context.state
411
+ const domain = await domainFactory.create({}, tx)
412
+ const user = await userFactory.create({}, tx)
413
+
414
+ // Given: Round 1 스레드
415
+ const instance = await activityInstanceFactory.createWithActivity(
416
+ { state: ActivityInstanceStatus.Started },
417
+ undefined,
418
+ domain,
419
+ tx
420
+ )
421
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
422
+ { state: ActivityThreadStatus.Started, round: 1 },
423
+ instance,
424
+ user,
425
+ tx
426
+ )
427
+
428
+ // When: 반려됨
429
+ thread.state = ActivityThreadStatus.Rejected
430
+ const rejected = await tx.save('ActivityThread', thread) as any
431
+
432
+ // When: 재시작
433
+ rejected.state = ActivityThreadStatus.Started
434
+ rejected.round = rejected.round + 1
435
+ const restarted = await tx.save('ActivityThread', rejected) as any
436
+
437
+ // Then: Round 증가
438
+ expect(restarted.round).toBe(2)
439
+ })
440
+ })
441
+
442
+ it('동일 round에서 approval의 round도 일치해야 한다', async () => {
443
+ await withTestTransaction(async (context) => {
444
+ const { tx } = context.state
445
+ const domain = await domainFactory.create({}, tx)
446
+ const user = await userFactory.create({}, tx)
447
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
448
+
449
+ // Given: Round 2 스레드
450
+ const instance = await activityInstanceFactory.createWithActivity(
451
+ { state: ActivityInstanceStatus.Started },
452
+ undefined,
453
+ domain,
454
+ tx
455
+ )
456
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
457
+ { state: ActivityThreadStatus.Submitted, round: 2 },
458
+ instance,
459
+ user,
460
+ tx
461
+ )
462
+
463
+ // When: Approval 생성
464
+ const approval = await tx.save('ActivityApproval', {
465
+ domain,
466
+ activityThread: thread,
467
+ round: thread.round, // 스레드 round와 일치
468
+ order: 1,
469
+ judgment: ActivityApprovalJudgment.Pending,
470
+ approverRole: approverRole,
471
+ creator: user,
472
+ updater: user
473
+ }) as any
474
+
475
+ // Then: Round 일치
476
+ expect(approval.round).toBe(thread.round)
477
+ expect(approval.round).toBe(2)
478
+ })
479
+ })
480
+ })
481
+
482
+ describe('ActivityApprovalJudgment 값 검증', () => {
483
+ it('Pending은 빈 문자열이어야 한다', () => {
484
+ expect(ActivityApprovalJudgment.Pending).toBe('')
485
+ })
486
+
487
+ it('유효한 judgment 값만 허용되어야 한다', () => {
488
+ const validJudgments = [
489
+ ActivityApprovalJudgment.Pending,
490
+ ActivityApprovalJudgment.Rejected,
491
+ ActivityApprovalJudgment.Escalated,
492
+ ActivityApprovalJudgment.Delegated,
493
+ ActivityApprovalJudgment.Approved,
494
+ ActivityApprovalJudgment.Aborted
495
+ ]
496
+
497
+ expect(validJudgments).toContain('')
498
+ expect(validJudgments).toContain('rejected')
499
+ expect(validJudgments).toContain('escalated')
500
+ expect(validJudgments).toContain('delegated')
501
+ expect(validJudgments).toContain('approved')
502
+ expect(validJudgments).toContain('aborted')
503
+
504
+ expect(validJudgments).not.toContain('invalid')
505
+ expect(validJudgments).not.toContain('Approved') // 대소문자 구분
506
+ })
507
+ })
508
+
509
+ describe('OOC Resolve 시나리오 (버그 수정 주요 케이스)', () => {
510
+ it('OOC Resolve에서 ROLE 타입 outlierApprovalLine이 정상 작동해야 한다', async () => {
511
+ await withTestTransaction(async (context) => {
512
+ const { tx } = context.state
513
+ const domain = await domainFactory.create({}, tx)
514
+ const oocApproverRole = await roleFactory.create({ name: 'OOC Approver', domain }, tx)
515
+ const resolver = await userFactory.create({ name: 'Resolver' }, tx)
516
+ const approver = await userFactory.createWithRole({ name: 'Approver' }, oocApproverRole, tx)
517
+
518
+ // Given: OOC Resolve Activity with ROLE-based outlierApprovalLine
519
+ const outlierApprovalLine = [createRoleApprovalLineItem(oocApproverRole)]
520
+
521
+ const instance = await activityInstanceFactory.createWithApprovalLine(
522
+ outlierApprovalLine,
523
+ {
524
+ name: '[OOC 조치]',
525
+ state: ActivityInstanceStatus.Started
526
+ },
527
+ undefined,
528
+ domain,
529
+ tx
530
+ )
531
+
532
+ expect(instance.approvalLine?.[0].type).toBe('Role')
533
+ expect(instance.approvalLine?.[0].value).toBe(oocApproverRole.id)
534
+
535
+ // Given: Resolver가 작업 완료 후 Submit
536
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
537
+ { state: ActivityThreadStatus.Submitted },
538
+ instance,
539
+ resolver,
540
+ tx
541
+ )
542
+
543
+ // When: ROLE 기반 approval 생성 (버그 수정 후 정상 작동)
544
+ // 수정 전: if (approverRoleId) throw - roleId가 있으면 항상 에러
545
+ // 수정 후: if (!approverRoleId) throw - roleId가 없을 때만 에러
546
+ const approvalLineItem = outlierApprovalLine[0]
547
+ const approverRoleId = approvalLineItem.value
548
+
549
+ // 버그 수정 확인: roleId가 있으므로 정상 처리되어야 함
550
+ expect(approverRoleId).toBeDefined()
551
+ expect(approverRoleId).toBe(oocApproverRole.id)
552
+
553
+ const approval = await tx.save('ActivityApproval', {
554
+ domain,
555
+ activityThread: thread,
556
+ round: 1,
557
+ order: 1,
558
+ judgment: ActivityApprovalJudgment.Pending,
559
+ approverRole: oocApproverRole,
560
+ transaction: 'submit',
561
+ creator: resolver,
562
+ updater: resolver
563
+ }) as any
564
+
565
+ expect(approval).toBeDefined()
566
+ expect(approval.approverRole?.id).toBe(oocApproverRole.id)
567
+
568
+ // When: 해당 역할 소유자가 승인
569
+ approval.judgment = ActivityApprovalJudgment.Approved
570
+ approval.approver = approver
571
+ approval.terminatedAt = new Date()
572
+ const approved = await tx.save('ActivityApproval', approval) as any
573
+
574
+ // Then: OOC Resolve 승인 완료
575
+ expect(approved.judgment).toBe(ActivityApprovalJudgment.Approved)
576
+ expect(approved.approver?.id).toBe(approver.id)
577
+ expect(approved.approverRole?.id).toBe(oocApproverRole.id)
578
+ })
579
+ })
580
+ })
581
+ })