@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/dataset",
3
- "version": "9.1.19",
3
+ "version": "9.2.13",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -40,21 +40,21 @@
40
40
  "@operato/shell": "^9.0.0",
41
41
  "@operato/styles": "^9.0.0",
42
42
  "@operato/utils": "^9.0.0",
43
- "@things-factory/auth-base": "^9.1.19",
44
- "@things-factory/aws-base": "^9.1.19",
45
- "@things-factory/board-service": "^9.1.19",
46
- "@things-factory/env": "^9.1.13",
47
- "@things-factory/integration-base": "^9.1.19",
48
- "@things-factory/organization": "^9.1.19",
49
- "@things-factory/personalization": "^9.1.19",
50
- "@things-factory/scheduler-client": "^9.1.19",
51
- "@things-factory/shell": "^9.1.19",
52
- "@things-factory/work-shift": "^9.1.19",
53
- "@things-factory/worklist": "^9.1.19",
43
+ "@things-factory/auth-base": "^9.2.13",
44
+ "@things-factory/aws-base": "^9.2.13",
45
+ "@things-factory/board-service": "^9.2.13",
46
+ "@things-factory/env": "^9.2.13",
47
+ "@things-factory/integration-base": "^9.2.13",
48
+ "@things-factory/organization": "^9.2.13",
49
+ "@things-factory/personalization": "^9.2.13",
50
+ "@things-factory/scheduler-client": "^9.2.13",
51
+ "@things-factory/shell": "^9.2.13",
52
+ "@things-factory/work-shift": "^9.2.13",
53
+ "@things-factory/worklist": "^9.2.13",
54
54
  "cron-parser": "^4.3.0",
55
55
  "moment-timezone": "^0.5.45",
56
56
  "simple-statistics": "^7.8.3",
57
57
  "statistics": "^3.3.0"
58
58
  },
