/** * OOC Lifecycle Integration Tests * DataOoc 전체 생명주기 통합 테스트 */ import { TestDatabase } from '../../../../test/test-database' import { withTestTransaction } from '../../../../test/test-context' import { domainFactory, userFactory, roleFactory, dataSetFactory, dataSampleFactory, dataOocFactory, activityFactory, activityInstanceFactory, activityThreadFactory } from '../../../../test/factories' import { DataOocStatus, ActivityInstanceStatus, ActivityThreadStatus } from '../../../../test/entities/schemas' import { simulateOocReviewCompletion, simulateOocResolveCompletion, createHistoryEntry, isValidOocTransition } from '../../../../test/helpers/workflow-helpers' describe('OOC Complete Lifecycle Integration Tests', () => { let testDb: TestDatabase beforeAll(async () => { testDb = TestDatabase.getInstance() }) describe('OOC Creation from DataSample', () => { it('DataSample에서 ooc=true일 때 DataOoc이 생성되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state // Given: OOC 조건의 DataSample const { dataSet } = await dataSetFactory.createWithRoles({}, undefined, tx) const dataSample = await dataSampleFactory.createOocSample({}, dataSet, tx) // When: DataOoc 생성 const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED, data: dataSample.data, ooc: true, oos: false }, dataSet, dataSample, tx ) // Then: OOC이 생성되어야 함 expect(dataOoc).toBeDefined() expect(dataOoc.id).toBeDefined() expect(dataOoc.ooc).toBe(true) expect(dataOoc.state).toBe(DataOocStatus.ISSUED) expect(dataOoc.dataSet?.id).toBe(dataSet.id) expect(dataOoc.dataSample?.id).toBe(dataSample.id) }) }) it('DataOoc 생성 시 초기 상태는 ISSUED여야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state // Given & When: DataOoc 생성 const dataOoc = await dataOocFactory.create( { state: DataOocStatus.ISSUED }, tx ) // Then expect(dataOoc.state).toBe(DataOocStatus.ISSUED) }) }) it('DataOoc 생성 시 history 배열이 초기화되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, user } = context.state // Given & When const dataOoc = await dataOocFactory.create( { state: DataOocStatus.ISSUED, history: [createHistoryEntry(user, DataOocStatus.ISSUED)] }, tx ) // Then expect(dataOoc.history).toBeDefined() expect(dataOoc.history?.length).toBe(1) expect(dataOoc.history?.[0].state).toBe(DataOocStatus.ISSUED) }) }) it('DataOoc의 data, judgment 필드가 DataSample에서 복사되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state // Given const { dataSet } = await dataSetFactory.createWithRoles({}, undefined, tx) const sampleData = { temperature: 95, humidity: 50 } const sampleJudgment = { temperature: { ooc: true, oos: false }, humidity: { ooc: false, oos: false } } // When const dataOoc = await dataOocFactory.createWithDataSetAndSample( { data: sampleData, judgment: sampleJudgment, ooc: true }, dataSet, undefined, tx ) // Then expect(dataOoc.data).toEqual(sampleData) expect(dataOoc.judgment).toEqual(sampleJudgment) }) }) }) describe('OOC Review Activity Workflow', () => { it('OOC Review 발행 시 supervisoryRole로 할당되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state const domain = await domainFactory.create({}, tx) // Given: DataSet with supervisoryRole const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles( {}, domain, tx ) // When: OOC Review Activity 발행 시뮬레이션 const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED }, dataSet, undefined, tx ) const reviewInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 검토] ${dataSet.name}`, state: ActivityInstanceStatus.Issued, assigneeRole: supervisoryRole, input: { dataOocId: dataOoc.id } }, reviewActivity, domain, tx ) // Then expect(reviewInstance.assigneeRole?.id).toBe(supervisoryRole.id) expect(reviewInstance.input?.dataOocId).toBe(dataOoc.id) }) }) it('OOC Review 완료 시 DataOoc 상태가 REVIEWED로 전이되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, user } = context.state // Given const dataOoc = await dataOocFactory.create( { state: DataOocStatus.ISSUED }, tx ) // When: Review 완료 시뮬레이션 const reviewed = await simulateOocReviewCompletion( dataOoc, 'Temperature 조절 필요', user, { tx, domain: context.state.domain, user } ) // Then expect(reviewed.state).toBe(DataOocStatus.REVIEWED) expect(reviewed.reviewedAt).toBeDefined() expect(reviewed.reviewer?.id).toBe(user.id) expect(reviewed.correctiveInstruction).toBe('Temperature 조절 필요') }) }) it('OOC Review 완료 시 history에 기록이 추가되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, user } = context.state // Given const initialHistory = [createHistoryEntry(user, DataOocStatus.ISSUED)] const dataOoc = await dataOocFactory.create( { state: DataOocStatus.ISSUED, history: initialHistory }, tx ) // When const reviewed = await simulateOocReviewCompletion( dataOoc, 'Instruction', user, { tx, domain: context.state.domain, user } ) // Then expect(reviewed.history?.length).toBe(2) expect(reviewed.history?.[1].state).toBe(DataOocStatus.REVIEWED) }) }) }) describe('OOC Resolve Activity Workflow', () => { it('OOC Resolve 발행 시 resolverRole로 할당되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state const domain = await domainFactory.create({}, tx) // Given const { dataSet, resolverRole } = await dataSetFactory.createWithRoles( {}, domain, tx ) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.REVIEWED, correctiveInstruction: 'Fix temperature' }, dataSet, undefined, tx ) // When const resolveInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 조치] ${dataSet.name}`, state: ActivityInstanceStatus.Issued, assigneeRole: resolverRole, input: { dataOocId: dataOoc.id, instruction: dataOoc.correctiveInstruction } }, resolveActivity, domain, tx ) // Then expect(resolveInstance.assigneeRole?.id).toBe(resolverRole.id) expect(resolveInstance.input?.instruction).toBe('Fix temperature') }) }) it('OOC Resolve 발행 시 outlierApprovalLine이 적용되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state const domain = await domainFactory.create({}, tx) const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx) // Given: DataSet with outlierApprovalLine const outlierApprovalLine = [ { type: 'Role', value: approverRole.id, approver: { id: approverRole.id, name: approverRole.name } } ] const dataSet = await dataSetFactory.createWithDomain( { outlierApprovalLine }, domain, tx ) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) // When const resolveInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Issued, approvalLine: dataSet.outlierApprovalLine }, resolveActivity, domain, tx ) // Then expect(resolveInstance.approvalLine?.length).toBe(1) expect(resolveInstance.approvalLine?.[0].type).toBe('Role') }) }) it('OOC Resolve 완료 시 DataOoc 상태가 CORRECTED로 전이되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, user } = context.state // Given: REVIEWED 상태의 DataOoc const dataOoc = await dataOocFactory.create( { state: DataOocStatus.REVIEWED, reviewedAt: new Date(), correctiveInstruction: 'Fix it' }, tx ) // When: Resolve 완료 시뮬레이션 const corrected = await simulateOocResolveCompletion( dataOoc, 'Temperature를 정상 범위로 조절함', user, { tx, domain: context.state.domain, user } ) // Then expect(corrected.state).toBe(DataOocStatus.CORRECTED) expect(corrected.correctedAt).toBeDefined() expect(corrected.corrector?.id).toBe(user.id) expect(corrected.correctiveAction).toBe('Temperature를 정상 범위로 조절함') }) }) }) describe('Complete OOC Workflow End-to-End', () => { it('전체 워크플로우: ISSUED -> REVIEWED -> CORRECTED', async () => { await withTestTransaction(async (context) => { const { tx, domain, user } = context.state // 1. Setup const { dataSet, supervisoryRole, resolverRole } = await dataSetFactory.createWithRoles( {}, domain, tx ) const reviewer = await userFactory.create({ name: 'Reviewer' }, tx) const corrector = await userFactory.create({ name: 'Corrector' }, tx) // 2. OOC 생성 (ISSUED) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED, history: [createHistoryEntry(user, DataOocStatus.ISSUED)] }, dataSet, undefined, tx ) expect(dataOoc.state).toBe(DataOocStatus.ISSUED) // 3. Review 완료 (REVIEWED) const reviewed = await simulateOocReviewCompletion( dataOoc, 'Temperature 조절 필요', reviewer, { tx, domain, user: reviewer } ) expect(reviewed.state).toBe(DataOocStatus.REVIEWED) expect(reviewed.correctiveInstruction).toBe('Temperature 조절 필요') // 4. Resolve 완료 (CORRECTED) const corrected = await simulateOocResolveCompletion( reviewed, 'Temperature를 정상 범위로 조절함', corrector, { tx, domain, user: corrector } ) expect(corrected.state).toBe(DataOocStatus.CORRECTED) expect(corrected.correctiveAction).toBe('Temperature를 정상 범위로 조절함') // 5. 최종 상태 검증 expect(corrected.reviewedAt).toBeDefined() expect(corrected.correctedAt).toBeDefined() expect(corrected.reviewer?.id).toBe(reviewer.id) expect(corrected.corrector?.id).toBe(corrector.id) expect(corrected.history?.length).toBe(3) }) }) it('워크플로우 중 잘못된 상태 전이는 불가능해야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state // Given: ISSUED 상태의 DataOoc const dataOoc = await dataOocFactory.create( { state: DataOocStatus.ISSUED }, tx ) // Then: ISSUED에서 CORRECTED로 직접 전이는 불가 expect(isValidOocTransition(DataOocStatus.ISSUED, DataOocStatus.CORRECTED)).toBe(false) }) }) }) describe('Edge Cases', () => { it('supervisoryRole이 없으면 Review Activity가 발행되지 않아야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state const domain = await domainFactory.create({}, tx) // Given: supervisoryRole이 없는 DataSet const dataSet = await dataSetFactory.createWithDomain( { supervisoryRole: undefined }, domain, tx ) // Then: supervisoryRole이 없음 expect(dataSet.supervisoryRole).toBeUndefined() }) }) it('resolverRole이 없으면 Resolve Activity가 발행되지 않아야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state const domain = await domainFactory.create({}, tx) // Given: resolverRole이 없는 DataSet const dataSet = await dataSetFactory.createWithDomain( { resolverRole: undefined }, domain, tx ) // Then: resolverRole이 없음 expect(dataSet.resolverRole).toBeUndefined() }) }) it('CORRECTED 상태의 DataOoc은 더 이상 전이할 수 없다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state // Given: CORRECTED 상태의 DataOoc const dataOoc = await dataOocFactory.create( { state: DataOocStatus.CORRECTED, correctedAt: new Date() }, tx ) // Then expect(dataOoc.state).toBe(DataOocStatus.CORRECTED) expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.ISSUED)).toBe(false) expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.REVIEWED)).toBe(false) }) }) it('DataOoc 생성 시 domain이 설정되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given & When const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED }, dataSet, undefined, tx ) // Then expect(dataOoc.domain?.id).toBe(domain.id) }) }) }) })