@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,288 @@
1
+ /**
2
+ * issue-ooc-review Controller Unit Tests
3
+ * OOC Review 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
+ dataSampleFactory,
14
+ dataOocFactory,
15
+ activityFactory,
16
+ activityInstanceFactory
17
+ } from '../../../../../test/factories'
18
+ import { DataOocStatus, ActivityInstanceStatus } from '../../../../../test/entities/schemas'
19
+
20
+ describe('issue-ooc-review Controller', () => {
21
+ let testDb: TestDatabase
22
+
23
+ beforeAll(async () => {
24
+ testDb = TestDatabase.getInstance()
25
+ })
26
+
27
+ describe('OOC Review Activity 발행 조건', () => {
28
+ it('Activity가 존재하고 supervisoryRole이 있으면 Review Instance가 생성되어야 한다', async () => {
29
+ await withTestTransaction(async (context) => {
30
+ const { tx, domain } = context.state
31
+
32
+ // Given: OOC Review Activity와 supervisoryRole이 있는 DataSet
33
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
34
+ const supervisoryRole = await roleFactory.create({ name: 'Supervisor', domain }, tx)
35
+ const dataSet = await dataSetFactory.createWithDomain(
36
+ { supervisoryRole },
37
+ domain,
38
+ tx
39
+ )
40
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
41
+ { state: DataOocStatus.ISSUED },
42
+ dataSet,
43
+ undefined,
44
+ tx
45
+ )
46
+
47
+ // When: issueOocReview 로직 시뮬레이션
48
+ const activityInstance = await activityInstanceFactory.createWithActivity(
49
+ {
50
+ name: `[OOC 검토] ${dataSet.name}`,
51
+ description: dataSet.description,
52
+ state: ActivityInstanceStatus.Issued,
53
+ dueAt: new Date(dataOoc.collectedAt.getTime() + reviewActivity.standardTime * 1000),
54
+ input: { dataOocId: dataOoc.id },
55
+ assigneeRole: supervisoryRole,
56
+ threadsMin: 1,
57
+ threadsMax: 1,
58
+ approvalLine: []
59
+ },
60
+ reviewActivity,
61
+ domain,
62
+ tx
63
+ )
64
+
65
+ // Then
66
+ expect(activityInstance).toBeDefined()
67
+ expect(activityInstance.name).toBe(`[OOC 검토] ${dataSet.name}`)
68
+ expect(activityInstance.input?.dataOocId).toBe(dataOoc.id)
69
+ expect(activityInstance.assigneeRole?.id).toBe(supervisoryRole.id)
70
+ expect(activityInstance.activity?.id).toBe(reviewActivity.id)
71
+ })
72
+ })
73
+
74
+ it('supervisoryRole이 없으면 Review Activity가 발행되지 않아야 한다', async () => {
75
+ await withTestTransaction(async (context) => {
76
+ const { tx, domain } = context.state
77
+
78
+ // Given: supervisoryRole이 없는 DataSet
79
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
80
+ const dataSet = await dataSetFactory.createWithDomain(
81
+ { supervisoryRole: undefined, supervisoryRoleId: undefined },
82
+ domain,
83
+ tx
84
+ )
85
+
86
+ // When & Then: supervisoryRole 체크
87
+ const supervisoryRoleId = dataSet.supervisoryRoleId
88
+ expect(supervisoryRoleId).toBeFalsy()
89
+
90
+ // issueOocReview 로직에서는 이 경우 console.error만 출력하고 리턴
91
+ // Activity Instance가 생성되지 않음
92
+ })
93
+ })
94
+
95
+ it('OOC Review Activity가 설치되지 않으면 Review가 발행되지 않아야 한다', async () => {
96
+ await withTestTransaction(async (context) => {
97
+ const { tx, domain } = context.state
98
+
99
+ // Given: OOC Review Activity가 없는 상태
100
+ const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
101
+
102
+ // When: Activity 조회
103
+ const activity = await tx
104
+ .getRepository('Activity')
105
+ .findOne({ where: { domain: { id: domain.id }, name: 'OOC Review' } })
106
+
107
+ // Then: Activity가 없음
108
+ expect(activity).toBeNull()
109
+ // issueOocReview 로직에서는 이 경우 console.warn만 출력하고 리턴
110
+ })
111
+ })
112
+ })
113
+
114
+ describe('OOC Review Instance 속성', () => {
115
+ it('dueAt은 collectedAt + standardTime으로 계산되어야 한다', async () => {
116
+ await withTestTransaction(async (context) => {
117
+ const { tx, domain } = context.state
118
+
119
+ // Given
120
+ const standardTime = 48 * 60 * 60 // 48시간
121
+ const reviewActivity = await activityFactory.createWithDomain(
122
+ {
123
+ name: 'OOC Review',
124
+ standardTime
125
+ },
126
+ domain,
127
+ tx
128
+ )
129
+ const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
130
+ const collectedAt = new Date('2024-01-15T10:00:00.000Z')
131
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
132
+ { state: DataOocStatus.ISSUED, collectedAt },
133
+ dataSet,
134
+ undefined,
135
+ tx
136
+ )
137
+
138
+ // When: dueAt 계산
139
+ const expectedDueAt = new Date(collectedAt.getTime() + standardTime * 1000)
140
+
141
+ // Then
142
+ expect(expectedDueAt.getTime()).toBe(collectedAt.getTime() + standardTime * 1000)
143
+ })
144
+ })
145
+
146
+ it('input에 dataOocId가 포함되어야 한다', async () => {
147
+ await withTestTransaction(async (context) => {
148
+ const { tx, domain } = context.state
149
+
150
+ // Given
151
+ const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
152
+ const dataOoc = await dataOocFactory.createWithDataSetAndSample(
153
+ { state: DataOocStatus.ISSUED },
154
+ dataSet,
155
+ undefined,
156
+ tx
157
+ )
158
+
159
+ // When: issueOocReview 입력 구성
160
+ const input = { dataOocId: dataOoc.id }
161
+
162
+ // Then
163
+ expect(input.dataOocId).toBe(dataOoc.id)
164
+ })
165
+ })
166
+
167
+ it('threadsMin과 threadsMax가 1로 설정되어야 한다 (single 담당자)', async () => {
168
+ await withTestTransaction(async (context) => {
169
+ const { tx, domain } = context.state
170
+
171
+ // Given
172
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
173
+ const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
174
+
175
+ // When: ActivityInstance 생성
176
+ const activityInstance = await activityInstanceFactory.createWithActivity(
177
+ {
178
+ threadsMin: 1,
179
+ threadsMax: 1,
180
+ assigneeRole: supervisoryRole
181
+ },
182
+ reviewActivity,
183
+ domain,
184
+ tx
185
+ )
186
+
187
+ // Then
188
+ expect(activityInstance.threadsMin).toBe(1)
189
+ expect(activityInstance.threadsMax).toBe(1)
190
+ })
191
+ })
192
+
193
+ it('approvalLine은 빈 배열이어야 한다 (Review는 결재 없음)', async () => {
194
+ await withTestTransaction(async (context) => {
195
+ const { tx, domain } = context.state
196
+
197
+ // Given
198
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
199
+ const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
200
+
201
+ // When: ActivityInstance 생성 (issueOocReview 방식)
202
+ const activityInstance = await activityInstanceFactory.createWithActivity(
203
+ {
204
+ approvalLine: [],
205
+ assigneeRole: supervisoryRole
206
+ },
207
+ reviewActivity,
208
+ domain,
209
+ tx
210
+ )
211
+
212
+ // Then
213
+ expect(activityInstance.approvalLine).toEqual([])
214
+ })
215
+ })
216
+ })
217
+
218
+ describe('DataOoc과 ReviewActivityInstance 연결', () => {
219
+ it('DataOoc의 reviewActivityInstance 필드에 생성된 Instance가 저장되어야 한다', async () => {
220
+ await withTestTransaction(async (context) => {
221
+ const { tx, domain } = context.state
222
+
223
+ // Given
224
+ const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
225
+ const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
226
+ let dataOoc = await dataOocFactory.createWithDataSetAndSample(
227
+ { state: DataOocStatus.ISSUED },
228
+ dataSet,
229
+ undefined,
230
+ tx
231
+ )
232
+
233
+ // When: Review Instance 생성 및 DataOoc에 연결
234
+ const reviewInstance = await activityInstanceFactory.createWithActivity(
235
+ {
236
+ name: `[OOC 검토] ${dataSet.name}`,
237
+ state: ActivityInstanceStatus.Issued,
238
+ input: { dataOocId: dataOoc.id },
239
+ assigneeRole: supervisoryRole
240
+ },
241
+ reviewActivity,
242
+ domain,
243
+ tx
244
+ )
245
+
246
+ dataOoc.reviewActivityInstance = reviewInstance
247
+ dataOoc = await tx.save('DataOoc', dataOoc)
248
+
249
+ // Then
250
+ expect(dataOoc.reviewActivityInstance).toBeDefined()
251
+ expect(dataOoc.reviewActivityInstance?.id).toBe(reviewInstance.id)
252
+ })
253
+ })
254
+ })
255
+
256
+ describe('Parent Domain 지원', () => {
257
+ it('domain.parentId가 있으면 parent domain의 Activity도 조회해야 한다', async () => {
258
+ await withTestTransaction(async (context) => {
259
+ const { tx } = context.state
260
+
261
+ // Given: Parent Domain과 Child Domain 구조
262
+ const parentDomain = await domainFactory.create({ name: 'Parent Domain' }, tx)
263
+ const childDomain = await domainFactory.create(
264
+ {
265
+ name: 'Child Domain',
266
+ parentId: parentDomain.id
267
+ },
268
+ tx
269
+ )
270
+
271
+ // Parent Domain에 Activity 생성
272
+ const reviewActivity = await activityFactory.createOocReviewActivity(parentDomain, tx)
273
+
274
+ // When: Child Domain에서 Activity 조회 (In 연산자 사용)
275
+ const activities = await tx.getRepository('Activity').find({
276
+ where: [
277
+ { domain: { id: childDomain.id }, name: 'OOC Review' },
278
+ { domain: { id: parentDomain.id }, name: 'OOC Review' }
279
+ ]
280
+ })
281
+
282
+ // Then: Parent의 Activity가 조회됨
283
+ expect(activities.length).toBeGreaterThan(0)
284
+ expect(activities[0].id).toBe(reviewActivity.id)
285
+ })
286
+ })
287
+ })
288
+ })
@@ -0,0 +1,150 @@
1
+ /**
2
+ * DataUseCase Unit Tests
3
+ * 데이터 평가 로직 단위 테스트
4
+ */
5
+
6
+ // 단위 테스트에서는 dist 파일에서 직접 import하여 패키지 의존성 우회
7
+ import { DataUseCase } from '../../dist-server/controllers/data-use-case'
8
+
9
+ // 테스트용 타입 정의 (실제 타입과 호환)
10
+ interface DataItem {
11
+ name: string
12
+ tag?: string
13
+ active: boolean
14
+ type: string
15
+ spec?: Record<string, any>
16
+ }
17
+
18
+ interface DataSet {
19
+ useCase: string | null
20
+ }
21
+
22
+ describe('DataUseCase', () => {
23
+ describe('evaluate', () => {
24
+ it('useCase가 없으면 ooc/oos 모두 false를 반환해야 한다', () => {
25
+ // Given
26
+ const dataSet = { useCase: null } as DataSet
27
+ const dataItems: DataItem[] = []
28
+ const data = {}
29
+
30
+ // When
31
+ const result = DataUseCase.evaluate(dataSet, dataItems, data)
32
+
33
+ // Then
34
+ expect(result.ooc).toBe(false)
35
+ expect(result.oos).toBe(false)
36
+ })
37
+
38
+ it('active가 false인 dataItem은 평가하지 않아야 한다', () => {
39
+ // Given
40
+ const dataSet = { useCase: 'QA' } as DataSet
41
+ const dataItems: DataItem[] = [
42
+ {
43
+ name: 'Temperature',
44
+ tag: 'temperature',
45
+ active: false,
46
+ type: 'number' as any,
47
+ spec: {
48
+ QA: { ucl: 80, lcl: 20 }
49
+ }
50
+ }
51
+ ]
52
+ const data = { temperature: 100 } // UCL 초과하지만 inactive
53
+
54
+ // When
55
+ const result = DataUseCase.evaluate(dataSet, dataItems, data)
56
+
57
+ // Then
58
+ expect(result.ooc).toBe(false)
59
+ expect(result.judgment).toEqual({})
60
+ })
61
+
62
+ it('tag가 없는 dataItem은 평가하지 않아야 한다', () => {
63
+ // Given
64
+ const dataSet = { useCase: 'QA' } as DataSet
65
+ const dataItems: DataItem[] = [
66
+ {
67
+ name: 'Temperature',
68
+ tag: undefined,
69
+ active: true,
70
+ type: 'number' as any,
71
+ spec: {
72
+ QA: { ucl: 80, lcl: 20 }
73
+ }
74
+ }
75
+ ]
76
+ const data = { temperature: 100 }
77
+
78
+ // When
79
+ const result = DataUseCase.evaluate(dataSet, dataItems, data)
80
+
81
+ // Then
82
+ expect(result.ooc).toBe(false)
83
+ })
84
+
85
+ it('값이 null이면 해당 항목을 건너뛰어야 한다', () => {
86
+ // Given
87
+ const dataSet = { useCase: 'QA' } as DataSet
88
+ const dataItems: DataItem[] = [
89
+ {
90
+ name: 'Temperature',
91
+ tag: 'temperature',
92
+ active: true,
93
+ type: 'number' as any,
94
+ spec: {
95
+ QA: { ucl: 80, lcl: 20 }
96
+ }
97
+ }
98
+ ]
99
+ const data = { temperature: null }
100
+
101
+ // When
102
+ const result = DataUseCase.evaluate(dataSet, dataItems, data)
103
+
104
+ // Then
105
+ expect(result.ooc).toBe(false)
106
+ expect(result.judgment).toEqual({})
107
+ })
108
+
109
+ it('배열 형태의 값도 처리할 수 있어야 한다', () => {
110
+ // Given
111
+ const dataSet = { useCase: 'QA' } as DataSet
112
+ const dataItems: DataItem[] = [
113
+ {
114
+ name: 'Temperature',
115
+ tag: 'temperature',
116
+ active: true,
117
+ type: 'number' as any,
118
+ spec: {
119
+ QA: { ucl: 80, lcl: 20 }
120
+ }
121
+ }
122
+ ]
123
+ const data = { temperature: [50, 60, 70] } // 배열 형태
124
+
125
+ // When
126
+ const result = DataUseCase.evaluate(dataSet, dataItems, data)
127
+
128
+ // Then: 에러 없이 처리되어야 함
129
+ expect(result).toBeDefined()
130
+ })
131
+ })
132
+
133
+ describe('registry', () => {
134
+ it('등록된 useCase 이름 목록을 반환해야 한다', () => {
135
+ // When
136
+ const names = DataUseCase.getUseCaseNames()
137
+
138
+ // Then
139
+ expect(Array.isArray(names)).toBe(true)
140
+ })
141
+
142
+ it('존재하지 않는 useCase는 undefined를 반환해야 한다', () => {
143
+ // When
144
+ const useCase = DataUseCase.getUseCase('non-existent-use-case')
145
+
146
+ // Then
147
+ expect(useCase).toBeUndefined()
148
+ })
149
+ })
150
+ })
@@ -0,0 +1,233 @@
1
+ /**
2
+ * OOC State Transition Unit Tests
3
+ * DataOoc 상태 전이 로직 단위 테스트
4
+ */
5
+
6
+ import {
7
+ isValidOocState,
8
+ isValidOocTransition
9
+ } from '../../../../test/helpers/workflow-helpers'
10
+ import { DataOocStatus } from '../../../../test/entities/schemas'
11
+
12
+ describe('OOC State Transition Logic', () => {
13
+ describe('isValidOocState', () => {
14
+ it('ISSUED 상태는 유효해야 한다', () => {
15
+ const dataOoc = { state: DataOocStatus.ISSUED } as any
16
+ expect(isValidOocState(dataOoc)).toBe(true)
17
+ })
18
+
19
+ it('REVIEWED 상태는 유효해야 한다', () => {
20
+ const dataOoc = { state: DataOocStatus.REVIEWED } as any
21
+ expect(isValidOocState(dataOoc)).toBe(true)
22
+ })
23
+
24
+ it('CORRECTED 상태는 유효해야 한다', () => {
25
+ const dataOoc = { state: DataOocStatus.CORRECTED } as any
26
+ expect(isValidOocState(dataOoc)).toBe(true)
27
+ })
28
+
29
+ it('REJECTED 상태는 유효해야 한다', () => {
30
+ const dataOoc = { state: DataOocStatus.REJECTED } as any
31
+ expect(isValidOocState(dataOoc)).toBe(true)
32
+ })
33
+
34
+ it('유효하지 않은 상태는 false를 반환해야 한다', () => {
35
+ const dataOoc = { state: 'INVALID_STATE' } as any
36
+ expect(isValidOocState(dataOoc)).toBe(false)
37
+ })
38
+ })
39
+
40
+ describe('isValidOocTransition', () => {
41
+ describe('유효한 상태 전이', () => {
42
+ it('ISSUED -> REVIEWED 전이는 유효해야 한다', () => {
43
+ expect(isValidOocTransition(DataOocStatus.ISSUED, DataOocStatus.REVIEWED)).toBe(true)
44
+ })
45
+
46
+ it('REVIEWED -> CORRECTED 전이는 유효해야 한다', () => {
47
+ expect(isValidOocTransition(DataOocStatus.REVIEWED, DataOocStatus.CORRECTED)).toBe(true)
48
+ })
49
+ })
50
+
51
+ describe('유효하지 않은 상태 전이', () => {
52
+ it('ISSUED -> CORRECTED 직접 전이는 불가능해야 한다', () => {
53
+ expect(isValidOocTransition(DataOocStatus.ISSUED, DataOocStatus.CORRECTED)).toBe(false)
54
+ })
55
+
56
+ it('REVIEWED -> ISSUED 역방향 전이는 불가능해야 한다', () => {
57
+ expect(isValidOocTransition(DataOocStatus.REVIEWED, DataOocStatus.ISSUED)).toBe(false)
58
+ })
59
+
60
+ it('CORRECTED -> REVIEWED 역방향 전이는 불가능해야 한다', () => {
61
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.REVIEWED)).toBe(false)
62
+ })
63
+
64
+ it('CORRECTED -> ISSUED 역방향 전이는 불가능해야 한다', () => {
65
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.ISSUED)).toBe(false)
66
+ })
67
+
68
+ it('CORRECTED는 종료 상태이므로 더 이상 전이할 수 없다', () => {
69
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.CORRECTED)).toBe(false)
70
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.REVIEWED)).toBe(false)
71
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.ISSUED)).toBe(false)
72
+ })
73
+
74
+ it('REJECTED는 종료 상태이므로 더 이상 전이할 수 없다', () => {
75
+ expect(isValidOocTransition(DataOocStatus.REJECTED, DataOocStatus.ISSUED)).toBe(false)
76
+ expect(isValidOocTransition(DataOocStatus.REJECTED, DataOocStatus.REVIEWED)).toBe(false)
77
+ expect(isValidOocTransition(DataOocStatus.REJECTED, DataOocStatus.CORRECTED)).toBe(false)
78
+ })
79
+
80
+ it('같은 상태로의 전이는 불가능해야 한다', () => {
81
+ expect(isValidOocTransition(DataOocStatus.ISSUED, DataOocStatus.ISSUED)).toBe(false)
82
+ expect(isValidOocTransition(DataOocStatus.REVIEWED, DataOocStatus.REVIEWED)).toBe(false)
83
+ })
84
+ })
85
+ })
86
+
87
+ describe('DataOocStatus Enum', () => {
88
+ it('모든 상태 값이 정의되어야 한다', () => {
89
+ expect(DataOocStatus.ISSUED).toBe('ISSUED')
90
+ expect(DataOocStatus.REVIEWED).toBe('REVIEWED')
91
+ expect(DataOocStatus.CORRECTED).toBe('CORRECTED')
92
+ expect(DataOocStatus.REJECTED).toBe('REJECTED')
93
+ })
94
+
95
+ it('총 4개의 상태가 있어야 한다', () => {
96
+ const statusValues = Object.values(DataOocStatus)
97
+ expect(statusValues.length).toBe(4)
98
+ })
99
+ })
100
+ })
101
+
102
+ describe('OOC State Rules', () => {
103
+ describe('ISSUED 상태 규칙', () => {
104
+ it('OOC 생성 시 초기 상태는 ISSUED여야 한다', () => {
105
+ // DataOoc이 생성될 때 기본 state는 ISSUED
106
+ expect(DataOocStatus.ISSUED).toBe('ISSUED')
107
+ })
108
+
109
+ it('ISSUED 상태에서는 reviewedAt이 설정되지 않아야 한다', () => {
110
+ const dataOoc = {
111
+ state: DataOocStatus.ISSUED,
112
+ reviewedAt: null,
113
+ correctedAt: null
114
+ }
115
+ expect(dataOoc.reviewedAt).toBeNull()
116
+ expect(dataOoc.correctedAt).toBeNull()
117
+ })
118
+ })
119
+
120
+ describe('REVIEWED 상태 규칙', () => {
121
+ it('REVIEWED로 전이 시 reviewedAt이 설정되어야 한다', () => {
122
+ const now = new Date()
123
+ const dataOoc = {
124
+ state: DataOocStatus.REVIEWED,
125
+ reviewedAt: now,
126
+ correctedAt: null
127
+ }
128
+ expect(dataOoc.reviewedAt).toEqual(now)
129
+ expect(dataOoc.correctedAt).toBeNull()
130
+ })
131
+
132
+ it('REVIEWED로 전이 시 reviewer가 설정되어야 한다', () => {
133
+ const reviewer = { id: 'user-1', name: 'Reviewer' }
134
+ const dataOoc = {
135
+ state: DataOocStatus.REVIEWED,
136
+ reviewer
137
+ }
138
+ expect(dataOoc.reviewer).toBeDefined()
139
+ expect(dataOoc.reviewer.id).toBe('user-1')
140
+ })
141
+
142
+ it('REVIEWED로 전이 시 correctiveInstruction이 설정될 수 있다', () => {
143
+ const dataOoc = {
144
+ state: DataOocStatus.REVIEWED,
145
+ correctiveInstruction: 'Temperature를 조절하세요'
146
+ }
147
+ expect(dataOoc.correctiveInstruction).toBe('Temperature를 조절하세요')
148
+ })
149
+ })
150
+
151
+ describe('CORRECTED 상태 규칙', () => {
152
+ it('CORRECTED로 전이 시 correctedAt이 설정되어야 한다', () => {
153
+ const now = new Date()
154
+ const dataOoc = {
155
+ state: DataOocStatus.CORRECTED,
156
+ reviewedAt: new Date(Date.now() - 3600000), // 1시간 전
157
+ correctedAt: now
158
+ }
159
+ expect(dataOoc.correctedAt).toEqual(now)
160
+ })
161
+
162
+ it('CORRECTED로 전이 시 corrector가 설정되어야 한다', () => {
163
+ const corrector = { id: 'user-2', name: 'Corrector' }
164
+ const dataOoc = {
165
+ state: DataOocStatus.CORRECTED,
166
+ corrector
167
+ }
168
+ expect(dataOoc.corrector).toBeDefined()
169
+ expect(dataOoc.corrector.id).toBe('user-2')
170
+ })
171
+
172
+ it('CORRECTED로 전이 시 correctiveAction이 설정되어야 한다', () => {
173
+ const dataOoc = {
174
+ state: DataOocStatus.CORRECTED,
175
+ correctiveAction: 'Temperature를 정상 범위로 조절함'
176
+ }
177
+ expect(dataOoc.correctiveAction).toBe('Temperature를 정상 범위로 조절함')
178
+ })
179
+
180
+ it('CORRECTED는 종료 상태이다', () => {
181
+ // 더 이상 전이 불가
182
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.ISSUED)).toBe(false)
183
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.REVIEWED)).toBe(false)
184
+ expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.CORRECTED)).toBe(false)
185
+ })
186
+ })
187
+
188
+ describe('History 추적 규칙', () => {
189
+ it('상태 전이 시 history에 기록이 추가되어야 한다', () => {
190
+ const history = [
191
+ {
192
+ user: { id: 'user-1', name: 'Creator' },
193
+ state: DataOocStatus.ISSUED,
194
+ timestamp: '2024-01-01T00:00:00.000Z'
195
+ },
196
+ {
197
+ user: { id: 'user-2', name: 'Reviewer' },
198
+ state: DataOocStatus.REVIEWED,
199
+ timestamp: '2024-01-01T01:00:00.000Z'
200
+ }
201
+ ]
202
+
203
+ expect(history.length).toBe(2)
204
+ expect(history[0].state).toBe(DataOocStatus.ISSUED)
205
+ expect(history[1].state).toBe(DataOocStatus.REVIEWED)
206
+ })
207
+
208
+ it('history 엔트리에는 user, state, timestamp가 있어야 한다', () => {
209
+ const historyEntry = {
210
+ user: { id: 'user-1', name: 'User' },
211
+ state: DataOocStatus.ISSUED,
212
+ timestamp: new Date().toISOString()
213
+ }
214
+
215
+ expect(historyEntry.user).toBeDefined()
216
+ expect(historyEntry.user.id).toBeDefined()
217
+ expect(historyEntry.user.name).toBeDefined()
218
+ expect(historyEntry.state).toBeDefined()
219
+ expect(historyEntry.timestamp).toBeDefined()
220
+ })
221
+
222
+ it('CORRECTED 전이 시 history에 comment(correctiveAction)가 포함될 수 있다', () => {
223
+ const historyEntry = {
224
+ user: { id: 'user-1', name: 'Corrector' },
225
+ state: DataOocStatus.CORRECTED,
226
+ comment: 'Temperature를 정상 범위로 조절함',
227
+ timestamp: new Date().toISOString()
228
+ }
229
+
230
+ expect(historyEntry.comment).toBe('Temperature를 정상 범위로 조절함')
231
+ })
232
+ })
233
+ })