@things-factory/dataset 9.1.19 → 9.2.5

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 (27) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/activities/activity-data-review.js +5 -2
  3. package/dist-server/activities/activity-data-review.js.map +1 -1
  4. package/dist-server/activities/activity-ooc-review.js +4 -1
  5. package/dist-server/activities/activity-ooc-review.js.map +1 -1
  6. package/dist-server/controllers/create-data-ooc.js +2 -1
  7. package/dist-server/controllers/create-data-ooc.js.map +1 -1
  8. package/dist-server/controllers/create-data-sample.js +2 -2
  9. package/dist-server/controllers/create-data-sample.js.map +1 -1
  10. package/dist-server/controllers/issue-data-collection-task.js +9 -5
  11. package/dist-server/controllers/issue-data-collection-task.js.map +1 -1
  12. package/dist-server/controllers/issue-ooc-resolve.js +11 -6
  13. package/dist-server/controllers/issue-ooc-resolve.js.map +1 -1
  14. package/dist-server/controllers/issue-ooc-review.js +9 -6
  15. package/dist-server/controllers/issue-ooc-review.js.map +1 -1
  16. package/dist-server/tsconfig.tsbuildinfo +1 -1
  17. package/package.json +12 -12
  18. package/spec/integration/debug.spec.ts +42 -0
  19. package/spec/integration/ooc-lifecycle.spec.ts +484 -0
  20. package/spec/integration/ooc-workflow.spec.ts +276 -0
  21. package/spec/integration/simple.spec.ts +62 -0
  22. package/spec/unit/controllers/activity-callbacks.spec.ts +609 -0
  23. package/spec/unit/controllers/create-data-ooc.spec.ts +310 -0
  24. package/spec/unit/controllers/issue-ooc-resolve.spec.ts +431 -0
  25. package/spec/unit/controllers/issue-ooc-review.spec.ts +288 -0
  26. package/spec/unit/data-use-case.spec.ts +150 -0
  27. package/spec/unit/ooc-state-transition.spec.ts +233 -0
