/** * Activity Callback Unit Tests * OOC Review/Resolve Activity callback 테스트 */ import { TestDatabase } from '../../../../../test/test-database' import { withTestTransaction } from '../../../../../test/test-context' import { domainFactory, userFactory, roleFactory, dataSetFactory, dataOocFactory, activityFactory, activityInstanceFactory, activityThreadFactory } from '../../../../../test/factories' import { DataOocStatus, ActivityInstanceStatus, ActivityThreadStatus } from '../../../../../test/entities/schemas' describe('Activity Callbacks', () => { let testDb: TestDatabase beforeAll(async () => { testDb = TestDatabase.getInstance() }) describe('ActivityOocReview Callback', () => { describe('Review Activity 완료 시 동작', () => { it('ActivityInstance가 Ended 상태가 되면 DataOoc 상태가 REVIEWED로 전이되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain, user } = context.state // Given: Review Activity와 DataOoc 설정 const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx) const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx) let dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED }, dataSet, undefined, tx ) // Review ActivityInstance 생성 const reviewInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 검토] ${dataSet.name}`, state: ActivityInstanceStatus.Issued, input: { dataOocId: dataOoc.id }, output: {}, assigneeRole: supervisoryRole }, reviewActivity, domain, tx ) // When: ActivityInstance가 Ended 상태로 변경 (callback 시뮬레이션) const terminatedAt = new Date() const correctiveInstruction = 'Temperature를 정상 범위로 조절하세요' reviewInstance.state = ActivityInstanceStatus.Ended reviewInstance.terminatedAt = terminatedAt reviewInstance.output = { instruction: correctiveInstruction } await tx.save('ActivityInstance', reviewInstance) // Callback 로직 시뮬레이션: DataOoc 업데이트 dataOoc = await tx.getRepository('DataOoc').findOne({ where: { id: dataOoc.id }, relations: ['dataSet'] }) as any dataOoc.reviewedAt = terminatedAt dataOoc.reviewer = user dataOoc.correctiveInstruction = correctiveInstruction dataOoc.state = DataOocStatus.REVIEWED dataOoc = await tx.save('DataOoc', dataOoc) // Then expect(dataOoc.state).toBe(DataOocStatus.REVIEWED) expect(dataOoc.reviewedAt).toEqual(terminatedAt) expect(dataOoc.reviewer?.id).toBe(user.id) expect(dataOoc.correctiveInstruction).toBe(correctiveInstruction) }) }) it('Review 완료 시 correctiveInstruction이 output.instruction에서 가져와야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain, user } = context.state // Given const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx) const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx) let dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED }, dataSet, undefined, tx ) const instruction = '습도 레벨을 30-70% 사이로 유지하세요' const reviewInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Ended, input: { dataOocId: dataOoc.id }, output: { instruction }, terminatedAt: new Date() }, reviewActivity, domain, tx ) // When: Callback 로직 const outputInstruction = reviewInstance.output?.instruction dataOoc.correctiveInstruction = outputInstruction dataOoc.state = DataOocStatus.REVIEWED dataOoc = await tx.save('DataOoc', dataOoc) // Then expect(dataOoc.correctiveInstruction).toBe(instruction) }) }) it('Review 완료 후 issueOocResolve가 호출되어야 한다 (Resolve 발행)', async () => { await withTestTransaction(async (context) => { const { tx, domain, user } = context.state // Given const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const { dataSet, supervisoryRole, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx) let dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED }, dataSet, undefined, tx ) const instruction = 'Fix the issue' const terminatedAt = new Date() // When: Review 완료 및 Callback 전체 로직 시뮬레이션 // 1. DataOoc REVIEWED로 업데이트 dataOoc.reviewedAt = terminatedAt dataOoc.reviewer = user dataOoc.correctiveInstruction = instruction dataOoc.state = DataOocStatus.REVIEWED dataOoc = await tx.save('DataOoc', dataOoc) // 2. issueOocResolve 호출 (Resolve Activity 발행) const resolveInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 조치] ${dataSet.name}`, state: ActivityInstanceStatus.Issued, input: { dataOocId: dataOoc.id, instruction }, assigneeRole: resolverRole, approvalLine: dataSet.outlierApprovalLine || [] }, resolveActivity, domain, tx ) dataOoc.resolveActivityInstance = resolveInstance dataOoc = await tx.save('DataOoc', dataOoc) // Then expect(dataOoc.state).toBe(DataOocStatus.REVIEWED) expect(dataOoc.resolveActivityInstance).toBeDefined() expect(dataOoc.resolveActivityInstance?.input?.instruction).toBe(instruction) }) }) it('Ended가 아닌 상태에서는 callback 로직이 실행되지 않아야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx) const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED }, dataSet, undefined, tx ) const reviewInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Started, // Ended가 아님 input: { dataOocId: dataOoc.id } }, reviewActivity, domain, tx ) // When: Callback 조건 체크 const shouldExecuteCallback = reviewInstance.state === ActivityInstanceStatus.Ended // Then expect(shouldExecuteCallback).toBe(false) }) }) }) }) describe('ActivityOocResolve Callback', () => { describe('Resolve Activity 완료 시 동작', () => { it('ActivityInstance가 Ended 상태가 되면 DataOoc 상태가 CORRECTED로 전이되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain, user } = context.state // Given: REVIEWED 상태의 DataOoc과 Resolve Activity const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const { dataSet, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx) let dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.REVIEWED, reviewedAt: new Date(), correctiveInstruction: 'Fix temperature' }, dataSet, undefined, tx ) // Resolve ActivityInstance 생성 const resolveInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 조치] ${dataSet.name}`, state: ActivityInstanceStatus.Issued, input: { dataOocId: dataOoc.id, instruction: dataOoc.correctiveInstruction }, output: {}, assigneeRole: resolverRole }, resolveActivity, domain, tx ) // Thread 생성 (corrector 정보 획득용) const assignee = await userFactory.create({ name: 'Corrector User' }, tx) const thread = await activityThreadFactory.createWithInstanceAndAssignee( { state: ActivityThreadStatus.Ended, terminatedAt: new Date() }, resolveInstance, assignee, tx ) // When: ActivityInstance가 Ended 상태로 변경 (callback 시뮬레이션) const terminatedAt = new Date() const correctiveAction = 'Temperature를 정상 범위로 조절 완료' resolveInstance.state = ActivityInstanceStatus.Ended resolveInstance.terminatedAt = terminatedAt resolveInstance.output = { action: correctiveAction } await tx.save('ActivityInstance', resolveInstance) // Callback 로직 시뮬레이션: // 1. Ended 상태의 ActivityThread에서 corrector 조회 const endedThreads = await tx.getRepository('ActivityThread').find({ where: { domain: { id: domain.id }, activityInstance: { id: resolveInstance.id }, state: ActivityThreadStatus.Ended }, relations: ['assignee'] }) const corrector = endedThreads[0]?.assignee // 2. DataOoc 업데이트 dataOoc = await tx.getRepository('DataOoc').findOne({ where: { id: dataOoc.id } }) as any dataOoc.correctedAt = terminatedAt dataOoc.corrector = corrector dataOoc.correctiveAction = correctiveAction dataOoc.state = DataOocStatus.CORRECTED dataOoc = await tx.save('DataOoc', dataOoc) // Then expect(dataOoc.state).toBe(DataOocStatus.CORRECTED) expect(dataOoc.correctedAt).toEqual(terminatedAt) expect(dataOoc.corrector?.id).toBe(assignee.id) expect(dataOoc.correctiveAction).toBe(correctiveAction) }) }) it('correctiveAction은 output.action에서 가져와야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain, user } = context.state // Given const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx) let dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.REVIEWED }, dataSet, undefined, tx ) const action = '온도 조절 밸브를 교체하고 캘리브레이션 수행' const resolveInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Ended, input: { dataOocId: dataOoc.id }, output: { action }, terminatedAt: new Date() }, resolveActivity, domain, tx ) // When: Callback 로직 const outputAction = resolveInstance.output?.action dataOoc.correctiveAction = outputAction dataOoc.state = DataOocStatus.CORRECTED dataOoc = await tx.save('DataOoc', dataOoc) // Then expect(dataOoc.correctiveAction).toBe(action) }) }) it('corrector는 Ended 상태의 ActivityThread의 assignee여야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.REVIEWED }, dataSet, undefined, tx ) const resolveInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Ended, input: { dataOocId: dataOoc.id } }, resolveActivity, domain, tx ) // 여러 Thread 중 Ended 상태인 Thread의 assignee const worker1 = await userFactory.create({ name: 'Worker 1' }, tx) const worker2 = await userFactory.create({ name: 'Worker 2' }, tx) await activityThreadFactory.createWithInstanceAndAssignee( { state: ActivityThreadStatus.Aborted }, // Aborted - 제외 resolveInstance, worker1, tx ) const endedThread = await activityThreadFactory.createWithInstanceAndAssignee( { state: ActivityThreadStatus.Ended }, // Ended - 선택됨 resolveInstance, worker2, tx ) // When: Ended 상태의 Thread 조회 const endedThreads = await tx.getRepository('ActivityThread').find({ where: { activityInstance: { id: resolveInstance.id }, state: ActivityThreadStatus.Ended }, relations: ['assignee'] }) // Then: Ended 상태의 Thread의 assignee가 corrector expect(endedThreads.length).toBe(1) expect(endedThreads[0].assignee?.id).toBe(worker2.id) }) }) it('Ended가 아닌 상태에서는 callback 로직이 실행되지 않아야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.REVIEWED }, dataSet, undefined, tx ) const resolveInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Submitted, // Ended가 아님 input: { dataOocId: dataOoc.id } }, resolveActivity, domain, tx ) // When: Callback 조건 체크 const shouldExecuteCallback = resolveInstance.state === ActivityInstanceStatus.Ended // Then expect(shouldExecuteCallback).toBe(false) }) }) }) describe('DataOoc 조회 및 업데이트', () => { it('callback에서 dataOocId로 DataOoc을 조회할 수 있어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given const { dataSet } = await dataSetFactory.createWithRoles({}, domain, tx) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.REVIEWED }, dataSet, undefined, tx ) // When: input에서 dataOocId 추출 후 조회 const input = { dataOocId: dataOoc.id } const foundDataOoc = await tx.getRepository('DataOoc').findOne({ where: { domain: { id: domain.id }, id: input.dataOocId }, relations: ['dataSet'] }) // Then expect(foundDataOoc).toBeDefined() expect(foundDataOoc?.id).toBe(dataOoc.id) expect(foundDataOoc?.dataSet?.id).toBe(dataSet.id) }) }) }) }) describe('전체 워크플로우 Callback 연계', () => { it('OOC 생성 → Review Callback → Resolve Callback 전체 플로우', async () => { await withTestTransaction(async (context) => { const { tx, domain, user } = context.state // Setup const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) 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) // 1. DataOoc 생성 (ISSUED) let dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED, history: [ { user: { id: user.id, name: user.name }, state: DataOocStatus.ISSUED, timestamp: new Date().toISOString() } ] }, dataSet, undefined, tx ) expect(dataOoc.state).toBe(DataOocStatus.ISSUED) // 2. Review Activity 발행 const reviewInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Issued, input: { dataOocId: dataOoc.id }, assigneeRole: supervisoryRole }, reviewActivity, domain, tx ) // 3. Review Callback 시뮬레이션 (Ended 상태로) const reviewTerminatedAt = new Date() const instruction = 'Temperature 조절 필요' reviewInstance.state = ActivityInstanceStatus.Ended reviewInstance.terminatedAt = reviewTerminatedAt reviewInstance.output = { instruction } await tx.save('ActivityInstance', reviewInstance) // DataOoc 업데이트 (Review Callback) dataOoc = await tx.getRepository('DataOoc').findOne({ where: { id: dataOoc.id }, relations: ['dataSet'] }) as any dataOoc.reviewedAt = reviewTerminatedAt dataOoc.reviewer = reviewer dataOoc.correctiveInstruction = instruction dataOoc.state = DataOocStatus.REVIEWED // history 업데이트 const history = dataOoc.history || [] history.push({ user: { id: reviewer.id, name: reviewer.name }, state: DataOocStatus.REVIEWED, timestamp: reviewTerminatedAt.toISOString() }) dataOoc.history = history dataOoc = await tx.save('DataOoc', dataOoc) expect(dataOoc.state).toBe(DataOocStatus.REVIEWED) // 4. Resolve Activity 발행 (Review Callback에서 호출) const resolveInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Issued, input: { dataOocId: dataOoc.id, instruction }, assigneeRole: resolverRole }, resolveActivity, domain, tx ) // 5. Resolve Thread 생성 및 완료 const resolveThread = await activityThreadFactory.createWithInstanceAndAssignee( { state: ActivityThreadStatus.Ended, terminatedAt: new Date() }, resolveInstance, corrector, tx ) // 6. Resolve Callback 시뮬레이션 (Ended 상태로) const resolveTerminatedAt = new Date() const action = 'Temperature를 정상 범위로 조절함' resolveInstance.state = ActivityInstanceStatus.Ended resolveInstance.terminatedAt = resolveTerminatedAt resolveInstance.output = { action } await tx.save('ActivityInstance', resolveInstance) // DataOoc 업데이트 (Resolve Callback) dataOoc = await tx.getRepository('DataOoc').findOne({ where: { id: dataOoc.id }, relations: ['reviewer'] }) as any dataOoc.correctedAt = resolveTerminatedAt dataOoc.corrector = corrector dataOoc.correctiveAction = action dataOoc.state = DataOocStatus.CORRECTED // history 업데이트 dataOoc.history.push({ user: { id: corrector.id, name: corrector.name }, state: DataOocStatus.CORRECTED, comment: action, timestamp: resolveTerminatedAt.toISOString() }) dataOoc = await tx.save('DataOoc', dataOoc) // Then: 최종 상태 확인 - 다시 조회하여 relations 포함 const finalDataOoc = await tx.getRepository('DataOoc').findOne({ where: { id: dataOoc.id }, relations: ['reviewer', 'corrector'] }) as any expect(finalDataOoc.state).toBe(DataOocStatus.CORRECTED) expect(finalDataOoc.reviewedAt).toBeDefined() expect(finalDataOoc.correctedAt).toBeDefined() expect(finalDataOoc.reviewer?.id).toBe(reviewer.id) expect(finalDataOoc.corrector?.id).toBe(corrector.id) expect(finalDataOoc.correctiveInstruction).toBe(instruction) expect(finalDataOoc.correctiveAction).toBe(action) expect(finalDataOoc.history.length).toBe(3) }) }) }) })