实现M002 V3字段控件契约后端第一版

This commit is contained in:
andy
2026-07-13 07:44:19 +08:00
parent 50180f88cb
commit 890565d43f
9 changed files with 907 additions and 33 deletions

View File

@@ -96,6 +96,13 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
.andExpect(jsonPath("$.items[0].system_task_type").value("SOURCE_MESSAGE_ONLY"))
.andReturn();
String s10TaskId = com.jayway.jsonpath.JsonPath.read(
s10Result.getResponse().getContentAsString(),
"$.items[0].task_id");
mockMvc.perform(get("/api/reservation/tasks/{taskId}", s10TaskId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.fields.length()").value(0));
assertSourceMessageOnlyTaskCount(s10Result, "S10");
assertSourceMessageOnlyTaskCount(s99Result, "S99");
}
@@ -407,6 +414,92 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
assertThat(parentReviewTaskCount).isEqualTo(1L);
}
@Test
void shouldResolveParentKeyConflictManualReviewWithGroupCodeAlias() throws Exception {
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
useParentSplitBusinessKeys(root, "PARENT-2608-RESOLVE", "CHILD-2608-RESOLVE-A", "CHILD-2608-RESOLVE-B");
convertParentSplitRootToP01(root);
ObjectNode parentEvent = (ObjectNode) root.path("message_events").get(2);
ObjectNode parentCaseKeys = (ObjectNode) parentEvent.path("case_keys");
parentCaseKeys.putNull("group_code");
parentCaseKeys.putNull("block_code");
ObjectNode contextUsed = (ObjectNode) parentEvent.path("context_used");
ArrayNode candidates = contextUsed.putArray("parent_identity_candidates");
candidates.addObject()
.put("field", "group_code")
.put("value", "PARENT-2608-RESOLVE-A")
.put("evidence_source", "subject");
candidates.addObject()
.put("field", "block_code")
.put("value", "PARENT-2608-RESOLVE-B")
.put("evidence_source", "attachment");
ObjectNode manualReview = parentEvent.putObject("manual_review");
manualReview.put("reason_code", "target_object_unclear");
manualReview.put("visible_reason", "Parent Group 的 group_code 与 block_code 原始候选冲突,请人工确认目标。");
manualReview.put("review_record_type", "business_event_review");
manualReview.putArray("missing_fields")
.add("/case_keys/group_code")
.add("/case_keys/block_code");
manualReview.putArray("blocking_points")
.add("Parent Group identity cannot be safely normalized.");
manualReview.putArray("conflicting_points")
.add("PARENT-2608-RESOLVE-A")
.add("PARENT-2608-RESOLVE-B");
manualReview.putArray("suggested_human_actions")
.add("confirm_parent_group_identity");
manualReview.putArray("evidence_to_check")
.add("parent_identity_candidates");
manualReview.putObject("known_fields");
String externalId = "p0-parent-split-key-conflict-resolve-001";
captureSourceMessage(externalId);
MvcResult createResult = mockMvc.perform(signedPost(
withSourceMessageId(root, externalId),
"nonce-p0-parent-key-conflict-resolve-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.items[2].route_code").value("R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_REVIEW"))
.andReturn();
String taskId = com.jayway.jsonpath.JsonPath.read(
createResult.getResponse().getContentAsString(),
"$.items[2].task_id");
String orderId = com.jayway.jsonpath.JsonPath.read(
createResult.getResponse().getContentAsString(),
"$.items[2].order_id");
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.group_code')].field_pointer")
.value(contains("/case_keys/group_code")));
mockMvc.perform(post("/api/reservation/tasks/{taskId}/manual-review-resolutions", taskId)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"confirmed_order_id": "%s",
"reason": "确认 Parent Group 目标。",
"field_overrides": [
{
"field_pointer": "/case_keys/group_code",
"value": "PARENT-2608-RESOLVED"
}
]
}
""".formatted(orderId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.task_status").value("READY"))
.andExpect(jsonPath("$.review_status").value("RESOLVED"))
.andExpect(jsonPath("$.review_resolution.field_overrides[0].field_pointer")
.value("/case_keys/group_code"))
.andExpect(jsonPath("$.review_resolution.field_overrides[0].field_path")
.value("case_keys.group_code"))
.andExpect(jsonPath("$.confirmed_payload.field_values['case_keys.group_code']")
.value("PARENT-2608-RESOLVED"))
.andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.cancel_object_type']")
.value("allotment_control_block"))
.andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.cancel_scope']")
.value("entire_allotment_control_block"));
}
@Test
void shouldFailClosedWhenCurrentProducerUsesLegacyParentCancelBooking() throws Exception {
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
@@ -590,12 +683,67 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
.andExpect(status().isOk())
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_quantity')].field_pointer")
.value(contains("/extracted_fields/room_items/0/room_quantity")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_quantity')].control_type")
.value(contains("number")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_quantity')].edit_scope")
.value(contains("normal_task")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_quantity')].write_target")
.value(contains("draft_payload.field_values")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_quantity')].options_source")
.value(contains("none")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_quantity')].raw_readonly")
.value(contains(false)))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_type_raw')].field_pointer")
.value(contains("/extracted_fields/room_items/0/room_type_raw")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_type_raw')].control_type")
.value(contains("readonly")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_type_raw')].edit_scope")
.value(contains("never")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_type_raw')].raw_readonly")
.value(contains(true)))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].field_pointer")
.value(contains("/extracted_fields/room_items/0/pms_room_type_code")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].control_type")
.value(contains("select")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].options_source")
.value(contains("active_pms_room_type_catalog")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].value")
.value(contains("RM2")));
.value(contains("RM2")))
.andExpect(jsonPath("$.fields[?(@.field_path=='relevant_message_excerpt')].control_type")
.value(contains("readonly")))
.andExpect(jsonPath("$.fields[?(@.field_path=='relevant_message_excerpt')].edit_scope")
.value(contains("never")))
.andExpect(jsonPath("$.fields[?(@.field_path=='relevant_message_excerpt')].write_target")
.value(contains("none")))
.andExpect(jsonPath("$.fields[?(@.field_path=='relevant_message_excerpt')].raw_readonly")
.value(contains(true)));
mockMvc.perform(put("/api/reservation/tasks/{taskId}/draft", taskId)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"field_values": {
"extracted_fields.room_items.0.room_type_raw": "SHOULD-NOT-OVERWRITE-RAW",
"extracted_fields.room_items.0.room_quantity": 5
}
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("TASK_FIELD_VALIDATION_FAILED"))
.andExpect(jsonPath("$.details[0]").value(containsString("extracted_fields.room_items.0.room_type_raw")));
mockMvc.perform(put("/api/reservation/tasks/{taskId}/draft", taskId)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"field_values": {
"extracted_fields.room_type": "SHOULD-NOT-OVERWRITE-LEGACY-RAW"
}
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("TASK_FIELD_VALIDATION_FAILED"))
.andExpect(jsonPath("$.details[0]").value(containsString("extracted_fields.room_items.0.room_type_raw")));
mockMvc.perform(put("/api/reservation/tasks/{taskId}/draft", taskId)
.contentType(MediaType.APPLICATION_JSON)
@@ -708,9 +856,59 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
.andExpect(status().isOk())
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].field_pointer")
.value(contains("/extracted_fields/room_items/0/pms_room_type_code")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].control_type")
.value(contains("select")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].edit_scope")
.value(contains("manual_review_only")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].write_target")
.value(contains("review_resolution.field_overrides")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].options_source")
.value(contains("active_pms_room_type_catalog")))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].raw_readonly")
.value(contains(false)))
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_type_raw')].value")
.value(contains("SUITE")));
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/0/room_type_raw",
"value": "SHOULD-NOT-OVERWRITE-RAW"
}
]
}
""".formatted(orderId)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("TASK_REVIEW_POINTER_INVALID"))
.andExpect(jsonPath("$.details[0]")
.value(containsString("/extracted_fields/room_items/0/room_type_raw")));
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/0/pms_room_type_code",
"value": "SU1"
},
{
"field_pointer": "/extracted_fields/room_items/0/room_type_raw",
"value": "SHOULD-NOT-OVERWRITE-RAW"
}
]
}
""".formatted(orderId)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("TASK_REVIEW_POINTER_INVALID"))
.andExpect(jsonPath("$.details[0]")
.value(containsString("/extracted_fields/room_items/0/room_type_raw")));
mockMvc.perform(post("/api/reservation/tasks/{taskId}/manual-review-resolutions", taskId)
.contentType(MediaType.APPLICATION_JSON)
.content("""
@@ -758,6 +956,45 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
assertThat(aiPayloadJson).doesNotContain("\"pms_room_type_code\":\"SU1\"");
}
@Test
void shouldRejectBlockCodeAliasOutsideCancelAllotmentManualReview() throws Exception {
JsonNode manualReview = fixture("manual_review_resolution.json");
ObjectNode event = manualReview.path("known_subtype_manual_review").path("event").deepCopy();
useManualReviewGroupCode(event, "CHILD-SUITE-BLOCK-ALIAS-001");
String externalId = "p0-manual-review-block-alias-scope-001";
captureSourceMessage(externalId);
MvcResult createResult = mockMvc.perform(signedPost(
businessRoot(externalId, event),
"nonce-p0-manual-review-block-alias-scope-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_REVIEW"))
.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_pointer": "/case_keys/block_code",
"value": "SHOULD-NOT-BECOME-GROUP-CODE"
}
]
}
""".formatted(orderId)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("TASK_REVIEW_POINTER_INVALID"))
.andExpect(jsonPath("$.details[0]").value(containsString("/case_keys/block_code")));
}
@Test
void shouldFailClosedWhenManualReviewMissingFieldPointerDoesNotResolve() throws Exception {
ObjectNode event = fixture("manual_review_resolution.json")