/** * OOC Workflow Integration Tests * OOC(Out Of Control) 처리 워크플로우 통합 테스트 */ import { TestDatabase } from '../../../../test/test-database' import { createTestContext, 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' describe('OOC Workflow Integration Tests', () => { let testDb: TestDatabase beforeAll(async () => { testDb = TestDatabase.getInstance() }) describe('OOC 생성 및 상태 관리', () => { it('DataOoc이 ISSUED 상태로 생성되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state // Given: 데이터셋 생성 const { dataSet, domain } = await dataSetFactory.createWithRoles({}, undefined, tx) // When: OOC 생성 const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED }, dataSet, undefined, tx ) // Then: ISSUED 상태 확인 expect(dataOoc.state).toBe(DataOocStatus.ISSUED) expect(dataOoc.dataSet?.id).toBe(dataSet.id) }) }) it('DataOoc 상태가 ISSUED → REVIEWED → CORRECTED로 전이되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state // Given: OOC 생성 const dataOoc = await dataOocFactory.create({ state: DataOocStatus.ISSUED }, tx) // When: REVIEWED 상태로 변경 dataOoc.state = DataOocStatus.REVIEWED dataOoc.reviewedAt = new Date() const reviewed = await tx.getRepository('DataOoc').save(dataOoc) // Then: REVIEWED 상태 확인 expect(reviewed.state).toBe(DataOocStatus.REVIEWED) expect(reviewed.reviewedAt).toBeDefined() // When: CORRECTED 상태로 변경 reviewed.state = DataOocStatus.CORRECTED reviewed.correctedAt = new Date() const corrected = await tx.getRepository('DataOoc').save(reviewed) // Then: CORRECTED 상태 확인 expect(corrected.state).toBe(DataOocStatus.CORRECTED) expect(corrected.correctedAt).toBeDefined() }) }) }) describe('Activity Instance 생명주기', () => { it('ActivityInstance가 Issued 상태로 생성되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state const domain = await domainFactory.create({}, tx) // Given: OOC Review Activity 생성 const activity = await activityFactory.createOocReviewActivity(domain, tx) // When: ActivityInstance 생성 const instance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Issued }, activity, domain, tx ) // Then expect(instance.state).toBe(ActivityInstanceStatus.Issued) expect(instance.activity?.id).toBe(activity.id) }) }) it('ActivityThread가 할당되면 Instance 상태가 Assigned로 변경되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state const domain = await domainFactory.create({}, tx) // Given: Instance 생성 const instance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Issued }, undefined, domain, tx ) // When: Thread 할당 const user = await userFactory.create({}, tx) const thread = await activityThreadFactory.createWithInstanceAndAssignee( { state: ActivityThreadStatus.Assigned }, instance, user, tx ) // Then expect(thread.state).toBe(ActivityThreadStatus.Assigned) expect(thread.assignee?.id).toBe(user.id) }) }) }) describe('결재라인 처리', () => { it('결재라인이 없는 경우 submit 시 바로 Ended 상태가 되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state const domain = await domainFactory.create({}, tx) // Given: 결재라인 없는 Instance const instance = await activityInstanceFactory.createWithApprovalLine( [], // 빈 결재라인 { state: ActivityInstanceStatus.Started }, undefined, domain, tx ) const user = await userFactory.create({}, tx) const thread = await activityThreadFactory.createWithInstanceAndAssignee( { state: ActivityThreadStatus.Started }, instance, user, tx ) // When: Thread 완료 시뮬레이션 thread.state = ActivityThreadStatus.Ended thread.terminatedAt = new Date() const endedThread = await tx.getRepository('ActivityThread').save(thread) // Then expect(endedThread.state).toBe(ActivityThreadStatus.Ended) }) }) it('ROLE 타입 결재라인이 올바르게 저장되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx } = context.state const domain = await domainFactory.create({}, tx) const approverRole = await roleFactory.create({ name: 'Approver Role', domain }, tx) // Given: ROLE 타입 결재라인 const approvalLine = [ { type: 'Role', approver: { id: approverRole.id, name: approverRole.name } // value는 저장되지 않음 (클라이언트 동작과 일치) } ] // When: Instance 생성 const instance = await activityInstanceFactory.createWithApprovalLine( approvalLine, {}, undefined, domain, tx ) // Then: 결재라인 확인 expect(instance.approvalLine).toBeDefined() expect(instance.approvalLine?.length).toBe(1) expect(instance.approvalLine?.[0].type).toBe('Role') expect(instance.approvalLine?.[0].approver?.id).toBe(approverRole.id) }) }) }) describe('OOC Review → Resolve 워크플로우', () => { it('전체 OOC 워크플로우가 정상 동작해야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain, user } = context.state // 1. DataSet with roles 생성 const { dataSet, supervisoryRole, resolverRole } = await dataSetFactory.createWithRoles( {}, domain, tx ) // 2. OOC Activities 생성 const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) // 3. DataOoc 생성 (ISSUED 상태) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED }, dataSet, undefined, tx ) expect(dataOoc.state).toBe(DataOocStatus.ISSUED) // 4. OOC Review Instance 생성 시뮬레이션 const reviewInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 검토] ${dataSet.name}`, state: ActivityInstanceStatus.Issued, assigneeRole: supervisoryRole, input: { dataOocId: dataOoc.id } }, reviewActivity, domain, tx ) expect(reviewInstance.input?.dataOocId).toBe(dataOoc.id) // 5. Review 완료 시뮬레이션 → DataOoc REVIEWED dataOoc.state = DataOocStatus.REVIEWED dataOoc.reviewedAt = new Date() dataOoc.correctiveInstruction = 'Temperature 조절 필요' await tx.getRepository('DataOoc').save(dataOoc) // 6. OOC Resolve Instance 생성 시뮬레이션 const resolveInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 조치] ${dataSet.name}`, state: ActivityInstanceStatus.Issued, assigneeRole: resolverRole, input: { dataOocId: dataOoc.id, instruction: dataOoc.correctiveInstruction }, approvalLine: dataSet.outlierApprovalLine || [] }, resolveActivity, domain, tx ) expect(resolveInstance.input?.instruction).toBe('Temperature 조절 필요') // 7. Resolve 완료 시뮬레이션 → DataOoc CORRECTED dataOoc.state = DataOocStatus.CORRECTED dataOoc.correctedAt = new Date() dataOoc.correctiveAction = 'Temperature를 정상 범위로 조절함' const finalOoc = await tx.getRepository('DataOoc').save(dataOoc) // Then: 최종 상태 확인 expect(finalOoc.state).toBe(DataOocStatus.CORRECTED) expect(finalOoc.correctiveInstruction).toBe('Temperature 조절 필요') expect(finalOoc.correctiveAction).toBe('Temperature를 정상 범위로 조절함') }) }) }) })