修正 SuperAgent 来源消息 ID 契约
This commit is contained in:
@@ -72,7 +72,7 @@ class ReservationAiQueryControllerTest {
|
||||
"target_key_source": "body_current",
|
||||
"body_thread_used_only_as_evidence": false
|
||||
}
|
||||
""".formatted(source.inboxId());
|
||||
""".formatted("mail-ai-query-case-001");
|
||||
|
||||
mockMvc.perform(signedPost(CASE_CONTEXT_ENDPOINT, body, "nonce-ai-query-case-001", "req-ai-query-case-001")
|
||||
.header("X-AI-Trace-Id", "trace-ai-query-case-001"))
|
||||
@@ -108,7 +108,7 @@ class ReservationAiQueryControllerTest {
|
||||
"source_event_index": 1,
|
||||
"group_code": "GRP-AIQUERY-NOT-FOUND"
|
||||
}
|
||||
""".formatted(source.inboxId());
|
||||
""".formatted("mail-ai-query-empty-001");
|
||||
|
||||
mockMvc.perform(signedPost(CASE_CONTEXT_ENDPOINT, body, "nonce-ai-query-empty-001", "req-ai-query-empty-001"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -132,7 +132,7 @@ class ReservationAiQueryControllerTest {
|
||||
"source_event_index": 1,
|
||||
"reservation_no": "RESV-AIQUERY-001"
|
||||
}
|
||||
""".formatted(source.inboxId());
|
||||
""".formatted("mail-ai-query-reservation-no-001");
|
||||
|
||||
mockMvc.perform(signedPost(CASE_CONTEXT_ENDPOINT, body, "nonce-ai-query-reservation-no-001",
|
||||
"req-ai-query-reservation-no-001"))
|
||||
@@ -165,6 +165,27 @@ class ReservationAiQueryControllerTest {
|
||||
.andExpect(jsonPath("$.data.target_object_validation.status").value("single"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreExternalSourceMessageIdInGlobalCaseContextQuery() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-ai-query-external-001");
|
||||
insertActiveGroupOrder(920000000000000601L, source.inboxId(), "GRP-AIQUERY-EXTERNAL-001");
|
||||
String body = """
|
||||
{
|
||||
"hotel_id": "HOTEL-TEST",
|
||||
"source_message_id": "mail-ai-query-external-001",
|
||||
"source_event_index": -1,
|
||||
"group_code": "GRP-AIQUERY-EXTERNAL-001"
|
||||
}
|
||||
""";
|
||||
|
||||
mockMvc.perform(signedPost(CASE_CONTEXT_ENDPOINT, body, "nonce-ai-query-external-001",
|
||||
"req-ai-query-external-001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.matched_order_records[0].object_id").value("ORDER:920000000000000601"))
|
||||
.andExpect(jsonPath("$.data.target_object_validation.status").value("single"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCaseContextWhenHmacSignatureInvalid() throws Exception {
|
||||
String body = """
|
||||
|
||||
@@ -108,7 +108,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
|
||||
mockMvc.perform(signedPost(body, "nonce-new-booking-temp-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.source_message_id").value(source.inboxId().toString()))
|
||||
.andExpect(jsonPath("$.source_message_id").value("mail-new-booking-temp-001"))
|
||||
.andExpect(jsonPath("$.idempotent_replay").value(false))
|
||||
.andExpect(jsonPath("$.accepted_count").value(1))
|
||||
.andExpect(jsonPath("$.items[0].source_event_index").value(1))
|
||||
@@ -145,6 +145,42 @@ class SuperAgentTaskResultControllerTest {
|
||||
org.assertj.core.api.Assertions.assertThat(taskCardCount).isGreaterThanOrEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveTaskResultSourceMessageByExternalMessageId() throws Exception {
|
||||
String externalMessageId = "mail-external-source-id-001";
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalMessageId);
|
||||
String body = minimalBodyWithHotel(externalMessageId, "New Booking", "normal_task", "new_fit_reservation", """
|
||||
"case_keys": {},
|
||||
"extracted_fields": {}
|
||||
""");
|
||||
|
||||
mockMvc.perform(signedPost(body, "nonce-external-source-id-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.source_message_id").value(externalMessageId))
|
||||
.andExpect(jsonPath("$.accepted_count").value(1))
|
||||
.andExpect(jsonPath("$.items[0].task_status").value("PENDING_CONFIRM"));
|
||||
|
||||
Long transitionCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_ai_transition
|
||||
WHERE source_message_id = ?
|
||||
""", Long.class, source.inboxId());
|
||||
assertThat(transitionCount).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectExternalSourceMessageIdWhenHotelIdMissing() throws Exception {
|
||||
String body = minimalBody("mail-external-without-hotel-001", "New Booking", "normal_task", "new_fit_reservation", """
|
||||
"case_keys": {},
|
||||
"extracted_fields": {}
|
||||
""");
|
||||
|
||||
mockMvc.perform(signedPost(body, "nonce-external-source-without-hotel-001"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ID_REQUIRED"))
|
||||
.andExpect(jsonPath("$.message").value("使用外部 source_message_id 时 hotel_id 不能为空。"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnIdempotentReplayForSameBodyWithNewNonce() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-idempotent-replay-001");
|
||||
@@ -1125,6 +1161,40 @@ class SuperAgentTaskResultControllerTest {
|
||||
""".formatted(sourceMessageId, resultType, taskType, taskSubtype, itemFields);
|
||||
}
|
||||
|
||||
private String minimalBodyWithHotel(
|
||||
String sourceMessageId,
|
||||
String taskType,
|
||||
String resultType,
|
||||
String taskSubtype,
|
||||
String itemFields) {
|
||||
return """
|
||||
{
|
||||
"hotel_id": "HOTEL-TEST",
|
||||
"source_message_id": "%s",
|
||||
"ai_task_results": [
|
||||
{
|
||||
"source_event_index": 1,
|
||||
"catalog_code": "S01",
|
||||
"skill_id": "S01_new_booking_skill",
|
||||
"result_type": "%s",
|
||||
"task_type": "%s",
|
||||
"task_subtype": "%s",
|
||||
"current_or_history": "current",
|
||||
"visible_reason": "AI extracted a task result.",
|
||||
"relevant_message_excerpt": "Please handle booking message.",
|
||||
"attachments": [],
|
||||
"file_references": [],
|
||||
"context_used": {},
|
||||
%s,
|
||||
"additional_operations": [],
|
||||
"idempotency_key": null
|
||||
}
|
||||
],
|
||||
"extraction_warnings": []
|
||||
}
|
||||
""".formatted(sourceMessageId, resultType, taskType, taskSubtype, itemFields);
|
||||
}
|
||||
|
||||
private String twoTaskBody(String sourceMessageId, String groupCode) {
|
||||
return """
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user