@things-factory/dataset 9.1.19 → 9.2.13

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 +13 -13
  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,431 @@
1
+ /**
2
+ * issue-ooc-resolve Controller Unit Tests
3
+ * OOC Resolve Activity 발행 컨트롤러 테스트
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
+ } from '../../../../../test/factories'
17
+ import { DataOocStatus, ActivityInstanceStatus } from '../../../../../test/entities/schemas'
18
+
19
+ describe('issue-ooc-resolve Controller', () => {
20
+ let testDb: TestDatabase
21
+
22
+ beforeAll(async () => {
23
+ testDb = TestDatabase.getInstance()
24
+ })
25
+
26
+ describe('OOC Resolve Activity 발행 조건', () => {
27
+ it('Activity가 존재하고 resolverRole이 있으면 Resolve Instance가 생성되어야 한다', async () => {
28
+ await withTestTransaction(async (context) => {
29
+ const { tx, domain } = context.state
30
+
31
+ // Given: OOC Resolve Activity와 resolverRole이 있는 DataSet
32
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
33
+ const resolverRole = await roleFactory.create({ name: 'Resolver', domain }, tx)
34
+ const dataSet = await dataSetFactory.createWithDomain(
35
+ { resolverRole },
36
+ domain,
37
+ tx
38
+ )
39
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
40
+ {
41
+ state: DataOocStatus.REVIEWED,
42
+ correctiveInstruction: 'Temperature 조절 필요'
43
+ },
44
+ dataSet,
45
+ undefined,
46
+ tx
47
+ )
48
+
49
+ // When: issueOocResolve 로직 시뮬레이션
50
+ const activityInstance = await activityInstanceFactory.createWithActivity(
51
+ {
52
+ name: `[OOC 조치] ${dataSet.name}`,
53
+ description: dataSet.description,
54
+ state: ActivityInstanceStatus.Issued,
55
+ dueAt: new Date(Date.now() + resolveActivity.standardTime * 1000),
56
+ input: {
57
+ dataOocId: dataOoc.id,
58
+ instruction: dataOoc.correctiveInstruction
59
+ },
60
+ assigneeRole: resolverRole,
61
+ threadsMin: 1,
62
+ threadsMax: 1,
63
+ approvalLine: dataSet.outlierApprovalLine || []
64
+ },
65
+ resolveActivity,
66
+ domain,
67
+ tx
68
+ )
69
+
70
+ // Then
71
+ expect(activityInstance).toBeDefined()
72
+ expect(activityInstance.name).toBe(`[OOC 조치] ${dataSet.name}`)
73
+ expect(activityInstance.input?.dataOocId).toBe(dataOoc.id)
74
+ expect(activityInstance.input?.instruction).toBe('Temperature 조절 필요')
75
+ expect(activityInstance.assigneeRole?.id).toBe(resolverRole.id)
76
+ expect(activityInstance.activity?.id).toBe(resolveActivity.id)
77
+ })
78
+ })
79
+
80
+ it('resolverRole이 없으면 Resolve Activity가 발행되지 않아야 한다', async () => {
81
+ await withTestTransaction(async (context) => {
82
+ const { tx, domain } = context.state
83
+
84
+ // Given: resolverRole이 없는 DataSet
85
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
86
+ const dataSet = await dataSetFactory.createWithDomain(
87
+ { resolverRole: undefined, resolverRoleId: undefined },
88
+ domain,
89
+ tx
90
+ )
91
+
92
+ // When & Then: resolverRole 체크
93
+ const resolverRoleId = dataSet.resolverRoleId
94
+ expect(resolverRoleId).toBeFalsy()
95
+
96
+ // issueOocResolve 로직에서는 이 경우 console.error만 출력하고 리턴
97
+ })
98
+ })
99
+
100
+ it('OOC Resolve Activity가 설치되지 않으면 Resolve가 발행되지 않아야 한다', async () => {
101
+ await withTestTransaction(async (context) => {
102
+ const { tx, domain } = context.state
103
+
104
+ // Given: OOC Resolve Activity가 없는 상태
105
+ const { dataSet, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx)
106
+
107
+ // When: Activity 조회
108
+ const activity = await tx
109
+ .getRepository('Activity')
110
+ .findOne({ where: { domain: { id: domain.id }, name: 'OOC Resolve' } })
111
+
112
+ // Then: Activity가 없음
113
+ expect(activity).toBeNull()
114
+ // issueOocResolve 로직에서는 이 경우 console.error만 출력하고 리턴
115
+ })
116
+ })
117
+ })
118
+
119
+ describe('OOC Resolve Instance 속성', () => {
120
+ it('input에 dataOocId와 instruction이 포함되어야 한다', async () => {
121
+ await withTestTransaction(async (context) => {
122
+ const { tx, domain } = context.state
123
+
124
+ // Given
125
+ const { dataSet, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx)
126
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
127
+ {
128
+ state: DataOocStatus.REVIEWED,
129
+ correctiveInstruction: 'Temperature를 20-80 범위로 조절하세요'
130
+ },
131
+ dataSet,
132
+ undefined,
133
+ tx
134
+ )
135
+
136
+ // When: issueOocResolve 입력 구성
137
+ const input = {
138
+ dataOocId: dataOoc.id,
139
+ instruction: dataOoc.correctiveInstruction
140
+ }
141
+
142
+ // Then
143
+ expect(input.dataOocId).toBe(dataOoc.id)
144
+ expect(input.instruction).toBe('Temperature를 20-80 범위로 조절하세요')
145
+ })
146
+ })
147
+
148
+ it('dueAt은 현재시간 + standardTime으로 계산되어야 한다 (Review와 다름)', async () => {
149
+ await withTestTransaction(async (context) => {
150
+ const { tx, domain } = context.state
151
+
152
+ // Given
153
+ const standardTime = 72 * 60 * 60 // 72시간
154
+ const resolveActivity = await activityFactory.createWithDomain(
155
+ {
156
+ name: 'OOC Resolve',
157
+ standardTime
158
+ },
159
+ domain,
160
+ tx
161
+ )
162
+
163
+ // When: dueAt 계산 (현재 시간 기준)
164
+ const now = Date.now()
165
+ const expectedDueAt = new Date(now + standardTime * 1000)
166
+
167
+ // Then: dueAt이 현재시간 + standardTime
168
+ // Review는 collectedAt 기준, Resolve는 현재시간 기준
169
+ expect(expectedDueAt.getTime()).toBeGreaterThan(now)
170
+ expect(expectedDueAt.getTime() - now).toBe(standardTime * 1000)
171
+ })
172
+ })
173
+ })
174
+
175
+ describe('outlierApprovalLine 적용', () => {
176
+ it('DataSet의 outlierApprovalLine이 Resolve Instance에 적용되어야 한다', async () => {
177
+ await withTestTransaction(async (context) => {
178
+ const { tx, domain } = context.state
179
+
180
+ // Given: outlierApprovalLine이 설정된 DataSet
181
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
182
+ const outlierApprovalLine = [
183
+ {
184
+ type: 'Role',
185
+ value: approverRole.id,
186
+ approver: { id: approverRole.id, name: approverRole.name }
187
+ }
188
+ ]
189
+
190
+ const resolverRole = await roleFactory.create({ name: 'Resolver', domain }, tx)
191
+ const dataSet = await dataSetFactory.createWithDomain(
192
+ {
193
+ resolverRole,
194
+ outlierApprovalLine
195
+ },
196
+ domain,
197
+ tx
198
+ )
199
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
200
+
201
+ // When: Resolve Instance 생성
202
+ const activityInstance = await activityInstanceFactory.createWithActivity(
203
+ {
204
+ state: ActivityInstanceStatus.Issued,
205
+ assigneeRole: resolverRole,
206
+ approvalLine: dataSet.outlierApprovalLine
207
+ },
208
+ resolveActivity,
209
+ domain,
210
+ tx
211
+ )
212
+
213
+ // Then
214
+ expect(activityInstance.approvalLine).toBeDefined()
215
+ expect(activityInstance.approvalLine?.length).toBe(1)
216
+ expect(activityInstance.approvalLine?.[0].type).toBe('Role')
217
+ expect(activityInstance.approvalLine?.[0].approver?.id).toBe(approverRole.id)
218
+ })
219
+ })
220
+
221
+ it('outlierApprovalLine이 없으면 빈 배열 또는 undefined로 설정되어야 한다', async () => {
222
+ await withTestTransaction(async (context) => {
223
+ const { tx, domain } = context.state
224
+
225
+ // Given: outlierApprovalLine이 없는 DataSet
226
+ const { dataSet, resolverRole } = await dataSetFactory.createWithRoles(
227
+ { outlierApprovalLine: undefined },
228
+ domain,
229
+ tx
230
+ )
231
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
232
+
233
+ // When: Resolve Instance 생성
234
+ const activityInstance = await activityInstanceFactory.createWithActivity(
235
+ {
236
+ state: ActivityInstanceStatus.Issued,
237
+ assigneeRole: resolverRole,
238
+ approvalLine: dataSet.outlierApprovalLine || []
239
+ },
240
+ resolveActivity,
241
+ domain,
242
+ tx
243
+ )
244
+
245
+ // Then
246
+ expect(activityInstance.approvalLine).toEqual([])
247
+ })
248
+ })
249
+
250
+ it('다중 레벨 결재라인이 올바르게 적용되어야 한다', async () => {
251
+ await withTestTransaction(async (context) => {
252
+ const { tx, domain } = context.state
253
+
254
+ // Given: 2단계 결재라인
255
+ const approverRole1 = await roleFactory.create({ name: 'Team Lead', domain }, tx)
256
+ const approverRole2 = await roleFactory.create({ name: 'Manager', domain }, tx)
257
+ const outlierApprovalLine = [
258
+ { type: 'Role', value: approverRole1.id, approver: { id: approverRole1.id, name: approverRole1.name } },
259
+ { type: 'Role', value: approverRole2.id, approver: { id: approverRole2.id, name: approverRole2.name } }
260
+ ]
261
+
262
+ const resolverRole = await roleFactory.create({ name: 'Resolver', domain }, tx)
263
+ const dataSet = await dataSetFactory.createWithDomain(
264
+ {
265
+ resolverRole,
266
+ outlierApprovalLine
267
+ },
268
+ domain,
269
+ tx
270
+ )
271
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
272
+
273
+ // When
274
+ const activityInstance = await activityInstanceFactory.createWithActivity(
275
+ {
276
+ state: ActivityInstanceStatus.Issued,
277
+ assigneeRole: resolverRole,
278
+ approvalLine: dataSet.outlierApprovalLine
279
+ },
280
+ resolveActivity,
281
+ domain,
282
+ tx
283
+ )
284
+
285
+ // Then
286
+ expect(activityInstance.approvalLine?.length).toBe(2)
287
+ expect(activityInstance.approvalLine?.[0].approver?.id).toBe(approverRole1.id)
288
+ expect(activityInstance.approvalLine?.[1].approver?.id).toBe(approverRole2.id)
289
+ })
290
+ })
291
+
292
+ it('Employee 타입 결재라인도 지원되어야 한다', async () => {
293
+ await withTestTransaction(async (context) => {
294
+ const { tx, domain } = context.state
295
+
296
+ // Given: Employee 타입 결재라인
297
+ const approverUser = await userFactory.create({ name: 'Approver User' }, tx)
298
+ const outlierApprovalLine = [
299
+ {
300
+ type: 'Employee',
301
+ value: approverUser.id,
302
+ approver: { id: approverUser.id, name: approverUser.name }
303
+ }
304
+ ]
305
+
306
+ const resolverRole = await roleFactory.create({ name: 'Resolver', domain }, tx)
307
+ const dataSet = await dataSetFactory.createWithDomain(
308
+ {
309
+ resolverRole,
310
+ outlierApprovalLine
311
+ },
312
+ domain,
313
+ tx
314
+ )
315
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
316
+
317
+ // When
318
+ const activityInstance = await activityInstanceFactory.createWithActivity(
319
+ {
320
+ state: ActivityInstanceStatus.Issued,
321
+ assigneeRole: resolverRole,
322
+ approvalLine: dataSet.outlierApprovalLine
323
+ },
324
+ resolveActivity,
325
+ domain,
326
+ tx
327
+ )
328
+
329
+ // Then
330
+ expect(activityInstance.approvalLine?.[0].type).toBe('Employee')
331
+ expect(activityInstance.approvalLine?.[0].approver?.id).toBe(approverUser.id)
332
+ })
333
+ })
334
+ })
335
+
336
+ describe('DataOoc과 ResolveActivityInstance 연결', () => {
337
+ it('DataOoc의 resolveActivityInstance 필드에 생성된 Instance가 저장되어야 한다', async () => {
338
+ await withTestTransaction(async (context) => {
339
+ const { tx, domain } = context.state
340
+
341
+ // Given
342
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
343
+ const { dataSet, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx)
344
+ let dataOoc = await dataOocFactory.createWithDataSetAndSample(
345
+ {
346
+ state: DataOocStatus.REVIEWED,
347
+ correctiveInstruction: 'Fix temperature'
348
+ },
349
+ dataSet,
350
+ undefined,
351
+ tx
352
+ )
353
+
354
+ // When: Resolve Instance 생성 및 DataOoc에 연결
355
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
356
+ {
357
+ name: `[OOC 조치] ${dataSet.name}`,
358
+ state: ActivityInstanceStatus.Issued,
359
+ input: {
360
+ dataOocId: dataOoc.id,
361
+ instruction: dataOoc.correctiveInstruction
362
+ },
363
+ assigneeRole: resolverRole
364
+ },
365
+ resolveActivity,
366
+ domain,
367
+ tx
368
+ )
369
+
370
+ dataOoc.resolveActivityInstance = resolveInstance
371
+ dataOoc = await tx.save('DataOoc', dataOoc)
372
+
373
+ // Then
374
+ expect(dataOoc.resolveActivityInstance).toBeDefined()
375
+ expect(dataOoc.resolveActivityInstance?.id).toBe(resolveInstance.id)
376
+ })
377
+ })
378
+ })
379
+
380
+ describe('Review → Resolve 연계', () => {
381
+ it('Review 완료 후 Resolve가 자동으로 발행되는 플로우 테스트', async () => {
382
+ await withTestTransaction(async (context) => {
383
+ const { tx, domain, user } = context.state
384
+
385
+ // Given: Review 완료 상태의 DataOoc
386
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
387
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
388
+ const { dataSet, supervisoryRole, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx)
389
+
390
+ let dataOoc = await dataOocFactory.createWithDataSetAndSample(
391
+ { state: DataOocStatus.ISSUED },
392
+ dataSet,
393
+ undefined,
394
+ tx
395
+ )
396
+
397
+ // Review 완료 시뮬레이션
398
+ dataOoc.state = DataOocStatus.REVIEWED
399
+ dataOoc.reviewedAt = new Date()
400
+ dataOoc.reviewer = user
401
+ dataOoc.correctiveInstruction = 'Temperature 조절 필요'
402
+ dataOoc = await tx.save('DataOoc', dataOoc)
403
+
404
+ // When: Resolve Activity 발행 (activity-ooc-review callback에서 호출됨)
405
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
406
+ {
407
+ name: `[OOC 조치] ${dataSet.name}`,
408
+ state: ActivityInstanceStatus.Issued,
409
+ input: {
410
+ dataOocId: dataOoc.id,
411
+ instruction: dataOoc.correctiveInstruction
412
+ },
413
+ assigneeRole: resolverRole,
414
+ approvalLine: dataSet.outlierApprovalLine || []
415
+ },
416
+ resolveActivity,
417
+ domain,
418
+ tx
419
+ )
420
+
421
+ dataOoc.resolveActivityInstance = resolveInstance
422
+ dataOoc = await tx.save('DataOoc', dataOoc)
423
+
424
+ // Then: Review 완료 후 Resolve가 발행됨
425
+ expect(dataOoc.state).toBe(DataOocStatus.REVIEWED)
426
+ expect(dataOoc.resolveActivityInstance).toBeDefined()
427
+ expect(dataOoc.resolveActivityInstance?.input?.instruction).toBe('Temperature 조절 필요')
428
+ })
429
+ })
430
+ })
431
+ })