修复 M002 V3 P0 回归校验问题
This commit is contained in:
@@ -850,12 +850,19 @@ class SuperAgentTaskResultControllerTest {
|
||||
"message_events": [
|
||||
{
|
||||
"event_type": "New Booking",
|
||||
"event_role": "travel_agent_request",
|
||||
"source_event_index": "E1",
|
||||
"current_or_history": "current",
|
||||
"case_keys": {
|
||||
"group_code": null,
|
||||
"confirmation_number": "CNF-V3-NEW-001"
|
||||
"confirmation_number": "CNF-V3-NEW-001",
|
||||
"reservation_number": null,
|
||||
"block_code": null
|
||||
},
|
||||
"relevant_message_excerpt": "Please create a new FIT reservation.",
|
||||
"attachments": [],
|
||||
"file_references": [],
|
||||
"context_used": {},
|
||||
"extracted_fields": {
|
||||
"booking_object_type": "FIT Reservation",
|
||||
"arrival_date": "2026-09-01"
|
||||
@@ -942,12 +949,19 @@ class SuperAgentTaskResultControllerTest {
|
||||
"message_events": [
|
||||
{
|
||||
"event_type": "Trace",
|
||||
"event_role": "travel_agent_request",
|
||||
"source_event_index": "E2",
|
||||
"current_or_history": "current",
|
||||
"case_keys": {
|
||||
"group_code": "GRP-V3-ERR-001",
|
||||
"confirmation_number": null
|
||||
"confirmation_number": null,
|
||||
"reservation_number": null,
|
||||
"block_code": null
|
||||
},
|
||||
"relevant_message_excerpt": "Please add a trace note.",
|
||||
"attachments": [],
|
||||
"file_references": [],
|
||||
"context_used": {},
|
||||
"extracted_fields": {},
|
||||
"manual_review": null
|
||||
}
|
||||
@@ -1000,12 +1014,19 @@ class SuperAgentTaskResultControllerTest {
|
||||
"message_events": [
|
||||
{
|
||||
"event_type": "%s",
|
||||
"event_role": "travel_agent_request",
|
||||
"source_event_index": "E1",
|
||||
"current_or_history": "current",
|
||||
"case_keys": {
|
||||
"group_code": "%s",
|
||||
"confirmation_number": null
|
||||
"confirmation_number": null,
|
||||
"reservation_number": null,
|
||||
"block_code": null
|
||||
},
|
||||
"relevant_message_excerpt": "Unsupported event with explicit contract error.",
|
||||
"attachments": [],
|
||||
"file_references": [],
|
||||
"context_used": {},
|
||||
"extracted_fields": {},
|
||||
"contract_errors": [
|
||||
{
|
||||
@@ -1017,12 +1038,19 @@ class SuperAgentTaskResultControllerTest {
|
||||
},
|
||||
{
|
||||
"event_type": "New Booking",
|
||||
"event_role": "travel_agent_request",
|
||||
"source_event_index": "E2",
|
||||
"current_or_history": "current",
|
||||
"case_keys": {
|
||||
"group_code": null,
|
||||
"confirmation_number": "CNF-V3-SIBLING-001"
|
||||
"confirmation_number": "CNF-V3-SIBLING-001",
|
||||
"reservation_number": null,
|
||||
"block_code": null
|
||||
},
|
||||
"relevant_message_excerpt": "Please create sibling FIT reservation.",
|
||||
"attachments": [],
|
||||
"file_references": [],
|
||||
"context_used": {},
|
||||
"extracted_fields": {
|
||||
"booking_object_type": "FIT Reservation"
|
||||
},
|
||||
@@ -1073,12 +1101,19 @@ class SuperAgentTaskResultControllerTest {
|
||||
"message_events": [
|
||||
{
|
||||
"event_type": "Trace",
|
||||
"event_role": "travel_agent_request",
|
||||
"source_event_index": "E1",
|
||||
"current_or_history": "current",
|
||||
"case_keys": {
|
||||
"group_code": "GRP-V3-FRONTEND-BLOCK-001",
|
||||
"confirmation_number": null
|
||||
"confirmation_number": null,
|
||||
"reservation_number": null,
|
||||
"block_code": null
|
||||
},
|
||||
"relevant_message_excerpt": "Trace event has explicit contract error.",
|
||||
"attachments": [],
|
||||
"file_references": [],
|
||||
"context_used": {},
|
||||
"extracted_fields": {},
|
||||
"contract_errors": [
|
||||
{
|
||||
@@ -1090,12 +1125,19 @@ class SuperAgentTaskResultControllerTest {
|
||||
},
|
||||
{
|
||||
"event_type": "New Booking",
|
||||
"event_role": "travel_agent_request",
|
||||
"source_event_index": "E2",
|
||||
"current_or_history": "current",
|
||||
"case_keys": {
|
||||
"group_code": null,
|
||||
"confirmation_number": "CNF-V3-FRONTEND-BLOCK-001"
|
||||
"confirmation_number": "CNF-V3-FRONTEND-BLOCK-001",
|
||||
"reservation_number": null,
|
||||
"block_code": null
|
||||
},
|
||||
"relevant_message_excerpt": "Please create frontend-visible FIT reservation.",
|
||||
"attachments": [],
|
||||
"file_references": [],
|
||||
"context_used": {},
|
||||
"extracted_fields": {
|
||||
"booking_object_type": "FIT Reservation"
|
||||
},
|
||||
@@ -1185,12 +1227,19 @@ class SuperAgentTaskResultControllerTest {
|
||||
"message_events": [
|
||||
{
|
||||
"event_type": "New Booking",
|
||||
"event_role": "travel_agent_request",
|
||||
"source_event_index": "E1",
|
||||
"current_or_history": "current",
|
||||
"case_keys": {
|
||||
"group_code": null,
|
||||
"confirmation_number": "CNF-V3-MR-001"
|
||||
"confirmation_number": "CNF-V3-MR-001",
|
||||
"reservation_number": null,
|
||||
"block_code": null
|
||||
},
|
||||
"relevant_message_excerpt": "Manual review payload is incomplete.",
|
||||
"attachments": [],
|
||||
"file_references": [],
|
||||
"context_used": {},
|
||||
"extracted_fields": {
|
||||
"booking_object_type": "FIT Reservation"
|
||||
},
|
||||
@@ -2456,18 +2505,26 @@ class SuperAgentTaskResultControllerTest {
|
||||
"message_events": [
|
||||
{
|
||||
"event_type": "New Booking",
|
||||
"event_role": "travel_agent_request",
|
||||
"source_event_index": "E1",
|
||||
"current_or_history": "current",
|
||||
"case_keys": {
|
||||
"group_code": null,
|
||||
"confirmation_number": "%s"
|
||||
"confirmation_number": "%s",
|
||||
"reservation_number": null,
|
||||
"block_code": null
|
||||
},
|
||||
"relevant_message_excerpt": "Please create a new FIT reservation with manual room type review.",
|
||||
"attachments": [],
|
||||
"file_references": [],
|
||||
"context_used": {},
|
||||
"extracted_fields": {
|
||||
"booking_object_type": "FIT Reservation",
|
||||
"arrival_date": "%s",
|
||||
"departure_date": "%s",
|
||||
"room_quantity": 2,
|
||||
"room_type": "Deluxe King"
|
||||
"room_type": "Deluxe King",
|
||||
"pms_room_type_code": null
|
||||
},
|
||||
"manual_review": {
|
||||
"review_record_type": "business_event_review",
|
||||
|
||||
@@ -158,6 +158,24 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
.andExpect(jsonPath("$.result_type").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTypedInfrastructureInputErrorWhenV3ShapeMissesSourceMessageObject() throws Exception {
|
||||
mockMvc.perform(signedPost("""
|
||||
{
|
||||
"body_current": "Please cancel group HD260710A",
|
||||
"message_events": [],
|
||||
"case_candidates": [],
|
||||
"extraction_warnings": [],
|
||||
"unhandled_current_intents": []
|
||||
}
|
||||
""", "nonce-p0-missing-source-object-001"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.result_type").value("infrastructure_input_error"))
|
||||
.andExpect(jsonPath("$.error_code").value("missing_source_message_id"))
|
||||
.andExpect(jsonPath("$.retryable").value(true))
|
||||
.andExpect(jsonPath("$.missing_fields[0]").value("source_message.source_message_id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLetNonBlankSourceIdentityPassP0GuardWithoutTypedMissingError() throws Exception {
|
||||
JsonNode validInput = caseItem(
|
||||
@@ -197,9 +215,57 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
.isEqualTo("Need Manual Review");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectS99WhenMainAgentReviewHasUnexpectedField() throws Exception {
|
||||
JsonNode s99 = caseValue(fixture("main_outcomes.json").path("cases"), "legal_s99_material_unavailable");
|
||||
ObjectNode copy = s99.deepCopy();
|
||||
((ObjectNode) copy.path("manual_review")).put("unexpected_field", "not-allowed");
|
||||
String externalId = "p0-s99-unexpected-review-field-001";
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalId);
|
||||
|
||||
mockMvc.perform(signedPost(withSourceMessageId(copy, externalId), "nonce-p0-s99-extra-field-001"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("ADAPTER_CONTRACT_ERROR"));
|
||||
|
||||
Long taskCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_task
|
||||
WHERE source_message_id = ?
|
||||
""", Long.class, source.inboxId());
|
||||
assertThat(taskCount).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectV3BusinessRootWhenP0RootArraysAreMissing() throws Exception {
|
||||
ObjectNode root = objectMapper.createObjectNode();
|
||||
ObjectNode sourceMessage = root.putObject("source_message");
|
||||
String externalId = "p0-root-arrays-missing-001";
|
||||
sourceMessage.put("source_message_id", externalId);
|
||||
sourceMessage.put("subject", "P0 fixture");
|
||||
sourceMessage.put("from", "agent@example.test");
|
||||
sourceMessage.putArray("cc");
|
||||
sourceMessage.put("received_at", "2026-07-11T10:00:00+08:00");
|
||||
sourceMessage.put("source_channel", "Email");
|
||||
root.putArray("message_events")
|
||||
.add(fixture("row_multiple_derived.json").path("message_events").get(0));
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalId);
|
||||
|
||||
mockMvc.perform(signedPost(objectMapper.writeValueAsString(root), "nonce-p0-root-arrays-missing-001"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("ADAPTER_CONTRACT_ERROR"));
|
||||
|
||||
Long taskCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_task
|
||||
WHERE source_message_id = ?
|
||||
""", Long.class, source.inboxId());
|
||||
assertThat(taskCount).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateParentSplitTasksFromP0FixtureInEventOrder() throws Exception {
|
||||
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
|
||||
useParentSplitBusinessKeys(root, "PARENT-2608-OK", "CHILD-2608-OK-A", "CHILD-2608-OK-B");
|
||||
String externalId = "p0-parent-split-001";
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalId);
|
||||
|
||||
@@ -234,6 +300,33 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
assertThat(transitionCount).isEqualTo(3L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailClosedWhenParentSplitReferencesDoNotMatchChildEvents() throws Exception {
|
||||
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
|
||||
useParentSplitBusinessKeys(root, "PARENT-2608-MIS", "CHILD-2608-MIS-A", "CHILD-2608-MIS-B");
|
||||
ObjectNode parentEvent = (ObjectNode) root.path("message_events").get(2);
|
||||
((ArrayNode) parentEvent.path("related_source_event_indices"))
|
||||
.set(1, objectMapper.getNodeFactory().textNode("E_UNKNOWN_CHILD"));
|
||||
String externalId = "p0-parent-split-mismatch-001";
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalId);
|
||||
|
||||
mockMvc.perform(signedPost(withSourceMessageId(root, externalId), "nonce-p0-parent-split-mismatch-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.accepted_count").value(3))
|
||||
.andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
|
||||
.andExpect(jsonPath("$.items[1].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
|
||||
.andExpect(jsonPath("$.items[2].adapter_error_code")
|
||||
.value("LINKED_PARENT_RELEASE_CONTRACT_INCOMPLETE"))
|
||||
.andExpect(jsonPath("$.items[2].task_id").doesNotExist());
|
||||
|
||||
Long taskCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_task
|
||||
WHERE source_message_id = ?
|
||||
""", Long.class, source.inboxId());
|
||||
assertThat(taskCount).isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateRowDerivedMainParentAndTraceTasksFromP0Fixture() throws Exception {
|
||||
ObjectNode root = fixture("row_multiple_derived.json").deepCopy();
|
||||
@@ -255,6 +348,27 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
assertThat(taskCount).isEqualTo(3L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailClosedWhenV3BusinessEventMissesRequiredField() throws Exception {
|
||||
ObjectNode event = fixture("row_multiple_derived.json").path("message_events").get(0).deepCopy();
|
||||
event.remove("attachments");
|
||||
String externalId = "p0-event-required-field-missing-001";
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalId);
|
||||
|
||||
mockMvc.perform(signedPost(businessRoot(externalId, event), "nonce-p0-event-required-missing-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.accepted_count").value(1))
|
||||
.andExpect(jsonPath("$.items[0].adapter_error_code").value("EVENT_CONTRACT_INCOMPLETE"))
|
||||
.andExpect(jsonPath("$.items[0].task_id").doesNotExist());
|
||||
|
||||
Long taskCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_task
|
||||
WHERE source_message_id = ?
|
||||
""", Long.class, source.inboxId());
|
||||
assertThat(taskCount).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreserveMixedAllotmentUnhandledIntentAndRejectPartialAllotmentAsBusinessTask() throws Exception {
|
||||
JsonNode fixture = fixture("allotment_scope.json");
|
||||
@@ -286,6 +400,7 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
void shouldCreateTypeKnownManualReviewAndResolveSameCardFromP0Fixture() throws Exception {
|
||||
JsonNode manualReview = fixture("manual_review_resolution.json");
|
||||
ObjectNode event = manualReview.path("known_subtype_manual_review").path("event").deepCopy();
|
||||
useManualReviewGroupCode(event, "CHILD-SUITE-RESOLVE-001");
|
||||
String externalId = "p0-manual-review-same-card-001";
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalId);
|
||||
String body = businessRoot(externalId, event);
|
||||
@@ -347,6 +462,69 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
assertThat(aiPayloadJson).doesNotContain("\"pms_room_type_code\":\"SU1\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailClosedWhenManualReviewMissingFieldPointerDoesNotResolve() throws Exception {
|
||||
ObjectNode event = fixture("manual_review_resolution.json")
|
||||
.path("known_subtype_manual_review")
|
||||
.path("event")
|
||||
.deepCopy();
|
||||
((ArrayNode) event.path("manual_review").path("missing_fields"))
|
||||
.set(0, objectMapper.getNodeFactory()
|
||||
.textNode("/extracted_fields/room_items/99/pms_room_type_code"));
|
||||
String externalId = "p0-manual-review-invalid-pointer-001";
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalId);
|
||||
|
||||
mockMvc.perform(signedPost(businessRoot(externalId, event), "nonce-p0-manual-review-invalid-pointer-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.accepted_count").value(1))
|
||||
.andExpect(jsonPath("$.items[0].adapter_error_code")
|
||||
.value("MANUAL_REVIEW_MISSING_FIELD_POINTER_INVALID"))
|
||||
.andExpect(jsonPath("$.items[0].task_id").doesNotExist());
|
||||
|
||||
Long taskCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_task
|
||||
WHERE source_message_id = ?
|
||||
""", Long.class, source.inboxId());
|
||||
assertThat(taskCount).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectOutOfRangeRoomItemPointerWhenResolvingManualReview() throws Exception {
|
||||
JsonNode manualReview = fixture("manual_review_resolution.json");
|
||||
ObjectNode event = manualReview.path("known_subtype_manual_review").path("event").deepCopy();
|
||||
useManualReviewGroupCode(event, "CHILD-SUITE-POINTER-001");
|
||||
String externalId = "p0-manual-review-resolution-pointer-001";
|
||||
captureSourceMessage(externalId);
|
||||
MvcResult result = mockMvc.perform(signedPost(
|
||||
businessRoot(externalId, event),
|
||||
"nonce-p0-manual-review-resolution-pointer-create-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String taskId = com.jayway.jsonpath.JsonPath.read(
|
||||
result.getResponse().getContentAsString(),
|
||||
"$.items[0].task_id");
|
||||
String orderId = com.jayway.jsonpath.JsonPath.read(
|
||||
result.getResponse().getContentAsString(),
|
||||
"$.items[0].order_id");
|
||||
|
||||
mockMvc.perform(post("/api/reservation/tasks/{taskId}/manual-review-resolutions", taskId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"confirmed_order_id": "%s",
|
||||
"field_overrides": [
|
||||
{
|
||||
"field_pointer": "/extracted_fields/room_items/99/pms_room_type_code",
|
||||
"value": "SU1"
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(orderId)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("TASK_REVIEW_POINTER_INVALID"));
|
||||
}
|
||||
|
||||
private JsonNode fixture(String fileName) throws Exception {
|
||||
return objectMapper.readTree(FIXTURES_DIR.resolve(fileName).toFile());
|
||||
}
|
||||
@@ -397,6 +575,36 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
return objectMapper.writeValueAsString(root);
|
||||
}
|
||||
|
||||
private void useParentSplitBusinessKeys(
|
||||
ObjectNode root,
|
||||
String parentGroupCode,
|
||||
String childGroupCodeOne,
|
||||
String childGroupCodeTwo) {
|
||||
ArrayNode events = (ArrayNode) root.path("message_events");
|
||||
useEventGroupCode((ObjectNode) events.get(0), childGroupCodeOne);
|
||||
useEventGroupCode((ObjectNode) events.get(1), childGroupCodeTwo);
|
||||
((ObjectNode) events.get(0).path("extracted_fields")).put("parent_group_code", parentGroupCode);
|
||||
((ObjectNode) events.get(1).path("extracted_fields")).put("parent_group_code", parentGroupCode);
|
||||
|
||||
ObjectNode parentEvent = (ObjectNode) events.get(2);
|
||||
useEventGroupCode(parentEvent, parentGroupCode);
|
||||
ObjectNode parentExtractedFields = (ObjectNode) parentEvent.path("extracted_fields");
|
||||
parentExtractedFields.put("parent_group_code", parentGroupCode);
|
||||
ArrayNode childGroupCodes = (ArrayNode) parentExtractedFields.path("child_group_codes");
|
||||
childGroupCodes.removeAll();
|
||||
childGroupCodes.add(childGroupCodeOne);
|
||||
childGroupCodes.add(childGroupCodeTwo);
|
||||
}
|
||||
|
||||
private void useManualReviewGroupCode(ObjectNode event, String groupCode) {
|
||||
useEventGroupCode(event, groupCode);
|
||||
((ObjectNode) event.path("manual_review").path("known_fields")).put("group_code", groupCode);
|
||||
}
|
||||
|
||||
private void useEventGroupCode(ObjectNode event, String groupCode) {
|
||||
((ObjectNode) event.path("case_keys")).put("group_code", groupCode);
|
||||
}
|
||||
|
||||
private SourceMessageCaptureResult captureSourceMessage(String externalMessageId) {
|
||||
return captureService.capture(new CaptureSourceMessageCommand(
|
||||
"HOTEL-TEST",
|
||||
|
||||
Reference in New Issue
Block a user