@@ -0,0 +1,609 @@
1
+ /**
2
+ * Activity Callback Unit Tests
3
+ * OOC Review/Resolve Activity callback 테스트
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
+ dataSetFactory,
13
+ dataOocFactory,
14
+ activityFactory,
15
+ activityInstanceFactory,
16
+ activityThreadFactory
17
+ } from '../../../../../test/factories'
18
+ import {
19
+ DataOocStatus,
20
+ ActivityInstanceStatus,
21
+ ActivityThreadStatus
22
+ } from '../../../../../test/entities/schemas'
23
+
24
+ describe('Activity Callbacks', () => {
25
+ let testDb: TestDatabase
26
+
27
+ beforeAll(async () => {
28
+ testDb = TestDatabase.getInstance()
29
+ })
30
+
31
+ describe('ActivityOocReview Callback', () => {
32
+ describe('Review Activity 완료 시 동작', () => {
33
+ it('ActivityInstance가 Ended 상태가 되면 DataOoc 상태가 REVIEWED로 전이되어야 한다', async () => {
34
+ await withTestTransaction(async (context) => {
35
+ const { tx, domain, user } = context.state
36
+
37
+ // Given: Review Activity와 DataOoc 설정
38
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
39
+ const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
40
+ let dataOoc = await dataOocFactory.createWithDataSetAndSample(
41
+ { state: DataOocStatus.ISSUED },
42
+ dataSet,
43
+ undefined,
44
+ tx
45
+ )
46
+
47
+ // Review ActivityInstance 생성
48
+ const reviewInstance = await activityInstanceFactory.createWithActivity(
49
+ {
50
+ name: `[OOC 검토] ${dataSet.name}`,
51
+ state: ActivityInstanceStatus.Issued,
52
+ input: { dataOocId: dataOoc.id },
53
+ output: {},
54
+ assigneeRole: supervisoryRole
55
+ },
56
+ reviewActivity,
57
+ domain,
58
+ tx
59
+ )
60
+
61
+ // When: ActivityInstance가 Ended 상태로 변경 (callback 시뮬레이션)
62
+ const terminatedAt = new Date()
63
+ const correctiveInstruction = 'Temperature를 정상 범위로 조절하세요'
64
+
65
+ reviewInstance.state = ActivityInstanceStatus.Ended
66
+ reviewInstance.terminatedAt = terminatedAt
67
+ reviewInstance.output = { instruction: correctiveInstruction }
68
+ await tx.save('ActivityInstance', reviewInstance)
69
+
70
+ // Callback 로직 시뮬레이션: DataOoc 업데이트
71
+ dataOoc = await tx.getRepository('DataOoc').findOne({
72
+ where: { id: dataOoc.id },
73
+ relations: ['dataSet']
74
+ }) as any
75
+
76
+ dataOoc.reviewedAt = terminatedAt
77
+ dataOoc.reviewer = user
78
+ dataOoc.correctiveInstruction = correctiveInstruction
79
+ dataOoc.state = DataOocStatus.REVIEWED
80
+ dataOoc = await tx.save('DataOoc', dataOoc)
81
+
82
+ // Then
83
+ expect(dataOoc.state).toBe(DataOocStatus.REVIEWED)
84
+ expect(dataOoc.reviewedAt).toEqual(terminatedAt)
85
+ expect(dataOoc.reviewer?.id).toBe(user.id)
86
+ expect(dataOoc.correctiveInstruction).toBe(correctiveInstruction)
87
+ })
88
+ })
89
+
90
+ it('Review 완료 시 correctiveInstruction이 output.instruction에서 가져와야 한다', async () => {
91
+ await withTestTransaction(async (context) => {
92
+ const { tx, domain, user } = context.state
93
+
94
+ // Given
95
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
96
+ const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx)
97
+ let dataOoc = await dataOocFactory.createWithDataSetAndSample(
98
+ { state: DataOocStatus.ISSUED },
99
+ dataSet,
100
+ undefined,
101
+ tx
102
+ )
103
+
104
+ const instruction = '습도 레벨을 30-70% 사이로 유지하세요'
105
+ const reviewInstance = await activityInstanceFactory.createWithActivity(
106
+ {
107
+ state: ActivityInstanceStatus.Ended,
108
+ input: { dataOocId: dataOoc.id },
109
+ output: { instruction },
110
+ terminatedAt: new Date()
111
+ },
112
+ reviewActivity,
113
+ domain,
114
+ tx
115
+ )
116
+
117
+ // When: Callback 로직
118
+ const outputInstruction = reviewInstance.output?.instruction
119
+
120
+ dataOoc.correctiveInstruction = outputInstruction
121
+ dataOoc.state = DataOocStatus.REVIEWED
122
+ dataOoc = await tx.save('DataOoc', dataOoc)
123
+
124
+ // Then
125
+ expect(dataOoc.correctiveInstruction).toBe(instruction)
126
+ })
127
+ })
128
+
129
+ it('Review 완료 후 issueOocResolve가 호출되어야 한다 (Resolve 발행)', async () => {
130
+ await withTestTransaction(async (context) => {
131
+ const { tx, domain, user } = context.state
132
+
133
+ // Given
134
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
135
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
136
+ const { dataSet, supervisoryRole, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx)
137
+
138
+ let dataOoc = await dataOocFactory.createWithDataSetAndSample(
139
+ { state: DataOocStatus.ISSUED },
140
+ dataSet,
141
+ undefined,
142
+ tx
143
+ )
144
+
145
+ const instruction = 'Fix the issue'
146
+ const terminatedAt = new Date()
147
+
148
+ // When: Review 완료 및 Callback 전체 로직 시뮬레이션
149
+ // 1. DataOoc REVIEWED로 업데이트
150
+ dataOoc.reviewedAt = terminatedAt
151
+ dataOoc.reviewer = user
152
+ dataOoc.correctiveInstruction = instruction
153
+ dataOoc.state = DataOocStatus.REVIEWED
154
+ dataOoc = await tx.save('DataOoc', dataOoc)
155
+
156
+ // 2. issueOocResolve 호출 (Resolve Activity 발행)
157
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
158
+ {
159
+ name: `[OOC 조치] ${dataSet.name}`,
160
+ state: ActivityInstanceStatus.Issued,
161
+ input: {
162
+ dataOocId: dataOoc.id,
163
+ instruction
164
+ },
165
+ assigneeRole: resolverRole,
166
+ approvalLine: dataSet.outlierApprovalLine || []
167
+ },
168
+ resolveActivity,
169
+ domain,
170
+ tx
171
+ )
172
+
173
+ dataOoc.resolveActivityInstance = resolveInstance
174
+ dataOoc = await tx.save('DataOoc', dataOoc)
175
+
176
+ // Then
177
+ expect(dataOoc.state).toBe(DataOocStatus.REVIEWED)
178
+ expect(dataOoc.resolveActivityInstance).toBeDefined()
179
+ expect(dataOoc.resolveActivityInstance?.input?.instruction).toBe(instruction)
180
+ })
181
+ })
182
+
183
+ it('Ended가 아닌 상태에서는 callback 로직이 실행되지 않아야 한다', async () => {
184
+ await withTestTransaction(async (context) => {
185
+ const { tx, domain } = context.state
186
+
187
+ // Given
188
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
189
+ const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx)
190
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
191
+ { state: DataOocStatus.ISSUED },
192
+ dataSet,
193
+ undefined,
194
+ tx
195
+ )
196
+
197
+ const reviewInstance = await activityInstanceFactory.createWithActivity(
198
+ {
199
+ state: ActivityInstanceStatus.Started, // Ended가 아님
200
+ input: { dataOocId: dataOoc.id }
201
+ },
202
+ reviewActivity,
203
+ domain,
204
+ tx
205
+ )
206
+
207
+ // When: Callback 조건 체크
208
+ const shouldExecuteCallback = reviewInstance.state === ActivityInstanceStatus.Ended
209
+
210
+ // Then
211
+ expect(shouldExecuteCallback).toBe(false)
212
+ })
213
+ })
214
+ })
215
+ })
216
+
217
+ describe('ActivityOocResolve Callback', () => {
218
+ describe('Resolve Activity 완료 시 동작', () => {
219
+ it('ActivityInstance가 Ended 상태가 되면 DataOoc 상태가 CORRECTED로 전이되어야 한다', async () => {
220
+ await withTestTransaction(async (context) => {
221
+ const { tx, domain, user } = context.state
222
+
223
+ // Given: REVIEWED 상태의 DataOoc과 Resolve Activity
224
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
225
+ const { dataSet, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx)
226
+ let dataOoc = await dataOocFactory.createWithDataSetAndSample(
227
+ {
228
+ state: DataOocStatus.REVIEWED,
229
+ reviewedAt: new Date(),
230
+ correctiveInstruction: 'Fix temperature'
231
+ },
232
+ dataSet,
233
+ undefined,
234
+ tx
235
+ )
236
+
237
+ // Resolve ActivityInstance 생성
238
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
239
+ {
240
+ name: `[OOC 조치] ${dataSet.name}`,
241
+ state: ActivityInstanceStatus.Issued,
242
+ input: {
243
+ dataOocId: dataOoc.id,
244
+ instruction: dataOoc.correctiveInstruction
245
+ },
246
+ output: {},
247
+ assigneeRole: resolverRole
248
+ },
249
+ resolveActivity,
250
+ domain,
251
+ tx
252
+ )
253
+
254
+ // Thread 생성 (corrector 정보 획득용)
255
+ const assignee = await userFactory.create({ name: 'Corrector User' }, tx)
256
+ const thread = await activityThreadFactory.createWithInstanceAndAssignee(
257
+ {
258
+ state: ActivityThreadStatus.Ended,
259
+ terminatedAt: new Date()
260
+ },
261
+ resolveInstance,
262
+ assignee,
263
+ tx
264
+ )
265
+
266
+ // When: ActivityInstance가 Ended 상태로 변경 (callback 시뮬레이션)
267
+ const terminatedAt = new Date()
268
+ const correctiveAction = 'Temperature를 정상 범위로 조절 완료'
269
+
270
+ resolveInstance.state = ActivityInstanceStatus.Ended
271
+ resolveInstance.terminatedAt = terminatedAt
272
+ resolveInstance.output = { action: correctiveAction }
273
+ await tx.save('ActivityInstance', resolveInstance)
274
+
275
+ // Callback 로직 시뮬레이션:
276
+ // 1. Ended 상태의 ActivityThread에서 corrector 조회
277
+ const endedThreads = await tx.getRepository('ActivityThread').find({
278
+ where: {
279
+ domain: { id: domain.id },
280
+ activityInstance: { id: resolveInstance.id },
281
+ state: ActivityThreadStatus.Ended
282
+ },
283
+ relations: ['assignee']
284
+ })
285
+ const corrector = endedThreads[0]?.assignee
286
+
287
+ // 2. DataOoc 업데이트
288
+ dataOoc = await tx.getRepository('DataOoc').findOne({
289
+ where: { id: dataOoc.id }
290
+ }) as any
291
+
292
+ dataOoc.correctedAt = terminatedAt
293
+ dataOoc.corrector = corrector
294
+ dataOoc.correctiveAction = correctiveAction
295
+ dataOoc.state = DataOocStatus.CORRECTED
296
+ dataOoc = await tx.save('DataOoc', dataOoc)
297
+
298
+ // Then
299
+ expect(dataOoc.state).toBe(DataOocStatus.CORRECTED)
300
+ expect(dataOoc.correctedAt).toEqual(terminatedAt)
301
+ expect(dataOoc.corrector?.id).toBe(assignee.id)
302
+ expect(dataOoc.correctiveAction).toBe(correctiveAction)
303
+ })
304
+ })
305
+
306
+ it('correctiveAction은 output.action에서 가져와야 한다', async () => {
307
+ await withTestTransaction(async (context) => {
308
+ const { tx, domain, user } = context.state
309
+
310
+ // Given
311
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
312
+ const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx)
313
+ let dataOoc = await dataOocFactory.createWithDataSetAndSample(
314
+ { state: DataOocStatus.REVIEWED },
315
+ dataSet,
316
+ undefined,
317
+ tx
318
+ )
319
+
320
+ const action = '온도 조절 밸브를 교체하고 캘리브레이션 수행'
321
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
322
+ {
323
+ state: ActivityInstanceStatus.Ended,
324
+ input: { dataOocId: dataOoc.id },
325
+ output: { action },
326
+ terminatedAt: new Date()
327
+ },
328
+ resolveActivity,
329
+ domain,
330
+ tx
331
+ )
332
+
333
+ // When: Callback 로직
334
+ const outputAction = resolveInstance.output?.action
335
+
336
+ dataOoc.correctiveAction = outputAction
337
+ dataOoc.state = DataOocStatus.CORRECTED
338
+ dataOoc = await tx.save('DataOoc', dataOoc)
339
+
340
+ // Then
341
+ expect(dataOoc.correctiveAction).toBe(action)
342
+ })
343
+ })
344
+
345
+ it('corrector는 Ended 상태의 ActivityThread의 assignee여야 한다', async () => {
346
+ await withTestTransaction(async (context) => {
347
+ const { tx, domain } = context.state
348
+
349
+ // Given
350
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
351
+ const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx)
352
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
353
+ { state: DataOocStatus.REVIEWED },
354
+ dataSet,
355
+ undefined,
356
+ tx
357
+ )
358
+
359
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
360
+ {
361
+ state: ActivityInstanceStatus.Ended,
362
+ input: { dataOocId: dataOoc.id }
363
+ },
364
+ resolveActivity,
365
+ domain,
366
+ tx
367
+ )
368
+
369
+ // 여러 Thread 중 Ended 상태인 Thread의 assignee
370
+ const worker1 = await userFactory.create({ name: 'Worker 1' }, tx)
371
+ const worker2 = await userFactory.create({ name: 'Worker 2' }, tx)
372
+
373
+ await activityThreadFactory.createWithInstanceAndAssignee(
374
+ { state: ActivityThreadStatus.Aborted }, // Aborted - 제외
375
+ resolveInstance,
376
+ worker1,
377
+ tx
378
+ )
379
+
380
+ const endedThread = await activityThreadFactory.createWithInstanceAndAssignee(
381
+ { state: ActivityThreadStatus.Ended }, // Ended - 선택됨
382
+ resolveInstance,
383
+ worker2,
384
+ tx
385
+ )
386
+
387
+ // When: Ended 상태의 Thread 조회
388
+ const endedThreads = await tx.getRepository('ActivityThread').find({
389
+ where: {
390
+ activityInstance: { id: resolveInstance.id },
391
+ state: ActivityThreadStatus.Ended
392
+ },
393
+ relations: ['assignee']
394
+ })
395
+
396
+ // Then: Ended 상태의 Thread의 assignee가 corrector
397
+ expect(endedThreads.length).toBe(1)
398
+ expect(endedThreads[0].assignee?.id).toBe(worker2.id)
399
+ })
400
+ })
401
+
402
+ it('Ended가 아닌 상태에서는 callback 로직이 실행되지 않아야 한다', async () => {
403
+ await withTestTransaction(async (context) => {
404
+ const { tx, domain } = context.state
405
+
406
+ // Given
407
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
408
+ const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx)
409
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
410
+ { state: DataOocStatus.REVIEWED },
411
+ dataSet,
412
+ undefined,
413
+ tx
414
+ )
415
+
416
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
417
+ {
418
+ state: ActivityInstanceStatus.Submitted, // Ended가 아님
419
+ input: { dataOocId: dataOoc.id }
420
+ },
421
+ resolveActivity,
422
+ domain,
423
+ tx
424
+ )
425
+
426
+ // When: Callback 조건 체크
427
+ const shouldExecuteCallback = resolveInstance.state === ActivityInstanceStatus.Ended
428
+
429
+ // Then
430
+ expect(shouldExecuteCallback).toBe(false)
431
+ })
432
+ })
433
+ })
434
+
435
+ describe('DataOoc 조회 및 업데이트', () => {
436
+ it('callback에서 dataOocId로 DataOoc을 조회할 수 있어야 한다', async () => {
437
+ await withTestTransaction(async (context) => {
438
+ const { tx, domain } = context.state
439
+
440
+ // Given
441
+ const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx)
442
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
443
+ { state: DataOocStatus.REVIEWED },
444
+ dataSet,
445
+ undefined,
446
+ tx
447
+ )
448
+
449
+ // When: input에서 dataOocId 추출 후 조회
450
+ const input = { dataOocId: dataOoc.id }
451
+ const foundDataOoc = await tx.getRepository('DataOoc').findOne({
452
+ where: {
453
+ domain: { id: domain.id },
454
+ id: input.dataOocId
455
+ },
456
+ relations: ['dataSet']
457
+ })
458
+
459
+ // Then
460
+ expect(foundDataOoc).toBeDefined()
461
+ expect(foundDataOoc?.id).toBe(dataOoc.id)
462
+ expect(foundDataOoc?.dataSet?.id).toBe(dataSet.id)
463
+ })
464
+ })
465
+ })
466
+ })
467
+
468
+ describe('전체 워크플로우 Callback 연계', () => {
469
+ it('OOC 생성 → Review Callback → Resolve Callback 전체 플로우', async () => {
470
+ await withTestTransaction(async (context) => {
471
+ const { tx, domain, user } = context.state
472
+
473
+ // Setup
474
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
475
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
476
+ const { dataSet, supervisoryRole, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx)
477
+
478
+ const reviewer = await userFactory.create({ name: 'Reviewer' }, tx)
479
+ const corrector = await userFactory.create({ name: 'Corrector' }, tx)
480
+
481
+ // 1. DataOoc 생성 (ISSUED)
482
+ let dataOoc = await dataOocFactory.createWithDataSetAndSample(
483
+ {
484
+ state: DataOocStatus.ISSUED,
485
+ history: [
486
+ {
487
+ user: { id: user.id, name: user.name },
488
+ state: DataOocStatus.ISSUED,
489
+ timestamp: new Date().toISOString()
490
+ }
491
+ ]
492
+ },
493
+ dataSet,
494
+ undefined,
495
+ tx
496
+ )
497
+ expect(dataOoc.state).toBe(DataOocStatus.ISSUED)
498
+
499
+ // 2. Review Activity 발행
500
+ const reviewInstance = await activityInstanceFactory.createWithActivity(
501
+ {
502
+ state: ActivityInstanceStatus.Issued,
503
+ input: { dataOocId: dataOoc.id },
504
+ assigneeRole: supervisoryRole
505
+ },
506
+ reviewActivity,
507
+ domain,
508
+ tx
509
+ )
510
+
511
+ // 3. Review Callback 시뮬레이션 (Ended 상태로)
512
+ const reviewTerminatedAt = new Date()
513
+ const instruction = 'Temperature 조절 필요'
514
+
515
+ reviewInstance.state = ActivityInstanceStatus.Ended
516
+ reviewInstance.terminatedAt = reviewTerminatedAt
517
+ reviewInstance.output = { instruction }
518
+ await tx.save('ActivityInstance', reviewInstance)
519
+
520
+ // DataOoc 업데이트 (Review Callback)
521
+ dataOoc = await tx.getRepository('DataOoc').findOne({
522
+ where: { id: dataOoc.id },
523
+ relations: ['dataSet']
524
+ }) as any
525
+
526
+ dataOoc.reviewedAt = reviewTerminatedAt
527
+ dataOoc.reviewer = reviewer
528
+ dataOoc.correctiveInstruction = instruction
529
+ dataOoc.state = DataOocStatus.REVIEWED
530
+
531
+ // history 업데이트
532
+ const history = dataOoc.history || []
533
+ history.push({
534
+ user: { id: reviewer.id, name: reviewer.name },
535
+ state: DataOocStatus.REVIEWED,
536
+ timestamp: reviewTerminatedAt.toISOString()
537
+ })
538
+ dataOoc.history = history
539
+ dataOoc = await tx.save('DataOoc', dataOoc)
540
+
541
+ expect(dataOoc.state).toBe(DataOocStatus.REVIEWED)
542
+
543
+ // 4. Resolve Activity 발행 (Review Callback에서 호출)
544
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
545
+ {
546
+ state: ActivityInstanceStatus.Issued,
547
+ input: { dataOocId: dataOoc.id, instruction },
548
+ assigneeRole: resolverRole
549
+ },
550
+ resolveActivity,
551
+ domain,
552
+ tx
553
+ )
554
+
555
+ // 5. Resolve Thread 생성 및 완료
556
+ const resolveThread = await activityThreadFactory.createWithInstanceAndAssignee(
557
+ { state: ActivityThreadStatus.Ended, terminatedAt: new Date() },
558
+ resolveInstance,
559
+ corrector,
560
+ tx
561
+ )
562
+
563
+ // 6. Resolve Callback 시뮬레이션 (Ended 상태로)
564
+ const resolveTerminatedAt = new Date()
565
+ const action = 'Temperature를 정상 범위로 조절함'
566
+
567
+ resolveInstance.state = ActivityInstanceStatus.Ended
568
+ resolveInstance.terminatedAt = resolveTerminatedAt
569
+ resolveInstance.output = { action }
570
+ await tx.save('ActivityInstance', resolveInstance)
571
+
572
+ // DataOoc 업데이트 (Resolve Callback)
573
+ dataOoc = await tx.getRepository('DataOoc').findOne({
574
+ where: { id: dataOoc.id },
575
+ relations: ['reviewer']
576
+ }) as any
577
+
578
+ dataOoc.correctedAt = resolveTerminatedAt
579
+ dataOoc.corrector = corrector
580
+ dataOoc.correctiveAction = action
581
+ dataOoc.state = DataOocStatus.CORRECTED
582
+
583
+ // history 업데이트
584
+ dataOoc.history.push({
585
+ user: { id: corrector.id, name: corrector.name },
586
+ state: DataOocStatus.CORRECTED,
587
+ comment: action,
588
+ timestamp: resolveTerminatedAt.toISOString()
589
+ })
590
+ dataOoc = await tx.save('DataOoc', dataOoc)
591
+
592
+ // Then: 최종 상태 확인 - 다시 조회하여 relations 포함
593
+ const finalDataOoc = await tx.getRepository('DataOoc').findOne({
594
+ where: { id: dataOoc.id },
595
+ relations: ['reviewer', 'corrector']
596
+ }) as any
597
+
598
+ expect(finalDataOoc.state).toBe(DataOocStatus.CORRECTED)
599
+ expect(finalDataOoc.reviewedAt).toBeDefined()
600
+ expect(finalDataOoc.correctedAt).toBeDefined()
601
+ expect(finalDataOoc.reviewer?.id).toBe(reviewer.id)
602
+ expect(finalDataOoc.corrector?.id).toBe(corrector.id)
603
+ expect(finalDataOoc.correctiveInstruction).toBe(instruction)
604
+ expect(finalDataOoc.correctiveAction).toBe(action)
605
+ expect(finalDataOoc.history.length).toBe(3)
606
+ })
607
+ })
608
+ })
609
+ })