Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,6 @@ private TaskActionDecision resolvePreparation(TaskResult result) {
TaskAvailableAction.RUN_RENEWAL
);
}
if (renewalSupported && renewal.hasMissingSource(DOCUMENT_OCR)) {
return TaskActionDecision.of(
TaskAvailableAction.REVIEW_OCR,
"OCR_REVIEW_REQUIRED",
TaskAvailableAction.REVIEW_OCR
);
}
if (renewalSupported && renewal.hasMissingSource(USER_INPUT)) {
return TaskActionDecision.of(
TaskAvailableAction.RUN_RENEWAL,
Expand Down Expand Up @@ -98,6 +91,15 @@ private TaskActionDecision resolvePreparation(TaskResult result) {
TaskAvailableAction.REVIEW_WORKER_GUIDE
);
}
if (renewalSupported
&& renewal.hasMissingSource(DOCUMENT_OCR)
&& !renewal.requiresWorkerDocumentCollection()) {
return TaskActionDecision.of(
TaskAvailableAction.REVIEW_OCR,
"OCR_REVIEW_REQUIRED",
TaskAvailableAction.REVIEW_OCR
);
}

List<TaskAvailableAction> available = new ArrayList<>();
if (renewal.generatedDocumentPresent()) {
Expand All @@ -124,24 +126,27 @@ private record RenewalProgress(
boolean executed,
Set<String> missingSlots,
Map<String, String> sourceByField,
String scenario,
boolean guideReviewRequired,
boolean generatedDocumentPresent
) {
private static RenewalProgress from(Map<String, Object> businessData) {
Object executionValue = businessData.get("renewal_execution");
if (!(executionValue instanceof Map<?, ?> execution)) {
return new RenewalProgress(false, Set.of(), Map.of(), false, false);
return new RenewalProgress(false, Set.of(), Map.of(), null, false, false);
}

Set<String> missingSlots = stringSet(execution.get("missing_slots"));
Map<String, String> sources = requestedFieldSources(execution.get("requested_fields"));
String scenario = stringValue(execution.get("scenario"));
boolean guideReviewRequired = Boolean.TRUE.equals(execution.get("guide_review_required"));
boolean generatedDocumentPresent = execution.get("generated_documents") instanceof List<?> documents
&& !documents.isEmpty();
return new RenewalProgress(
true,
missingSlots,
sources,
scenario,
guideReviewRequired,
generatedDocumentPresent
);
Expand All @@ -151,6 +156,16 @@ private boolean hasMissingSource(String source) {
return missingSlots.stream().anyMatch(slot -> source.equals(sourceByField.get(slot)));
}

private boolean requiresWorkerDocumentCollection() {
// DOCUMENT_OCR is a future value source in ask_worker. The document can only arrive
// after approval and Worker Link delivery, so it must not block the approval request.
return "ask_worker".equals(scenario);
}

private static String stringValue(Object value) {
return value instanceof String text && !text.isBlank() ? text : null;
}

private static Set<String> stringSet(Object value) {
if (!(value instanceof List<?> values)) {
return Set.of();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ void distinguishesAnUnexpectedAgentWorkflow() throws Exception {
}

@Test
void storesAnAgentWorkerMessageAsAnUnsentDraft() throws Exception {
void storesAnAgentWorkerMessageAndAllowsApprovalBeforeOcrCollection() throws Exception {
when(runtimeClient.run(any(), any())).thenAnswer(invocation -> askWorkerResponse(invocation.getArgument(0)));
String token = login(HR_A_EMAIL);

Expand All @@ -236,6 +236,19 @@ void storesAnAgentWorkerMessageAsAnUnsentDraft() throws Exception {
"SELECT COUNT(*) FROM audit_event WHERE action = 'DOCUMENT_REQUEST_DRAFT_SAVED'",
Integer.class
)).isEqualTo(1);

long taskVersion = ((Number) JsonPath.read(response.body(), "$.task_version")).longValue();
HttpResponse<String> task = getTask(token);
assertThat(task.statusCode()).isEqualTo(200);
assertThat(JsonPath.<String>read(task.body(), "$.next_action"))
.isEqualTo("REQUEST_APPROVAL");
assertThat(JsonPath.<List<String>>read(task.body(), "$.available_actions"))
.containsExactly("REQUEST_APPROVAL");

HttpResponse<String> approval = requestApproval(token, taskVersion);
assertThat(approval.statusCode()).isEqualTo(201);
assertThat(JsonPath.<String>read(approval.body(), "$.task_status"))
.isEqualTo("READY_FOR_REVIEW");
}

@Test
Expand Down Expand Up @@ -830,20 +843,32 @@ private HttpResponse<String> postRenewalWithSlots(
}

private HttpResponse<String> requestApproval(String token) throws Exception {
return requestApproval(token, 0);
}

private HttpResponse<String> requestApproval(String token, long expectedVersion) throws Exception {
HttpRequest request = HttpRequest.newBuilder(
uri("/api/v1/tasks/" + TASK_A + "/approval-requests")
)
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.POST(HttpRequest.BodyPublishers.ofString("""
{
"expected_version":0,
"expected_version":%d,
"ai_snapshot":{"intent":"EXPIRY_RENEWAL"},
"hr_snapshot":{"worker_id":"%s"},
"changed_fields":[],
"source_versions":{"workflow_catalog_version":"0.2.0"}
}
""".formatted(WORKER_A)))
""".formatted(expectedVersion, WORKER_A)))
.build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
}

private HttpResponse<String> getTask(String token) throws Exception {
HttpRequest request = HttpRequest.newBuilder(uri("/api/v1/tasks/" + TASK_A))
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.GET()
.build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,32 @@ void freshSupportedRenewalCanRunAgent() {
}

@Test
void ocrMissingFieldMustBeReviewedBeforeManualRenewal() {
void workerDocumentCollectionRequiresApprovalBeforeFutureOcrReview() {
TaskActionDecision decision = resolver.resolve(result(
TaskStatus.DRAFT,
renewalExecution(
"ask_worker",
false,
List.of("passport_number"),
List.of(Map.of("key", "passport_number", "source_hint", "DOCUMENT_OCR")),
List.of()
),
List.of(completedChecklist()),
List.of()
));

assertThat(decision.nextAction()).isEqualTo(TaskAvailableAction.REQUEST_APPROVAL);
assertThat(decision.availableActions()).containsExactly(TaskAvailableAction.REQUEST_APPROVAL);
assertThat(decision.blockedReason()).isEqualTo("APPROVAL_REQUIRED_BEFORE_CONTINUATION");
}

@Test
void ocrScenarioCanStillRequireReview() {
TaskActionDecision decision = resolver.resolve(result(
TaskStatus.NEEDS_INFO,
renewalExecution(
"ocr",
false,
List.of("passport_number"),
List.of(Map.of("key", "passport_number", "source_hint", "DOCUMENT_OCR")),
List.of()
Expand All @@ -54,10 +76,30 @@ void ocrMissingFieldMustBeReviewedBeforeManualRenewal() {
));

assertThat(decision.nextAction()).isEqualTo(TaskAvailableAction.REVIEW_OCR);
assertThat(decision.availableActions()).doesNotContain(TaskAvailableAction.RUN_RENEWAL);
assertThat(decision.availableActions()).containsExactly(TaskAvailableAction.REVIEW_OCR);
assertThat(decision.blockedReason()).isEqualTo("OCR_REVIEW_REQUIRED");
}

@Test
void workerGuideReviewPrecedesApprovalWithFutureOcrFields() {
TaskActionDecision decision = resolver.resolve(result(
TaskStatus.DRAFT,
renewalExecution(
"ask_worker",
true,
List.of("passport_number"),
List.of(Map.of("key", "passport_number", "source_hint", "DOCUMENT_OCR")),
List.of()
),
List.of(completedChecklist()),
List.of()
));

assertThat(decision.nextAction()).isEqualTo(TaskAvailableAction.REVIEW_WORKER_GUIDE);
assertThat(decision.availableActions()).containsExactly(TaskAvailableAction.REVIEW_WORKER_GUIDE);
assertThat(decision.blockedReason()).isEqualTo("WORKER_GUIDE_REVIEW_REQUIRED");
}

@Test
void completedRenewalPreparationRequiresApprovalInsteadOfAnotherRun() {
TaskActionDecision decision = resolver.resolve(result(
Expand Down Expand Up @@ -168,11 +210,22 @@ private Map<String, Object> renewalExecution(
List<String> missingSlots,
List<Map<String, String>> requestedFields,
List<Map<String, String>> generatedDocuments
) {
return renewalExecution("generate", false, missingSlots, requestedFields, generatedDocuments);
}

private Map<String, Object> renewalExecution(
String scenario,
boolean guideReviewRequired,
List<String> missingSlots,
List<Map<String, String>> requestedFields,
List<Map<String, String>> generatedDocuments
) {
return Map.of("renewal_execution", Map.of(
"scenario", scenario,
"missing_slots", missingSlots,
"requested_fields", requestedFields,
"guide_review_required", false,
"guide_review_required", guideReviewRequired,
"generated_documents", generatedDocuments
));
}
Expand Down
Loading