diff --git a/README.md b/README.md index de5f3cee..2f7c00a2 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,10 @@ HR이 승인된 Task의 Worker Link 발급 덮어쓰지 않고, HR이 승인한 결과만 Renewal Context에서 사용합니다. 생성된 문서는 자동 발송하지 않으며 다시 HR 검토를 거칩니다. +Language Assistant가 근로자 안내를 안전하게 만들지 못한 경우에도 임시 문장을 발송하지 +않습니다. Server는 실패 코드를 보존하고 Task를 HR 검토로 돌리며, 담당자가 안내문을 +작성하고 승인한 뒤에만 Worker Link와 SMS 전달을 허용합니다. + ### 업무 이벤트가 알림으로 이어지는 흐름 ```text diff --git a/docs/ai-runtime-contract.md b/docs/ai-runtime-contract.md index 53338888..70a5616c 100644 --- a/docs/ai-runtime-contract.md +++ b/docs/ai-runtime-contract.md @@ -404,6 +404,32 @@ Workflow로 바꾼 응답은 `UNEXPECTED_WORKFLOW`로 거부합니다. `scenario=out_of_scope`, `intent=OUT_OF_SCOPE`인 종료 응답만 `workflowId`가 비어 있을 수 있습니다. 이는 Workflow 실행 결과가 아니라 지원 범위 밖 정상 종료 신호이기 때문입니다. +## 근로자 안내 실패와 HR 검토 + +Language Assistant가 비활성화됐거나 안내문을 안전하게 생성하지 못하면 Runtime은 임시 +문장을 만들지 않고 다음과 같이 HR 검토를 요청합니다. + +```json +{ + "scenario": "ask_worker", + "status": "READY_FOR_REVIEW", + "outcome": "REVIEW_REQUIRED", + "workerRequestMessage": null, + "guideReviewRequired": true, + "guideFailureCode": "LANGUAGE_ASSISTANT_NOT_CONFIGURED", + "caseSignals": ["REVIEW_WORKER_GUIDE"] +} +``` + +Server는 `guideFailureCode`를 안전한 허용 목록으로 검증하고 Task의 +`renewal_execution` 메타데이터와 감사로그에 보존합니다. 이 경로에서는 근로자 안내 +초안, Worker Link, SMS를 자동으로 생성하거나 발송하지 않습니다. Task는 누락정보 입력 +상태로 회귀하지 않고 HR 검토 대상으로 남습니다. HR이 안전한 안내문을 작성하고 기존 +승인 절차를 마친 뒤에만 Worker Link 발급과 전달 흐름을 진행합니다. + +기존 Runtime이 신규 필드를 보내지 않으면 `guideReviewRequired=false`, +`guideFailureCode=null`로 해석하므로 정상 `ask_worker` 계약은 그대로 유지됩니다. + ## Renewal 생성 문서 연결 Renewal 응답의 `generatedDocuments[]`는 다음 세 값을 문서 생성 기준으로 사용합니다. diff --git a/src/main/java/com/fowoco/server/aiintegration/application/renewal/RenewalRunResponse.java b/src/main/java/com/fowoco/server/aiintegration/application/renewal/RenewalRunResponse.java index 5dcde2c9..5039b52a 100644 --- a/src/main/java/com/fowoco/server/aiintegration/application/renewal/RenewalRunResponse.java +++ b/src/main/java/com/fowoco/server/aiintegration/application/renewal/RenewalRunResponse.java @@ -24,6 +24,8 @@ public record RenewalRunResponse( List requestedFields, String guideMessage, String workerRequestMessage, + Boolean guideReviewRequired, + String guideFailureCode, Map languageAssistant, Map ocrResult, List generatedDocuments, @@ -37,6 +39,7 @@ public record RenewalRunResponse( List errors ) { public RenewalRunResponse { + guideReviewRequired = Boolean.TRUE.equals(guideReviewRequired); slots = copyMap(slots); missingSlots = copyList(missingSlots); requestedFields = requestedFields == null ? List.of() : List.copyOf(requestedFields); diff --git a/src/main/java/com/fowoco/server/aiintegration/application/validation/RenewalRuntimeContractValidator.java b/src/main/java/com/fowoco/server/aiintegration/application/validation/RenewalRuntimeContractValidator.java index 6182775e..c2da5568 100644 --- a/src/main/java/com/fowoco/server/aiintegration/application/validation/RenewalRuntimeContractValidator.java +++ b/src/main/java/com/fowoco/server/aiintegration/application/validation/RenewalRuntimeContractValidator.java @@ -31,8 +31,15 @@ public final class RenewalRuntimeContractValidator { "GENERATE_DRAFTS", "READY_FOR_REVIEW", "OCR_SAVED", + "REVIEW_WORKER_GUIDE", "CANCEL_OUT_OF_SCOPE" ); + private static final Set GUIDE_FAILURE_CODES = Set.of( + "LANGUAGE_ASSISTANT_NOT_CONFIGURED", + "LANGUAGE_ASSISTANT_INVOCATION_FAILED", + "LANGUAGE_ASSISTANT_REVIEW_REQUIRED", + "WORKER_GUIDE_UNAVAILABLE" + ); private static final Pattern IDENTIFIER = Pattern.compile("[A-Za-z][A-Za-z0-9._-]{0,127}"); private static final Set DOCUMENT_TEMPLATES = Set.of( "standard_labor_contract_v6", @@ -117,6 +124,7 @@ public void validateResponse(RenewalRunRequest request, RenewalRunResponse respo boundaryPolicy.validateText(response.status(), 40, true); boundaryPolicy.validateText(response.outcome(), 80, true); boundaryPolicy.validateText(response.workerRequestMessage(), 1_000, false); + validateWorkerGuideReview(response); validateLanguageAssistant(response); if (response.missingSlots().size() > 100 || response.requestedFields().size() > 100 @@ -176,6 +184,7 @@ private void validateScenario(RenewalRunResponse response) { reject(AiRuntimeFailureCode.INVALID_RESPONSE_CONTRACT, "ask_hr requires missing slots."); } if ("ask_worker".equals(response.scenario()) + && !response.guideReviewRequired() && (response.workerRequestMessage() == null || response.workerRequestMessage().isBlank())) { reject(AiRuntimeFailureCode.INVALID_RESPONSE_CONTRACT, "ask_worker requires a worker message."); } @@ -187,6 +196,37 @@ private void validateScenario(RenewalRunResponse response) { } } + private void validateWorkerGuideReview(RenewalRunResponse response) { + if (!response.guideReviewRequired()) { + if (response.guideFailureCode() != null && !response.guideFailureCode().isBlank()) { + reject( + AiRuntimeFailureCode.INVALID_RESPONSE_CONTRACT, + "Worker guide failure code requires review." + ); + } + if (response.caseSignals().contains("REVIEW_WORKER_GUIDE")) { + reject( + AiRuntimeFailureCode.INVALID_RESPONSE_CONTRACT, + "Worker guide review signal requires review." + ); + } + return; + } + if (!"ask_worker".equals(response.scenario()) + || !"READY_FOR_REVIEW".equals(response.status()) + || !"REVIEW_REQUIRED".equals(response.outcome()) + || (response.workerRequestMessage() != null + && !response.workerRequestMessage().isBlank()) + || response.guideFailureCode() == null + || !GUIDE_FAILURE_CODES.contains(response.guideFailureCode()) + || !response.caseSignals().contains("REVIEW_WORKER_GUIDE")) { + reject( + AiRuntimeFailureCode.INVALID_RESPONSE_CONTRACT, + "Worker guide review response is invalid." + ); + } + } + private void validateGeneratedDocument(RenewalGeneratedDocument document) { if (document == null || !DOCUMENT_TEMPLATES.contains(document.templateId()) diff --git a/src/main/java/com/fowoco/server/task/api/RenewalExecutionResponse.java b/src/main/java/com/fowoco/server/task/api/RenewalExecutionResponse.java index 12c6c5c3..b9db6bfa 100644 --- a/src/main/java/com/fowoco/server/task/api/RenewalExecutionResponse.java +++ b/src/main/java/com/fowoco/server/task/api/RenewalExecutionResponse.java @@ -27,6 +27,8 @@ public record RenewalExecutionResponse( List generatedDocuments, UUID workerMessageDraftId, Long workerMessageDraftVersion, + boolean guideReviewRequired, + String guideFailureCode, boolean humanReviewRequired ) { static RenewalExecutionResponse from(RenewalExecutionResult result) { @@ -46,6 +48,8 @@ static RenewalExecutionResponse from(RenewalExecutionResult result) { result.generatedDocuments(), result.workerMessageDraft() == null ? null : result.workerMessageDraft().draftId(), result.workerMessageDraft() == null ? null : result.workerMessageDraft().version(), + result.agentResult().guideReviewRequired(), + result.agentResult().guideFailureCode(), true ); } diff --git a/src/main/java/com/fowoco/server/task/application/renewal/RenewalExecutionResultApplier.java b/src/main/java/com/fowoco/server/task/application/renewal/RenewalExecutionResultApplier.java index 2404c30e..4e96363e 100644 --- a/src/main/java/com/fowoco/server/task/application/renewal/RenewalExecutionResultApplier.java +++ b/src/main/java/com/fowoco/server/task/application/renewal/RenewalExecutionResultApplier.java @@ -126,11 +126,7 @@ RenewalExecutionResult apply( encoded.businessDataJson(), encoded.criticalFingerprint(), task.dueDate(), - !"out_of_scope".equals(agentResult.scenario()) - && agentResult.missingSlots().isEmpty() - && !checklistStatusRepository.existsIncompleteRequiredItem( - task.taskId(), task.companyId() - ), + requirementsSatisfied(task, agentResult), expectedVersion, actor.actorId(), now @@ -155,7 +151,7 @@ RenewalExecutionResult apply( } appendAudit( actor, AuditAction.TASK_UPDATED, AuditTargetType.TASK, saved.taskId(), - "Renewal Agent 결과를 업무카드에 반영함", metadata, now + auditSummary(agentResult), metadata, now ); DocumentRequestDraft draft = saveWorkerMessageDraft(saved, agentResult, actor, metadata, now); @@ -200,6 +196,8 @@ private Map executionMetadata( .map(this::requestedFieldMetadata) .toList()); metadata.put("case_signals", result.caseSignals()); + metadata.put("guide_review_required", result.guideReviewRequired()); + putIfPresent(metadata, "guide_failure_code", result.guideFailureCode()); metadata.put("generated_documents", generatedDocuments.stream() .map(this::generatedDocumentMetadata) .toList()); @@ -233,7 +231,8 @@ private DocumentRequestDraft saveWorkerMessageDraft( RequestMetadata metadata, Instant now ) { - if (!"ask_worker".equals(result.scenario()) + if (result.guideReviewRequired() + || !"ask_worker".equals(result.scenario()) || result.workerRequestMessage() == null || result.workerRequestMessage().isBlank()) { return null; @@ -261,6 +260,25 @@ private DocumentRequestDraft saveWorkerMessageDraft( return draft; } + private boolean requirementsSatisfied(Task task, RenewalRunResponse result) { + if (result.guideReviewRequired()) { + return true; + } + return !"out_of_scope".equals(result.scenario()) + && result.missingSlots().isEmpty() + && !checklistStatusRepository.existsIncompleteRequiredItem( + task.taskId(), task.companyId() + ); + } + + private String auditSummary(RenewalRunResponse result) { + if (!result.guideReviewRequired()) { + return "Renewal Agent 결과를 업무카드에 반영함"; + } + return "Renewal Agent 근로자 안내를 HR 검토로 전환함: " + + result.guideFailureCode(); + } + private String language(RenewalRunResponse result) { Object target = result.languageAssistant() == null ? null diff --git a/src/test/java/com/fowoco/server/aiintegration/application/validation/RenewalRuntimeContractValidatorTest.java b/src/test/java/com/fowoco/server/aiintegration/application/validation/RenewalRuntimeContractValidatorTest.java index ce463e42..7f73c00c 100644 --- a/src/test/java/com/fowoco/server/aiintegration/application/validation/RenewalRuntimeContractValidatorTest.java +++ b/src/test/java/com/fowoco/server/aiintegration/application/validation/RenewalRuntimeContractValidatorTest.java @@ -75,6 +75,7 @@ void rejectsAnIntentUsedAsAWorkflowId() { "EXPIRY_RENEWAL", valid.confidence(), valid.status(), valid.outcome(), valid.scenario(), valid.phase(), valid.step(), valid.slots(), valid.missingSlots(), valid.requestedFields(), valid.guideMessage(), valid.workerRequestMessage(), + valid.guideReviewRequired(), valid.guideFailureCode(), valid.languageAssistant(), valid.ocrResult(), valid.generatedDocuments(), valid.evidence(), valid.documentValidation(), valid.caseSignals(), valid.progressEvents(), valid.supervisorReason(), valid.supervisorSource(), valid.activeSubgraph(), valid.errors() @@ -93,6 +94,7 @@ void rejectsAResponseFromAnotherAttempt() { valid.workflowId(), valid.confidence(), valid.status(), valid.outcome(), valid.scenario(), valid.phase(), valid.step(), valid.slots(), valid.missingSlots(), valid.requestedFields(), valid.guideMessage(), valid.workerRequestMessage(), + valid.guideReviewRequired(), valid.guideFailureCode(), valid.languageAssistant(), valid.ocrResult(), valid.generatedDocuments(), valid.evidence(), valid.documentValidation(), valid.caseSignals(), valid.progressEvents(), valid.supervisorReason(), valid.supervisorSource(), valid.activeSubgraph(), valid.errors() @@ -119,7 +121,7 @@ void acceptsAgentIdentitySignalsAndNullableLanguageFields() { new RenewalRequestedField("passport_number", "DOCUMENT_OCR"), new RenewalRequestedField("alien_registration_number", "DOCUMENT_OCR") ), - valid.guideMessage(), "여권과 외국인등록증을 제출해 주세요.", language, + valid.guideMessage(), "여권과 외국인등록증을 제출해 주세요.", false, null, language, valid.ocrResult(), valid.generatedDocuments(), valid.evidence(), valid.documentValidation(), List.of( @@ -135,6 +137,63 @@ void acceptsAgentIdentitySignalsAndNullableLanguageFields() { .doesNotThrowAnyException(); } + @Test + void acceptsAllWorkerGuideFailureCodesAsFailClosedReviewResponses() { + RenewalRunRequest request = request(); + + for (String failureCode : List.of( + "LANGUAGE_ASSISTANT_NOT_CONFIGURED", + "LANGUAGE_ASSISTANT_INVOCATION_FAILED", + "LANGUAGE_ASSISTANT_REVIEW_REQUIRED", + "WORKER_GUIDE_UNAVAILABLE" + )) { + assertThatCode(() -> validator.validateResponse( + request, + workerGuideReviewResponse(request, failureCode, null, true) + )).doesNotThrowAnyException(); + } + } + + @Test + void rejectsAWorkerGuideReviewResponseThatContainsAnAutomaticDeliveryMessage() { + RenewalRunRequest request = request(); + + assertThatThrownBy(() -> validator.validateResponse( + request, + workerGuideReviewResponse( + request, + "LANGUAGE_ASSISTANT_REVIEW_REQUIRED", + "검토하지 않은 안내입니다.", + true + ) + )).isInstanceOf(AiRuntimeContractException.class); + } + + @Test + void rejectsAWorkerGuideReviewSignalWithoutTheReviewFlag() { + RenewalRunRequest request = request(); + + assertThatThrownBy(() -> validator.validateResponse( + request, + workerGuideReviewResponse( + request, + "LANGUAGE_ASSISTANT_NOT_CONFIGURED", + null, + false + ) + )).isInstanceOf(AiRuntimeContractException.class); + } + + @Test + void rejectsAnUnknownWorkerGuideFailureCode() { + RenewalRunRequest request = request(); + + assertThatThrownBy(() -> validator.validateResponse( + request, + workerGuideReviewResponse(request, "RAW_PROVIDER_ERROR", null, true) + )).isInstanceOf(AiRuntimeContractException.class); + } + @Test void acceptsTheAgentOutOfScopeResultWithoutTreatingItAsRenewal() { RenewalRunRequest request = request(); @@ -143,7 +202,7 @@ void acceptsTheAgentOutOfScopeResultWithoutTreatingItAsRenewal() { valid.requestId(), valid.attemptId(), valid.taskId(), "OUT_OF_SCOPE", "", new BigDecimal("0.93"), "CANCELLED", "OUT_OF_SCOPE", "out_of_scope", "PHASE_1", "STEP_2", Map.of(), List.of(), List.of(), - "지원 범위를 벗어난 요청입니다.", null, null, null, List.of(), List.of(), + "지원 범위를 벗어난 요청입니다.", null, false, null, null, null, List.of(), List.of(), null, List.of("CANCEL_OUT_OF_SCOPE"), List.of(), null, "rules", "main", List.of() ); @@ -209,7 +268,7 @@ private RenewalRunResponse response(RenewalRunRequest request, String workflowId workflowId, new BigDecimal("0.91"), "NEEDS_INFO", "NEEDS_INFO", "ask_hr", "PHASE_2", "STEP_5", Map.of(), List.of("wage"), List.of(new RenewalRequestedField("wage", "USER_INPUT")), - "임금을 확인해 주세요.", null, null, null, List.of(), List.of(), null, + "임금을 확인해 주세요.", null, false, null, null, null, List.of(), List.of(), null, List.of("REQUEST_CONTRACT_SLOTS", "NEEDS_INFO"), List.of(), null, "rules", "main", List.of() ); @@ -224,7 +283,7 @@ private RenewalRunResponse generateResponse( request.task().workflowId(), new BigDecimal("0.94"), "READY_FOR_REVIEW", "REVIEW_REQUIRED", "generate", "PHASE_4", "STEP_13", Map.of(), List.of(), List.of(), - null, null, null, null, + null, null, false, null, null, null, List.of(new RenewalGeneratedDocument( "standard_labor_contract_v6", "표준근로계약서", "hwp", "stub", null, null, List.copyOf(values.keySet()), List.of(), values @@ -233,4 +292,22 @@ private RenewalRunResponse generateResponse( List.of(), null, "rules", "main", List.of() ); } + + private RenewalRunResponse workerGuideReviewResponse( + RenewalRunRequest request, + String failureCode, + String workerMessage, + boolean reviewRequired + ) { + return new RenewalRunResponse( + request.requestId(), request.attemptId(), request.taskId(), "EXPIRY_RENEWAL", + request.task().workflowId(), new BigDecimal("0.91"), + "READY_FOR_REVIEW", "REVIEW_REQUIRED", "ask_worker", + "PHASE_3", "STEP_5", Map.of(), List.of("passport_number"), + List.of(new RenewalRequestedField("passport_number", "DOCUMENT_OCR")), + null, workerMessage, reviewRequired, failureCode, null, null, + List.of(), List.of(), null, List.of("REVIEW_WORKER_GUIDE"), + List.of(), null, "rules", "main", List.of() + ); + } } diff --git a/src/test/java/com/fowoco/server/aiintegration/infrastructure/http/RemoteRenewalRuntimeClientWireMockTest.java b/src/test/java/com/fowoco/server/aiintegration/infrastructure/http/RemoteRenewalRuntimeClientWireMockTest.java index eb4f93eb..ae881495 100644 --- a/src/test/java/com/fowoco/server/aiintegration/infrastructure/http/RemoteRenewalRuntimeClientWireMockTest.java +++ b/src/test/java/com/fowoco/server/aiintegration/infrastructure/http/RemoteRenewalRuntimeClientWireMockTest.java @@ -93,6 +93,24 @@ void decodesGeneratedDocumentValuesAndIgnoresTheAgentLocalPathForLaterUse() { .containsEntry("employee_name", "NGUYEN VAN AN"); } + @Test + void decodesTheWorkerGuideReviewContract() { + RenewalRunRequest request = request(); + wireMock.stubFor(post(urlEqualTo(PATH)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(workerGuideReviewResponseJson(request)))); + + RenewalRunResponse response = client().run(request, AiRuntimeCallContext.withoutTrace()); + + assertThat(response.guideReviewRequired()).isTrue(); + assertThat(response.guideFailureCode()) + .isEqualTo("LANGUAGE_ASSISTANT_NOT_CONFIGURED"); + assertThat(response.workerRequestMessage()).isNull(); + assertThat(response.caseSignals()).containsExactly("REVIEW_WORKER_GUIDE"); + } + private RemoteRenewalRuntimeClient client() { return new RemoteRenewalRuntimeClient( URI.create(wireMock.baseUrl() + PATH), @@ -207,4 +225,40 @@ private String generatedResponseJson(RenewalRunRequest request) { } """.formatted(request.requestId(), request.attemptId(), request.taskId()); } + + private String workerGuideReviewResponseJson(RenewalRunRequest request) { + return """ + { + "requestId":"%s", + "attemptId":"%s", + "taskId":"%s", + "intent":"EXPIRY_RENEWAL", + "workflowId":"WF-CON-001", + "confidence":0.91, + "status":"READY_FOR_REVIEW", + "outcome":"REVIEW_REQUIRED", + "scenario":"ask_worker", + "phase":"PHASE_3", + "step":"STEP_5", + "slots":{}, + "missingSlots":["passport_number"], + "requestedFields":[{"key":"passport_number","sourceHint":"DOCUMENT_OCR"}], + "guideMessage":null, + "workerRequestMessage":null, + "guideReviewRequired":true, + "guideFailureCode":"LANGUAGE_ASSISTANT_NOT_CONFIGURED", + "languageAssistant":null, + "ocrResult":null, + "generatedDocuments":[], + "evidence":[], + "documentValidation":null, + "caseSignals":["REVIEW_WORKER_GUIDE"], + "progressEvents":[], + "supervisorReason":null, + "supervisorSource":"rules", + "activeSubgraph":"main", + "errors":[] + } + """.formatted(request.requestId(), request.attemptId(), request.taskId()); + } } diff --git a/src/test/java/com/fowoco/server/task/RenewalExecutionIntegrationTest.java b/src/test/java/com/fowoco/server/task/RenewalExecutionIntegrationTest.java index 7a83c72c..4e7b6c5d 100644 --- a/src/test/java/com/fowoco/server/task/RenewalExecutionIntegrationTest.java +++ b/src/test/java/com/fowoco/server/task/RenewalExecutionIntegrationTest.java @@ -204,6 +204,51 @@ void storesAnAgentWorkerMessageAsAnUnsentDraft() throws Exception { )).isEqualTo(1); } + @Test + void keepsAWorkerGuideFailureInHrReviewWithoutCreatingDeliveryResources() throws Exception { + jdbcTemplate.update("UPDATE task SET status = 'NEEDS_INFO' WHERE task_id = ?", TASK_A); + when(runtimeClient.run(any(), any())).thenAnswer(invocation -> + workerGuideReviewResponse(invocation.getArgument(0)) + ); + String token = login(HR_A_EMAIL); + + HttpResponse response = postRenewal(token, 0); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(JsonPath.read(response.body(), "$.task_status")).isEqualTo("DRAFT"); + assertThat(JsonPath.read(response.body(), "$.guide_review_required")).isTrue(); + assertThat(JsonPath.read(response.body(), "$.guide_failure_code")) + .isEqualTo("LANGUAGE_ASSISTANT_NOT_CONFIGURED"); + assertThat(JsonPath.read(response.body(), "$.worker_message_draft_id")).isNull(); + + String businessData = jdbcTemplate.queryForObject( + "SELECT business_data_json FROM task WHERE task_id = ?", String.class, TASK_A + ); + assertThat(JsonPath.read( + businessData, + "$.renewal_execution.guide_review_required" + )).isTrue(); + assertThat(JsonPath.read( + businessData, + "$.renewal_execution.guide_failure_code" + )).isEqualTo("LANGUAGE_ASSISTANT_NOT_CONFIGURED"); + assertThat(jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM document_request_draft WHERE task_id = ?", + Integer.class, + TASK_A + )).isZero(); + assertThat(jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM worker_link WHERE task_id = ?", + Integer.class, + TASK_A + )).isZero(); + assertThat(jdbcTemplate.queryForObject( + "SELECT change_summary FROM audit_event WHERE target_id = ? AND action = 'TASK_UPDATED'", + String.class, + TASK_A + )).contains("LANGUAGE_ASSISTANT_NOT_CONFIGURED"); + } + @Test void storesHrSlotAnswersAndSendsThemToTheNextRenewalRun() throws Exception { when(runtimeClient.run(any(), any())).thenAnswer(invocation -> { @@ -482,7 +527,7 @@ private RenewalRunResponse generateResponse(RenewalRunRequest request) { request.requestId(), request.attemptId(), request.taskId(), "EXPIRY_RENEWAL", request.task().workflowId(), new BigDecimal("0.94"), "READY_FOR_REVIEW", "REVIEW_REQUIRED", "generate", "PHASE_4", "STEP_13", Map.of(), List.of(), List.of(), - null, null, null, null, + null, null, false, null, null, null, List.of(new RenewalGeneratedDocument( "standard_labor_contract_v6", "표준근로계약서", @@ -502,6 +547,19 @@ private RenewalRunResponse generateResponse(RenewalRunRequest request) { ); } + private RenewalRunResponse workerGuideReviewResponse(RenewalRunRequest request) { + return new RenewalRunResponse( + request.requestId(), request.attemptId(), request.taskId(), "EXPIRY_RENEWAL", + request.task().workflowId(), new BigDecimal("0.91"), + "READY_FOR_REVIEW", "REVIEW_REQUIRED", "ask_worker", + "PHASE_3", "STEP_5", Map.of(), List.of("passport_number"), + List.of(new RenewalRequestedField("passport_number", "DOCUMENT_OCR")), + null, null, true, "LANGUAGE_ASSISTANT_NOT_CONFIGURED", null, null, + List.of(), List.of(), null, List.of("REVIEW_WORKER_GUIDE"), + List.of(), null, "rules", "main", List.of() + ); + } + private byte[] validHwpFile() { try (org.apache.poi.poifs.filesystem.POIFSFileSystem fileSystem = new org.apache.poi.poifs.filesystem.POIFSFileSystem()) { @@ -522,7 +580,7 @@ private RenewalRunResponse outOfScopeResponse(RenewalRunRequest request) { request.requestId(), request.attemptId(), request.taskId(), "OUT_OF_SCOPE", "", new BigDecimal("0.93"), "CANCELLED", "OUT_OF_SCOPE", "out_of_scope", "PHASE_1", "STEP_2", Map.of(), List.of(), List.of(), - "지원 범위를 벗어난 요청입니다.", null, null, null, List.of(), List.of(), + "지원 범위를 벗어난 요청입니다.", null, false, null, null, null, List.of(), List.of(), null, List.of("CANCEL_OUT_OF_SCOPE"), List.of(), null, "rules", "main", List.of() ); @@ -543,7 +601,7 @@ private RenewalRunResponse response( request.requestId(), request.attemptId(), request.taskId(), "EXPIRY_RENEWAL", request.task().workflowId(), new BigDecimal("0.91"), status, outcome, scenario, "PHASE_2", "STEP_5", Map.of(), missingSlots, requestedFields, - null, workerMessage, languageAssistant, null, List.of(), List.of(), null, + null, workerMessage, false, null, languageAssistant, null, List.of(), List.of(), null, signals, List.of(), null, "rules", "main", List.of() ); } diff --git a/src/test/java/com/fowoco/server/task/application/renewal/RenewalExecutionServiceTest.java b/src/test/java/com/fowoco/server/task/application/renewal/RenewalExecutionServiceTest.java index 875c3861..b0a68c25 100644 --- a/src/test/java/com/fowoco/server/task/application/renewal/RenewalExecutionServiceTest.java +++ b/src/test/java/com/fowoco/server/task/application/renewal/RenewalExecutionServiceTest.java @@ -64,7 +64,7 @@ void setUp() { EVENT_ID, UUID.randomUUID(), TASK_ID, "EXPIRY_RENEWAL", "WF-STY-001", new BigDecimal("0.95"), "READY_FOR_REVIEW", "ARTIFACT_READY", "generate", "ACT", "GENERATE", - Map.of(), List.of(), List.of(), null, null, null, null, + Map.of(), List.of(), List.of(), null, null, false, null, null, null, List.of(document), List.of(), null, List.of(), List.of(), null, null, null, List.of() ); preparedDocuments = List.of(mock(PreparedRenewalDocument.class)); diff --git a/src/test/java/com/fowoco/server/workerlink/WorkerLinkSecurityIntegrationTest.java b/src/test/java/com/fowoco/server/workerlink/WorkerLinkSecurityIntegrationTest.java index d69bdc83..5f6ad982 100644 --- a/src/test/java/com/fowoco/server/workerlink/WorkerLinkSecurityIntegrationTest.java +++ b/src/test/java/com/fowoco/server/workerlink/WorkerLinkSecurityIntegrationTest.java @@ -360,6 +360,8 @@ private RenewalRunResponse continuationResponse(RenewalRunRequest request) { List.of(new RenewalRequestedField("wage", "USER_INPUT")), null, null, + false, + null, null, null, List.of(),