59
- "gitHead": "078438034dbe19915108e89ff24024f7044a85a9"
59
+ "gitHead": "d65748803a86a1fb6c9810ea4f93519c6f44f6d5"
60
60
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Debug Test
3
+ * 엔티티 등록 디버깅
4
+ */
5
+
6
+ import { TestDatabase } from '../../../../test/test-database'
7
+
8
+ describe('Debug Entity Registration', () => {
9
+ let testDb: TestDatabase
10
+
11
+ beforeAll(async () => {
12
+ testDb = TestDatabase.getInstance()
13
+ })
14
+
15
+ it('should debug entity registration', () => {
16
+ const ds = testDb.getDataSource()
17
+
18
+ console.log('DataSource initialized:', ds.isInitialized)
19
+ console.log('Number of entity metadatas:', ds.entityMetadatas.length)
20
+
21
+ ds.entityMetadatas.forEach((meta) => {
22
+ console.log('Registered entity:', meta.name)
23
+ })
24
+ })
25
+
26
+ it('should create and retrieve a Domain using entity name', async () => {
27
+ const manager = testDb.getManager()
28
+
29
+ const domain = await manager.save('Domain', {
30
+ name: 'Test',
31
+ subdomain: 'test',
32
+ timezone: 'UTC'
33
+ })
34
+
35
+ expect(domain.id).toBeDefined()
36
+ expect(domain.name).toBe('Test')
37
+
38
+ const found = await manager.findOne('Domain', { where: { id: domain.id } })
39
+ expect(found).toBeDefined()
40
+ expect(found?.name).toBe('Test')
41
+ })
42
+ })
@@ -0,0 +1,484 @@
1
+ /**
2
+ * OOC Lifecycle Integration Tests
3
+ * DataOoc 전체 생명주기 통합 테스트
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
+ dataSampleFactory,
14
+ dataOocFactory,
15
+ activityFactory,
16
+ activityInstanceFactory,
17
+ activityThreadFactory
18
+ } from '../../../../test/factories'
19
+
20
+ import {
21
+ DataOocStatus,
22
+ ActivityInstanceStatus,
23
+ ActivityThreadStatus
24
+ } from '../../../../test/entities/schemas'
25
+
26
+ import {
27
+ simulateOocReviewCompletion,
28
+ simulateOocResolveCompletion,
29
+ createHistoryEntry,
30
+ isValidOocTransition
31
+ } from '../../../../test/helpers/workflow-helpers'
32
+
33
+ describe('OOC Complete Lifecycle Integration Tests', () => {
34
+ let testDb: TestDatabase
35
+
36
+ beforeAll(async () => {
37
+ testDb = TestDatabase.getInstance()
38
+ })
39
+
40
+ describe('OOC Creation from DataSample', () => {
41
+ it('DataSample에서 ooc=true일 때 DataOoc이 생성되어야 한다', async () => {
42
+ await withTestTransaction(async (context) => {
43
+ const { tx } = context.state
44
+
45
+ // Given: OOC 조건의 DataSample
46
+ const { dataSet } = await dataSetFactory.createWithRoles({}, undefined, tx)
47
+ const dataSample = await dataSampleFactory.createOocSample({}, dataSet, tx)
48
+
49
+ // When: DataOoc 생성
50
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
51
+ {
52
+ state: DataOocStatus.ISSUED,
53
+ data: dataSample.data,
54
+ ooc: true,
55
+ oos: false
56
+ },
57
+ dataSet,
58
+ dataSample,
59
+ tx
60
+ )
61
+
62
+ // Then: OOC이 생성되어야 함
63
+ expect(dataOoc).toBeDefined()
64
+ expect(dataOoc.id).toBeDefined()
65
+ expect(dataOoc.ooc).toBe(true)
66
+ expect(dataOoc.state).toBe(DataOocStatus.ISSUED)
67
+ expect(dataOoc.dataSet?.id).toBe(dataSet.id)
68
+ expect(dataOoc.dataSample?.id).toBe(dataSample.id)
69
+ })
70
+ })
71
+
72
+ it('DataOoc 생성 시 초기 상태는 ISSUED여야 한다', async () => {
73
+ await withTestTransaction(async (context) => {
74
+ const { tx } = context.state
75
+
76
+ // Given & When: DataOoc 생성
77
+ const dataOoc = await dataOocFactory.create(
78
+ { state: DataOocStatus.ISSUED },
79
+ tx
80
+ )
81
+
82
+ // Then
83
+ expect(dataOoc.state).toBe(DataOocStatus.ISSUED)
84
+ })
85
+ })
86
+
87
+ it('DataOoc 생성 시 history 배열이 초기화되어야 한다', async () => {
88
+ await withTestTransaction(async (context) => {
89
+ const { tx, user } = context.state
90
+
91
+ // Given & When
92
+ const dataOoc = await dataOocFactory.create(
93
+ {
94
+ state: DataOocStatus.ISSUED,
95
+ history: [createHistoryEntry(user, DataOocStatus.ISSUED)]
96
+ },
97
+ tx
98
+ )
99
+
100
+ // Then
101
+ expect(dataOoc.history).toBeDefined()
102
+ expect(dataOoc.history?.length).toBe(1)
103
+ expect(dataOoc.history?.[0].state).toBe(DataOocStatus.ISSUED)
104
+ })
105
+ })
106
+
107
+ it('DataOoc의 data, judgment 필드가 DataSample에서 복사되어야 한다', async () => {
108
+ await withTestTransaction(async (context) => {
109
+ const { tx } = context.state
110
+
111
+ // Given
112
+ const { dataSet } = await dataSetFactory.createWithRoles({}, undefined, tx)
113
+ const sampleData = { temperature: 95, humidity: 50 }
114
+ const sampleJudgment = {
115
+ temperature: { ooc: true, oos: false },
116
+ humidity: { ooc: false, oos: false }
117
+ }
118
+
119
+ // When
120
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
121
+ {
122
+ data: sampleData,
123
+ judgment: sampleJudgment,
124
+ ooc: true
125
+ },
126
+ dataSet,
127
+ undefined,
128
+ tx
129
+ )
130
+
131
+ // Then
132
+ expect(dataOoc.data).toEqual(sampleData)
133
+ expect(dataOoc.judgment).toEqual(sampleJudgment)
134
+ })
135
+ })
136
+ })
137
+
138
+ describe('OOC Review Activity Workflow', () => {
139
+ it('OOC Review 발행 시 supervisoryRole로 할당되어야 한다', async () => {
140
+ await withTestTransaction(async (context) => {
141
+ const { tx } = context.state
142
+ const domain = await domainFactory.create({}, tx)
143
+
144
+ // Given: DataSet with supervisoryRole
145
+ const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles(
146
+ {},
147
+ domain,
148
+ tx
149
+ )
150
+
151
+ // When: OOC Review Activity 발행 시뮬레이션
152
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
153
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
154
+ { state: DataOocStatus.ISSUED },
155
+ dataSet,
156
+ undefined,
157
+ tx
158
+ )
159
+
160
+ const reviewInstance = await activityInstanceFactory.createWithActivity(
161
+ {
162
+ name: `[OOC 검토] ${dataSet.name}`,
163
+ state: ActivityInstanceStatus.Issued,
164
+ assigneeRole: supervisoryRole,
165
+ input: { dataOocId: dataOoc.id }
166
+ },
167
+ reviewActivity,
168
+ domain,
169
+ tx
170
+ )
171
+
172
+ // Then
173
+ expect(reviewInstance.assigneeRole?.id).toBe(supervisoryRole.id)
174
+ expect(reviewInstance.input?.dataOocId).toBe(dataOoc.id)
175
+ })
176
+ })
177
+
178
+ it('OOC Review 완료 시 DataOoc 상태가 REVIEWED로 전이되어야 한다', async () => {
179
+ await withTestTransaction(async (context) => {
180
+ const { tx, user } = context.state
181
+
182
+ // Given
183
+ const dataOoc = await dataOocFactory.create(
184
+ { state: DataOocStatus.ISSUED },
185
+ tx
186
+ )
187
+
188
+ // When: Review 완료 시뮬레이션
189
+ const reviewed = await simulateOocReviewCompletion(
190
+ dataOoc,
191
+ 'Temperature 조절 필요',
192
+ user,
193
+ { tx, domain: context.state.domain, user }
194
+ )
195
+
196
+ // Then
197
+ expect(reviewed.state).toBe(DataOocStatus.REVIEWED)
198
+ expect(reviewed.reviewedAt).toBeDefined()
199
+ expect(reviewed.reviewer?.id).toBe(user.id)
200
+ expect(reviewed.correctiveInstruction).toBe('Temperature 조절 필요')
201
+ })
202
+ })
203
+
204
+ it('OOC Review 완료 시 history에 기록이 추가되어야 한다', async () => {
205
+ await withTestTransaction(async (context) => {
206
+ const { tx, user } = context.state
207
+
208
+ // Given
209
+ const initialHistory = [createHistoryEntry(user, DataOocStatus.ISSUED)]
210
+ const dataOoc = await dataOocFactory.create(
211
+ {
212
+ state: DataOocStatus.ISSUED,
213
+ history: initialHistory
214
+ },
215
+ tx
216
+ )
217
+
218
+ // When
219
+ const reviewed = await simulateOocReviewCompletion(
220
+ dataOoc,
221
+ 'Instruction',
222
+ user,
223
+ { tx, domain: context.state.domain, user }
224
+ )
225
+
226
+ // Then
227
+ expect(reviewed.history?.length).toBe(2)
228
+ expect(reviewed.history?.[1].state).toBe(DataOocStatus.REVIEWED)
229
+ })
230
+ })
231
+ })
232
+
233
+ describe('OOC Resolve Activity Workflow', () => {
234
+ it('OOC Resolve 발행 시 resolverRole로 할당되어야 한다', async () => {
235
+ await withTestTransaction(async (context) => {
236
+ const { tx } = context.state
237
+ const domain = await domainFactory.create({}, tx)
238
+
239
+ // Given
240
+ const { dataSet, resolverRole } = await dataSetFactory.createWithRoles(
241
+ {},
242
+ domain,
243
+ tx
244
+ )
245
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
246
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
247
+ { state: DataOocStatus.REVIEWED, correctiveInstruction: 'Fix temperature' },
248
+ dataSet,
249
+ undefined,
250
+ tx
251
+ )
252
+
253
+ // When
254
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
255
+ {
256
+ name: `[OOC 조치] ${dataSet.name}`,
257
+ state: ActivityInstanceStatus.Issued,
258
+ assigneeRole: resolverRole,
259
+ input: {
260
+ dataOocId: dataOoc.id,
261
+ instruction: dataOoc.correctiveInstruction
262
+ }
263
+ },
264
+ resolveActivity,
265
+ domain,
266
+ tx
267
+ )
268
+
269
+ // Then
270
+ expect(resolveInstance.assigneeRole?.id).toBe(resolverRole.id)
271
+ expect(resolveInstance.input?.instruction).toBe('Fix temperature')
272
+ })
273
+ })
274
+
275
+ it('OOC Resolve 발행 시 outlierApprovalLine이 적용되어야 한다', async () => {
276
+ await withTestTransaction(async (context) => {
277
+ const { tx } = context.state
278
+ const domain = await domainFactory.create({}, tx)
279
+ const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
280
+
281
+ // Given: DataSet with outlierApprovalLine
282
+ const outlierApprovalLine = [
283
+ { type: 'Role', value: approverRole.id, approver: { id: approverRole.id, name: approverRole.name } }
284
+ ]
285
+ const dataSet = await dataSetFactory.createWithDomain(
286
+ { outlierApprovalLine },
287
+ domain,
288
+ tx
289
+ )
290
+ const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx)
291
+
292
+ // When
293
+ const resolveInstance = await activityInstanceFactory.createWithActivity(
294
+ {
295
+ state: ActivityInstanceStatus.Issued,
296
+ approvalLine: dataSet.outlierApprovalLine
297
+ },
298
+ resolveActivity,
299
+ domain,
300
+ tx
301
+ )
302
+
303
+ // Then
304
+ expect(resolveInstance.approvalLine?.length).toBe(1)
305
+ expect(resolveInstance.approvalLine?.[0].type).toBe('Role')
306
+ })
307
+ })
308
+
309
+ it('OOC Resolve 완료 시 DataOoc 상태가 CORRECTED로 전이되어야 한다', async () => {
310
+ await withTestTransaction(async (context) => {
311
+ const { tx, user } = context.state
312
+
313
+ // Given: REVIEWED 상태의 DataOoc
314
+ const dataOoc = await dataOocFactory.create(
315
+ {
316
+ state: DataOocStatus.REVIEWED,
317
+ reviewedAt: new Date(),
318
+ correctiveInstruction: 'Fix it'
319
+ },
320
+ tx
321
+ )
322
+
323
+ // When: Resolve 완료 시뮬레이션
324
+ const corrected = await simulateOocResolveCompletion(
325
+ dataOoc,
326
+ 'Temperature를 정상 범위로 조절함',
327
+ user,
328
+ { tx, domain: context.state.domain, user }
329
+ )
330
+
331
+ // Then
332
+ expect(corrected.state).toBe(DataOocStatus.CORRECTED)
333
+ expect(corrected.correctedAt).toBeDefined()
334
+ expect(corrected.corrector?.id).toBe(user.id)
335
+ expect(corrected.correctiveAction).toBe('Temperature를 정상 범위로 조절함')
336
+ })
337
+ })
338
+ })
339
+
340
+ describe('Complete OOC Workflow End-to-End', () => {
341
+ it('전체 워크플로우: ISSUED -> REVIEWED -> CORRECTED', async () => {
342
+ await withTestTransaction(async (context) => {
343
+ const { tx, domain, user } = context.state
344
+
345
+ // 1. Setup
346
+ const { dataSet, supervisoryRole, resolverRole } = await dataSetFactory.createWithRoles(
347
+ {},
348
+ domain,
349
+ tx
350
+ )
351
+ const reviewer = await userFactory.create({ name: 'Reviewer' }, tx)
352
+ const corrector = await userFactory.create({ name: 'Corrector' }, tx)
353
+
354
+ // 2. OOC 생성 (ISSUED)
355
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
356
+ {
357
+ state: DataOocStatus.ISSUED,
358
+ history: [createHistoryEntry(user, DataOocStatus.ISSUED)]
359
+ },
360
+ dataSet,
361
+ undefined,
362
+ tx
363
+ )
364
+ expect(dataOoc.state).toBe(DataOocStatus.ISSUED)
365
+
366
+ // 3. Review 완료 (REVIEWED)
367
+ const reviewed = await simulateOocReviewCompletion(
368
+ dataOoc,
369
+ 'Temperature 조절 필요',
370
+ reviewer,
371
+ { tx, domain, user: reviewer }
372
+ )
373
+ expect(reviewed.state).toBe(DataOocStatus.REVIEWED)
374
+ expect(reviewed.correctiveInstruction).toBe('Temperature 조절 필요')
375
+
376
+ // 4. Resolve 완료 (CORRECTED)
377
+ const corrected = await simulateOocResolveCompletion(
378
+ reviewed,
379
+ 'Temperature를 정상 범위로 조절함',
380
+ corrector,
381
+ { tx, domain, user: corrector }
382
+ )
383
+ expect(corrected.state).toBe(DataOocStatus.CORRECTED)
384
+ expect(corrected.correctiveAction).toBe('Temperature를 정상 범위로 조절함')
385
+
386
+ // 5. 최종 상태 검증
387
+ expect(corrected.reviewedAt).toBeDefined()
388
+ expect(corrected.correctedAt).toBeDefined()
389
+ expect(corrected.reviewer?.id).toBe(reviewer.id)
390
+ expect(corrected.corrector?.id).toBe(corrector.id)
391
+ expect(corrected.history?.length).toBe(3)
392
+ })
393
+ })
394
+
395
+ it('워크플로우 중 잘못된 상태 전이는 불가능해야 한다', async () => {
396
+ await withTestTransaction(async (context) => {
397
+ const { tx } = context.state
398
+
399
+ // Given: ISSUED 상태의 DataOoc
400
+ const dataOoc = await dataOocFactory.create(
401
+ { state: DataOocStatus.ISSUED },
402
+ tx
403
+ )
404
+
405
+ // Then: ISSUED에서 CORRECTED로 직접 전이는 불가
406
+ expect(isValidOocTransition(DataOocStatus.ISSUED, DataOocStatus.CORRECTED)).toBe(false)
407
+ })
408
+ })
409
+ })
410
+
411
+ describe('Edge Cases', () => {
412
+ it('supervisoryRole이 없으면 Review Activity가 발행되지 않아야 한다', async () => {
413
+ await withTestTransaction(async (context) => {
414
+ const { tx } = context.state
415
+ const domain = await domainFactory.create({}, tx)
416
+
417
+ // Given: supervisoryRole이 없는 DataSet
418
+ const dataSet = await dataSetFactory.createWithDomain(
419
+ { supervisoryRole: undefined },
420
+ domain,
421
+ tx
422
+ )
423
+
424
+ // Then: supervisoryRole이 없음
425
+ expect(dataSet.supervisoryRole).toBeUndefined()
426
+ })
427
+ })
428
+
429
+ it('resolverRole이 없으면 Resolve Activity가 발행되지 않아야 한다', async () => {
430
+ await withTestTransaction(async (context) => {
431
+ const { tx } = context.state
432
+ const domain = await domainFactory.create({}, tx)
433
+
434
+ // Given: resolverRole이 없는 DataSet
435
+ const dataSet = await dataSetFactory.createWithDomain(
436
+ { resolverRole: undefined },
437
+ domain,
438
+ tx
439
+ )
440
+
441
+ // Then: resolverRole이 없음
442
+ expect(dataSet.resolverRole).toBeUndefined()
443
+ })
444
+ })
445
+
446
+ it('CORRECTED 상태의 DataOoc은 더 이상 전이할 수 없다', async () => {
447
+ await withTestTransaction(async (context) => {
448
+ const { tx } = context.state
449
+
450
+ // Given: CORRECTED 상태의 DataOoc
451
+ const dataOoc = await dataOocFactory.create(
452
+ {
453
+ state: DataOocStatus.CORRECTED,
454
+ correctedAt: new Date()
455
+ },
456
+ tx
457
+ )
458
+
459
+ // Then
460
+ expect(dataOoc.state).toBe(DataOocStatus.CORRECTED)
461
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.ISSUED)).toBe(false)
462
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.REVIEWED)).toBe(false)
463
+ })
464
+ })
465
+
466
+ it('DataOoc 생성 시 domain이 설정되어야 한다', async () => {
467
+ await withTestTransaction(async (context) => {
468
+ const { tx, domain } = context.state
469
+
470
+ // Given & When
471
+ const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx)
472
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
473
+ { state: DataOocStatus.ISSUED },
474
+ dataSet,
475
+ undefined,
476
+ tx
477
+ )
478
+
479
+ // Then
480
+ expect(dataOoc.domain?.id).toBe(domain.id)
481
+ })
482
+ })
483
+ })
484
+ })