修复M002 V3字段矩阵迁移问题

This commit is contained in:
andy
2026-07-12 01:27:42 +08:00
parent ab303eea39
commit db95aece20
10 changed files with 330 additions and 19 deletions

View File

@@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.dao.DataIntegrityViolationException;
@@ -901,6 +902,73 @@ class SuperAgentTaskResultControllerTest {
assertThat(transitionCount).isEqualTo(1L);
}
@Test
void shouldConfirmV3BusinessTaskWithoutLegacyVisibleReason() throws Exception {
captureSourceMessage("mail-v3-confirm-without-legacy-visible-reason-001");
String body = """
{
"source_message": {
"source_message_id": "mail-v3-confirm-without-legacy-visible-reason-001",
"subject": "New booking without legacy visible reason",
"from": null,
"cc": [],
"received_at": null,
"source_channel": "Email"
},
"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-NO-VISIBLE-001",
"reservation_number": null,
"block_code": null
},
"relevant_message_excerpt": "Please create a new FIT reservation without old root visible reason.",
"attachments": [],
"file_references": [],
"context_used": {},
"extracted_fields": {
"booking_object_type": "FIT Reservation",
"arrival_date": "2026-09-01",
"departure_date": "2026-09-03",
"room_quantity": 2,
"room_type": "Deluxe King",
"pms_room_type_code": "RM2"
},
"manual_review": null
}
],
"case_candidates": [],
"extraction_warnings": [],
"unhandled_current_intents": []
}
""";
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-v3-confirm-without-visible-001"))
.andExpect(status().isCreated())
.andReturn();
String taskId = com.jayway.jsonpath.JsonPath.read(
createResult.getResponse().getContentAsString(),
"$.items[0].task_id");
mockMvc.perform(post("/api/reservation/tasks/{taskId}/confirm", taskId)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"field_values": {}
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.task_status").value("READY"))
.andExpect(jsonPath("$.confirmed_payload.field_values['relevant_message_excerpt']")
.value("Please create a new FIT reservation without old root visible reason."))
.andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.room_items.0.pms_room_type_code']")
.value("RM2"));
}
@Test
void shouldRejectEmptyV3BusinessRootWithoutPersistingBatch() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v3-empty-business-root-001");
@@ -1556,6 +1624,61 @@ class SuperAgentTaskResultControllerTest {
assertThat(auditCount).isEqualTo(1L);
}
@Test
void shouldRejectConfirmWhenReadonlyRequiredEvidenceFieldMissing() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-cp5-readonly-required-missing-001");
String body = """
{
"source_message_id": "%s",
"ai_task_results": [
{
"source_event_index": 1,
"catalog_code": "S01",
"skill_id": "S01_new_booking_skill",
"result_type": "normal_task",
"task_type": "New Booking",
"task_subtype": "new_fit_reservation",
"current_or_history": "current",
"relevant_message_excerpt": "Please handle booking message.",
"attachments": [],
"file_references": [],
"context_used": {},
"case_keys": {"confirmation_number": "CNF-CP5-READONLY-MISSING-001"},
"extracted_fields": {
"booking_object_type": "FIT Reservation",
"arrival_date": "2026-08-01",
"departure_date": "2026-08-02",
"room_quantity": 2,
"room_type": "Deluxe King",
"pms_room_type_code": "RM2"
},
"additional_operations": [],
"idempotency_key": null
}
],
"extraction_warnings": []
}
""".formatted(source.inboxId());
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-cp5-readonly-required-missing-001"))
.andExpect(status().isCreated())
.andReturn();
String taskId = com.jayway.jsonpath.JsonPath.read(
createResult.getResponse().getContentAsString(),
"$.items[0].task_id"
);
mockMvc.perform(post("/api/reservation/tasks/{taskId}/confirm", taskId)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"field_values": {}
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("TASK_FIELD_VALIDATION_FAILED"))
.andExpect(jsonPath("$.details[0]").value(containsString("visible_reason")));
}
@Test
void shouldExecuteFirstOperaSimulationOperationAndRecordAttempt() throws Exception {
String[] taskAndOperationIds = createReadyTaskWithTwoOperaOperations(
@@ -1915,6 +2038,90 @@ class SuperAgentTaskResultControllerTest {
assertThat(auditCount).isEqualTo(1L);
}
@Test
void shouldResolveTypeKnownManualReviewFromP0FieldPath() throws Exception {
captureSourceMessage("mail-v3-manual-review-field-path-001");
String body = typeKnownManualReviewBody(
"mail-v3-manual-review-field-path-001",
"CNF-V3-MR-FIELD-PATH-001",
"2026-09-02",
"2026-09-04");
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-v3-manual-review-field-path-001"))
.andExpect(status().isCreated())
.andReturn();
String taskId = com.jayway.jsonpath.JsonPath.read(
createResult.getResponse().getContentAsString(),
"$.items[0].task_id");
String orderId = com.jayway.jsonpath.JsonPath.read(
createResult.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_path": "extracted_fields.room_items.0.pms_room_type_code",
"value": "RM3"
}
]
}
""".formatted(orderId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.task_status").value("READY"))
.andExpect(jsonPath("$.review_resolution.field_overrides[0].field_pointer")
.value("/extracted_fields/room_items/0/pms_room_type_code"))
.andExpect(jsonPath("$.review_resolution.field_overrides[0].field_path")
.value("extracted_fields.room_items.0.pms_room_type_code"))
.andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.room_items.0.pms_room_type_code']")
.value("RM3"));
}
@Test
void shouldResolveTypeKnownManualReviewFromLegacyFieldPath() throws Exception {
captureSourceMessage("mail-v3-manual-review-legacy-field-path-001");
String body = typeKnownManualReviewBody(
"mail-v3-manual-review-legacy-field-path-001",
"CNF-V3-MR-LEGACY-FIELD-PATH-001",
"2026-09-02",
"2026-09-04");
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-v3-manual-review-legacy-field-path-001"))
.andExpect(status().isCreated())
.andReturn();
String taskId = com.jayway.jsonpath.JsonPath.read(
createResult.getResponse().getContentAsString(),
"$.items[0].task_id");
String orderId = com.jayway.jsonpath.JsonPath.read(
createResult.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_path": "extracted_fields.pms_room_type_code",
"value": "RM3"
}
]
}
""".formatted(orderId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.task_status").value("READY"))
.andExpect(jsonPath("$.review_resolution.field_overrides[0].field_pointer")
.value("/extracted_fields/room_items/0/pms_room_type_code"))
.andExpect(jsonPath("$.review_resolution.field_overrides[0].field_path")
.value("extracted_fields.room_items.0.pms_room_type_code"))
.andExpect(jsonPath("$.review_resolution.field_overrides[0].legacy_field_path")
.value("extracted_fields.pms_room_type_code"))
.andExpect(jsonPath("$.confirmed_payload.legacy_field_values['extracted_fields.pms_room_type_code']")
.value("RM3"));
}
@Test
void shouldRejectGenericConfirmForTypeKnownManualReview() throws Exception {
captureSourceMessage("mail-v3-manual-review-confirm-001");
@@ -2112,6 +2319,18 @@ class SuperAgentTaskResultControllerTest {
.andExpect(jsonPath("$.error_code").value("REVIEW_ORDER_ASSIGNMENT_MISMATCH"));
}
@Test
void shouldNotMarkHistoricalPayloadRowsAsP0ContractInV18Migration() throws Exception {
ClassPathResource migration = new ClassPathResource(
"db/migration/V18__update_reservation_field_contract_version.sql");
assertThat(migration.exists()).isTrue();
String sql = migration.getContentAsString(StandardCharsets.UTF_8);
assertThat(sql).contains("field_contract_version = 'code-v1'");
assertThat(sql).contains("draft_payload_json IS NULL");
assertThat(sql).contains("confirmed_payload_json IS NULL");
}
@Test
void shouldConvertFallbackToUpdateBookingAndLogicDeleteEmptyTemporaryOrder() throws Exception {
SourceMessageCaptureResult targetSource = captureSourceMessage("mail-fallback-target-order-001");