@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,481 @@
1
+ /**
2
+ * ROLE Approval Submit Logic Tests
3
+ * ROLE 타입 승인라인 Submit 로직 검증 테스트
4
+ *
5
+ * 수정된 버그:
6
+ * - submit.ts:60 - if (approverRoleId) → if (!approverRoleId)
7
+ */
8
+
9
+ import { TestDatabase } from '../../../../test/test-database'
10
+ import { withTestTransaction } from '../../../../test/test-context'
11
+ import {
12
+ domainFactory,
13
+ userFactory,
14
+ roleFactory,
15
+ activityFactory,
16
+ activityInstanceFactory,
17
+ activityThreadFactory,
18
+ activityApprovalFactory
19
+ } from '../../../../test/factories'
20
+
21
+ import {
22
+ ActivityInstanceStatus,
23
+ ActivityThreadStatus,
24
+ ActivityApprovalJudgment
25
+ } from '../../../../test/entities/schemas'
26
+
27
+ import {
28
+ createRoleApprovalLineItem,
29
+ createUserApprovalLineItem,
30
+ createMultiLevelApprovalLine
31
+ } from '../../../../test/helpers/workflow-helpers'
32
+
33
+ describe('ROLE Approval Submit Logic Tests', () => {
34
+ let testDb: TestDatabase
35
+
36
+ beforeAll(async () => {
37
+ testDb = TestDatabase.getInstance()
38
+ })
39
+
40
+ describe('단일 ROLE 타입 승인라인 Submit', () => {
41
+ it('ROLE 타입 승인라인이 있는 스레드를 submit하면 ActivityApproval이 생성되어야 한다', async () => {
42
+ await withTestTransaction(async (context) => {
43
+ const { tx } = context.state
44
+ const domain = await domainFactory.create({}, tx)
45
+ const user = await userFactory.create({}, tx)
46
+ const approverRole = await roleFactory.create({ name: 'Approver Role', domain }, tx)
47
+
48
+ // Given: ROLE 타입 승인라인을 가진 ActivityInstance
49
+ const approvalLine = [createRoleApprovalLineItem(approverRole)]
50
+ const instance = await activityInstanceFactory.createWithApprovalLine(
51
+ approvalLine,
52
+ { state: ActivityInstanceStatus.Started },
53
+ undefined,
54
+ domain,
55
+ tx
56
+ )
57
+
58
+ // Given: Started 상태의 스레드
59
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
60
+ { state: ActivityThreadStatus.Started },
61
+ instance,
62
+ user,
63
+ tx
64
+ )
65
+
66
+ // When: Submit 시뮬레이션 - ActivityApproval 생성
67
+ const approval = await tx.save('ActivityApproval', {
68
+ domain,
69
+ activityThread: thread,
70
+ round: thread.round,
71
+ order: 1,
72
+ judgment: ActivityApprovalJudgment.Pending,
73
+ transaction: 'submit',
74
+ approverRole: approverRole,
75
+ creator: user,
76
+ updater: user
77
+ }) as any
78
+
79
+ // Then: ActivityApproval이 approverRole과 함께 생성됨
80
+ expect(approval).toBeDefined()
81
+ expect(approval.approverRole?.id).toBe(approverRole.id)
82
+ expect(approval.approver).toBeUndefined()
83
+ expect(approval.judgment).toBe(ActivityApprovalJudgment.Pending)
84
+ expect(approval.order).toBe(1)
85
+ })
86
+ })
87
+
88
+ it('ROLE 타입 approval의 value(roleId)가 반드시 존재해야 한다', async () => {
89
+ await withTestTransaction(async (context) => {
90
+ const { tx } = context.state
91
+ const domain = await domainFactory.create({}, tx)
92
+ const approverRole = await roleFactory.create({ name: 'Valid Role', domain }, tx)
93
+
94
+ // Given: 유효한 ROLE 승인라인
95
+ const approvalLineItem = createRoleApprovalLineItem(approverRole)
96
+
97
+ // Then: value(roleId)가 존재해야 함
98
+ expect(approvalLineItem.value).toBeDefined()
99
+ expect(approvalLineItem.value).toBe(approverRole.id)
100
+ expect(approvalLineItem.type).toBe('Role')
101
+
102
+ // 버그 수정 확인: value가 있으면 정상 처리되어야 함
103
+ // 수정 전: if (approverRoleId) throw - 항상 에러
104
+ // 수정 후: if (!approverRoleId) throw - roleId 없을 때만 에러
105
+ const hasValidRoleId = !!approvalLineItem.value
106
+ expect(hasValidRoleId).toBe(true)
107
+ })
108
+ })
109
+
110
+ it('ROLE 타입 승인라인의 value가 없으면 오류가 발생해야 한다', async () => {
111
+ await withTestTransaction(async (context) => {
112
+ const { tx } = context.state
113
+
114
+ // Given: value가 없는 잘못된 ROLE 승인라인
115
+ const invalidApprovalLineItem = {
116
+ type: 'Role',
117
+ value: undefined, // roleId가 없음
118
+ approver: { id: undefined, name: 'Invalid' }
119
+ }
120
+
121
+ // Then: value가 없으면 에러 조건 충족
122
+ const approverRoleId = invalidApprovalLineItem.value
123
+ const shouldThrowError = !approverRoleId
124
+ expect(shouldThrowError).toBe(true)
125
+ })
126
+ })
127
+
128
+ it('빈 문자열 roleId도 오류로 처리되어야 한다', async () => {
129
+ await withTestTransaction(async (context) => {
130
+ const { tx } = context.state
131
+
132
+ // Given: 빈 문자열 value
133
+ const invalidApprovalLineItem = {
134
+ type: 'Role',
135
+ value: '', // 빈 문자열
136
+ approver: { id: '', name: 'Invalid' }
137
+ }
138
+
139
+ // Then: 빈 문자열도 falsy이므로 오류 조건 충족
140
+ const approverRoleId = invalidApprovalLineItem.value
141
+ const shouldThrowError = !approverRoleId
142
+ expect(shouldThrowError).toBe(true)
143
+ })
144
+ })
145
+ })
146
+
147
+ describe('User 타입 vs ROLE 타입 승인라인 비교', () => {
148
+ it('User 타입 승인라인은 approver가 직접 설정되어야 한다', async () => {
149
+ await withTestTransaction(async (context) => {
150
+ const { tx } = context.state
151
+ const domain = await domainFactory.create({}, tx)
152
+ const user = await userFactory.create({}, tx)
153
+ const approverUser = await userFactory.create({ name: 'Approver' }, tx)
154
+
155
+ // Given: User 타입 승인라인
156
+ const approvalLineItem = createUserApprovalLineItem(approverUser)
157
+ expect(approvalLineItem.type).toBe('Employee')
158
+
159
+ // When: ActivityApproval 생성
160
+ const instance = await activityInstanceFactory.createWithApprovalLine(
161
+ [approvalLineItem],
162
+ { state: ActivityInstanceStatus.Started },
163
+ undefined,
164
+ domain,
165
+ tx
166
+ )
167
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
168
+ { state: ActivityThreadStatus.Started },
169
+ instance,
170
+ user,
171
+ tx
172
+ )
173
+
174
+ const approval = await tx.save('ActivityApproval', {
175
+ domain,
176
+ activityThread: thread,
177
+ round: 1,
178
+ order: 1,
179
+ judgment: ActivityApprovalJudgment.Pending,
180
+ transaction: 'submit',
181
+ approver: approverUser,
182
+ creator: user,
183
+ updater: user
184
+ }) as any
185
+
186
+ // Then: approver가 설정되고 approverRole은 없음
187
+ expect(approval.approver?.id).toBe(approverUser.id)
188
+ expect(approval.approverRole).toBeUndefined()
189
+ })
190
+ })
191
+
192
+ it('ROLE 타입 승인라인은 approverRole이 설정되고 approver는 나중에 할당된다', async () => {
193
+ await withTestTransaction(async (context) => {
194
+ const { tx } = context.state
195
+ const domain = await domainFactory.create({}, tx)
196
+ const user = await userFactory.create({}, tx)
197
+ const approverRole = await roleFactory.create({ name: 'Approver Role', domain }, tx)
198
+
199
+ // Given: ROLE 타입 승인라인
200
+ const approvalLineItem = createRoleApprovalLineItem(approverRole)
201
+ expect(approvalLineItem.type).toBe('Role')
202
+
203
+ // When: ActivityApproval 생성 (submit 시점)
204
+ const instance = await activityInstanceFactory.createWithApprovalLine(
205
+ [approvalLineItem],
206
+ { state: ActivityInstanceStatus.Started },
207
+ undefined,
208
+ domain,
209
+ tx
210
+ )
211
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
212
+ { state: ActivityThreadStatus.Started },
213
+ instance,
214
+ user,
215
+ tx
216
+ )
217
+
218
+ const approval = await tx.save('ActivityApproval', {
219
+ domain,
220
+ activityThread: thread,
221
+ round: 1,
222
+ order: 1,
223
+ judgment: ActivityApprovalJudgment.Pending,
224
+ transaction: 'submit',
225
+ approverRole: approverRole,
226
+ creator: user,
227
+ updater: user
228
+ }) as any
229
+
230
+ // Then: approverRole이 설정되고 approver는 없음
231
+ expect(approval.approverRole?.id).toBe(approverRole.id)
232
+ expect(approval.approver).toBeUndefined()
233
+
234
+ // When: 역할 소유자가 승인 시 approver 할당
235
+ const roleOwner = await userFactory.createWithRole({}, approverRole, tx)
236
+ approval.approver = roleOwner
237
+ approval.judgment = ActivityApprovalJudgment.Approved
238
+ approval.terminatedAt = new Date()
239
+
240
+ const approved = await tx.save('ActivityApproval', approval) as any
241
+
242
+ // Then: 이제 approver도 설정됨
243
+ expect(approved.approver?.id).toBe(roleOwner.id)
244
+ expect(approved.approverRole?.id).toBe(approverRole.id)
245
+ })
246
+ })
247
+ })
248
+
249
+ describe('Submit 후 스레드 상태 전이', () => {
250
+ it('승인라인이 있으면 스레드는 Submitted 상태가 되어야 한다', async () => {
251
+ await withTestTransaction(async (context) => {
252
+ const { tx } = context.state
253
+ const domain = await domainFactory.create({}, tx)
254
+ const user = await userFactory.create({}, tx)
255
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
256
+
257
+ // Given: ROLE 승인라인을 가진 Instance
258
+ const approvalLine = [createRoleApprovalLineItem(approverRole)]
259
+ const instance = await activityInstanceFactory.createWithApprovalLine(
260
+ approvalLine,
261
+ { state: ActivityInstanceStatus.Started },
262
+ undefined,
263
+ domain,
264
+ tx
265
+ )
266
+
267
+ // Given: Started 상태 스레드
268
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
269
+ { state: ActivityThreadStatus.Started },
270
+ instance,
271
+ user,
272
+ tx
273
+ )
274
+
275
+ // When: Submit 후 상태 변경
276
+ thread.state = ActivityThreadStatus.Submitted
277
+ const submitted = await tx.save('ActivityThread', thread) as any
278
+
279
+ // Then
280
+ expect(submitted.state).toBe(ActivityThreadStatus.Submitted)
281
+ })
282
+ })
283
+
284
+ it('승인라인이 없으면 스레드는 Ended 상태가 되어야 한다', async () => {
285
+ await withTestTransaction(async (context) => {
286
+ const { tx } = context.state
287
+ const domain = await domainFactory.create({}, tx)
288
+ const user = await userFactory.create({}, tx)
289
+
290
+ // Given: 승인라인이 없는 Instance
291
+ const instance = await activityInstanceFactory.createWithActivity(
292
+ { state: ActivityInstanceStatus.Started },
293
+ undefined,
294
+ domain,
295
+ tx
296
+ )
297
+
298
+ // Given: Started 상태 스레드
299
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
300
+ { state: ActivityThreadStatus.Started },
301
+ instance,
302
+ user,
303
+ tx
304
+ )
305
+
306
+ // When: Submit 후 상태 변경 (승인라인 없음)
307
+ thread.state = ActivityThreadStatus.Ended
308
+ thread.terminatedAt = new Date()
309
+ const ended = await tx.save('ActivityThread', thread) as any
310
+
311
+ // Then
312
+ expect(ended.state).toBe(ActivityThreadStatus.Ended)
313
+ expect(ended.terminatedAt).toBeDefined()
314
+ })
315
+ })
316
+ })
317
+
318
+ describe('Submit 전 상태 검증', () => {
319
+ it('Assigned 상태에서도 Submit이 가능해야 한다 (암묵적 시작)', async () => {
320
+ await withTestTransaction(async (context) => {
321
+ const { tx } = context.state
322
+ const domain = await domainFactory.create({}, tx)
323
+ const user = await userFactory.create({}, tx)
324
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
325
+
326
+ // Given: Assigned 상태 스레드
327
+ const approvalLine = [createRoleApprovalLineItem(approverRole)]
328
+ const instance = await activityInstanceFactory.createWithApprovalLine(
329
+ approvalLine,
330
+ { state: ActivityInstanceStatus.Assigned },
331
+ undefined,
332
+ domain,
333
+ tx
334
+ )
335
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
336
+ { state: ActivityThreadStatus.Assigned },
337
+ instance,
338
+ user,
339
+ tx
340
+ )
341
+
342
+ // Then: Assigned와 Started 둘 다 submit 가능한 상태
343
+ const canSubmit =
344
+ thread.state === ActivityThreadStatus.Assigned ||
345
+ thread.state === ActivityThreadStatus.Started
346
+ expect(canSubmit).toBe(true)
347
+ })
348
+ })
349
+
350
+ it('Ended 상태에서는 Submit이 불가능해야 한다', async () => {
351
+ await withTestTransaction(async (context) => {
352
+ const { tx } = context.state
353
+ const domain = await domainFactory.create({}, tx)
354
+ const user = await userFactory.create({}, tx)
355
+
356
+ // Given: Ended 상태 스레드
357
+ const instance = await activityInstanceFactory.createWithActivity(
358
+ { state: ActivityInstanceStatus.Ended },
359
+ undefined,
360
+ domain,
361
+ tx
362
+ )
363
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
364
+ {
365
+ state: ActivityThreadStatus.Ended,
366
+ terminatedAt: new Date()
367
+ },
368
+ instance,
369
+ user,
370
+ tx
371
+ )
372
+
373
+ // Then: Submit 불가능
374
+ const canSubmit =
375
+ thread.state === ActivityThreadStatus.Assigned ||
376
+ thread.state === ActivityThreadStatus.Started
377
+ expect(canSubmit).toBe(false)
378
+ })
379
+ })
380
+
381
+ it('Aborted 상태에서는 Submit이 불가능해야 한다', async () => {
382
+ await withTestTransaction(async (context) => {
383
+ const { tx } = context.state
384
+ const domain = await domainFactory.create({}, tx)
385
+ const user = await userFactory.create({}, tx)
386
+
387
+ // Given: Aborted 상태 스레드
388
+ const instance = await activityInstanceFactory.createWithActivity(
389
+ { state: ActivityInstanceStatus.Aborted },
390
+ undefined,
391
+ domain,
392
+ tx
393
+ )
394
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
395
+ {
396
+ state: ActivityThreadStatus.Aborted,
397
+ terminatedAt: new Date()
398
+ },
399
+ instance,
400
+ user,
401
+ tx
402
+ )
403
+
404
+ // Then: Submit 불가능
405
+ const canSubmit =
406
+ thread.state === ActivityThreadStatus.Assigned ||
407
+ thread.state === ActivityThreadStatus.Started
408
+ expect(canSubmit).toBe(false)
409
+ })
410
+ })
411
+ })
412
+
413
+ describe('startedAt 자동 설정', () => {
414
+ it('Submit 시 startedAt이 없으면 자동 설정되어야 한다', async () => {
415
+ await withTestTransaction(async (context) => {
416
+ const { tx } = context.state
417
+ const domain = await domainFactory.create({}, tx)
418
+ const user = await userFactory.create({}, tx)
419
+
420
+ // Given: startedAt이 없는 Assigned 스레드
421
+ const instance = await activityInstanceFactory.createWithActivity(
422
+ { state: ActivityInstanceStatus.Assigned },
423
+ undefined,
424
+ domain,
425
+ tx
426
+ )
427
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
428
+ { state: ActivityThreadStatus.Assigned },
429
+ instance,
430
+ user,
431
+ tx
432
+ )
433
+ expect(thread.startedAt).toBeFalsy() // null or undefined
434
+
435
+ // When: Submit 시 startedAt 설정
436
+ if (!thread.startedAt) {
437
+ thread.startedAt = new Date()
438
+ }
439
+ const updated = await tx.save('ActivityThread', thread) as any
440
+
441
+ // Then
442
+ expect(updated.startedAt).toBeDefined()
443
+ expect(updated.startedAt).toBeInstanceOf(Date)
444
+ })
445
+ })
446
+
447
+ it('Submit 시 startedAt이 이미 있으면 유지되어야 한다', async () => {
448
+ await withTestTransaction(async (context) => {
449
+ const { tx } = context.state
450
+ const domain = await domainFactory.create({}, tx)
451
+ const user = await userFactory.create({}, tx)
452
+
453
+ // Given: startedAt이 이미 있는 Started 스레드
454
+ const originalStartedAt = new Date('2024-01-01T00:00:00Z')
455
+ const instance = await activityInstanceFactory.createWithActivity(
456
+ { state: ActivityInstanceStatus.Started },
457
+ undefined,
458
+ domain,
459
+ tx
460
+ )
461
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
462
+ {
463
+ state: ActivityThreadStatus.Started,
464
+ startedAt: originalStartedAt
465
+ },
466
+ instance,
467
+ user,
468
+ tx
469
+ )
470
+
471
+ // When: Submit 시 startedAt 확인
472
+ if (!thread.startedAt) {
473
+ thread.startedAt = new Date()
474
+ }
475
+
476
+ // Then: 기존 startedAt 유지
477
+ expect(thread.startedAt).toEqual(originalStartedAt)
478
+ })
479
+ })
480
+ })
481
+ })