实现 SuperAgent 查询上下文接口

This commit is contained in:
andy
2026-07-07 19:56:49 +08:00
parent bd7b791bee
commit 652c5c10c5
19 changed files with 2066 additions and 4 deletions

View File

@@ -0,0 +1,227 @@
package cn.nianxx.thhotel.workflows.reservation.control;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import cn.nianxx.thhotel.ThHotelApplication;
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
import java.time.Instant;
import java.util.List;
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.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest(classes = ThHotelApplication.class)
@AutoConfigureMockMvc
@ActiveProfiles("test")
class ReservationAiQueryControllerTest {
private static final String HOTEL_ID = "HOTEL-TEST";
@Autowired
private MockMvc mockMvc;
@Autowired
private SourceMessageCaptureService captureService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void shouldReturnCaseContextWithMatchedOrderAndPendingTask() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-ai-query-case-001");
insertActiveGroupOrder(920000000000000101L, source.inboxId(), "GRP-AIQUERY-001");
insertTransition(920000000000000201L, source.inboxId(), 1, "GRP-AIQUERY-001");
insertTask(920000000000000301L, 920000000000000101L, source.inboxId(), 920000000000000201L, "PENDING_CONFIRM");
mockMvc.perform(post("/api/ai-query/v1/case-context")
.contentType(MediaType.APPLICATION_JSON)
.header("X-Request-Id", "req-ai-query-case-001")
.header("X-AI-Trace-Id", "trace-ai-query-case-001")
.content("""
{
"hotel_id": "HOTEL-TEST",
"source_message_id": "%s",
"source_event_index": 1,
"group_code": "GRP-AIQUERY-001",
"target_key_source": "body_current",
"body_thread_used_only_as_evidence": false
}
""".formatted(source.inboxId())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.request_id").value("req-ai-query-case-001"))
.andExpect(jsonPath("$.trace_id").value("trace-ai-query-case-001"))
.andExpect(jsonPath("$.data.matched_order_records[0].object_id").value("ORDER:920000000000000101"))
.andExpect(jsonPath("$.data.matched_order_records[0].object_type").value("group_block"))
.andExpect(jsonPath("$.data.matched_order_records[0].order_id").value("920000000000000101"))
.andExpect(jsonPath("$.data.matched_order_records[0].group_code").value("GRP-AIQUERY-001"))
.andExpect(jsonPath("$.data.pending_or_open_tasks[0].task_id").value("920000000000000301"))
.andExpect(jsonPath("$.data.pending_or_open_tasks[0].source_event_index").value(1))
.andExpect(jsonPath("$.data.pending_or_open_tasks[0].task_status").value("PENDING_CONFIRM"))
.andExpect(jsonPath("$.data.active_workflows.length()").value(0))
.andExpect(jsonPath("$.data.terminated_records.length()").value(0))
.andExpect(jsonPath("$.data.target_object_validation.status").value("conflict"))
.andExpect(jsonPath("$.data.target_object_validation.can_create_new_booking_task").value(false))
.andExpect(jsonPath("$.data.target_object_validation.can_create_update_task").value(false))
.andExpect(jsonPath("$.data.target_object_validation.needs_manual_review_reason").value("OPEN_TASK_EXISTS"))
.andExpect(jsonPath("$.data.key_relationships.group_code_and_confirmation_same_object").value(nullValue()))
.andExpect(content().string(not(containsString("Please handle booking message"))));
}
@Test
void shouldReturnSuccessfulEmptyCaseContextWhenNoObjectMatches() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-ai-query-empty-001");
mockMvc.perform(post("/api/ai-query/v1/case-context")
.contentType(MediaType.APPLICATION_JSON)
.header("X-Request-Id", "req-ai-query-empty-001")
.content("""
{
"hotel_id": "HOTEL-TEST",
"source_message_id": "%s",
"source_event_index": 1,
"group_code": "GRP-AIQUERY-NOT-FOUND"
}
""".formatted(source.inboxId())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.matched_order_records.length()").value(0))
.andExpect(jsonPath("$.data.pending_or_open_tasks.length()").value(0))
.andExpect(jsonPath("$.data.active_workflows.length()").value(0))
.andExpect(jsonPath("$.data.terminated_records.length()").value(0))
.andExpect(jsonPath("$.data.target_object_validation.status").value("none"))
.andExpect(jsonPath("$.data.target_object_validation.can_create_new_booking_task").value(true));
}
@Test
void shouldNotAllowCreationWhenOnlyUnsupportedReservationNoProvided() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-ai-query-reservation-no-001");
mockMvc.perform(post("/api/ai-query/v1/case-context")
.contentType(MediaType.APPLICATION_JSON)
.header("X-Request-Id", "req-ai-query-reservation-no-001")
.content("""
{
"hotel_id": "HOTEL-TEST",
"source_message_id": "%s",
"source_event_index": 1,
"reservation_no": "RESV-AIQUERY-001"
}
""".formatted(source.inboxId())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.matched_order_records.length()").value(0))
.andExpect(jsonPath("$.data.pending_or_open_tasks.length()").value(0))
.andExpect(jsonPath("$.data.target_object_validation.status").value("conflict"))
.andExpect(jsonPath("$.data.target_object_validation.can_create_new_booking_task").value(false))
.andExpect(jsonPath("$.data.target_object_validation.needs_manual_review_reason")
.value("UNSUPPORTED_RESERVATION_NO_QUERY"));
}
@Test
void shouldReturnObjectDetailWithNullableOperaProjectionWarning() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-ai-query-detail-001");
insertActiveGroupOrder(920000000000000401L, source.inboxId(), "GRP-AIQUERY-DETAIL-001");
mockMvc.perform(post("/api/ai-query/v1/object-detail")
.contentType(MediaType.APPLICATION_JSON)
.header("X-Request-Id", "req-ai-query-detail-001")
.header("X-AI-Trace-Id", "trace-ai-query-detail-001")
.content("""
{
"hotel_id": "HOTEL-TEST",
"object_id": "ORDER:920000000000000401",
"object_type": "group_block"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.object_id").value("ORDER:920000000000000401"))
.andExpect(jsonPath("$.data.object_type").value("group_block"))
.andExpect(jsonPath("$.data.order_id").value("920000000000000401"))
.andExpect(jsonPath("$.data.group_code").value("GRP-AIQUERY-DETAIL-001"))
.andExpect(jsonPath("$.data.reservation_no").value(nullValue()))
.andExpect(jsonPath("$.data.block_id").value(nullValue()))
.andExpect(jsonPath("$.data.room_items.length()").value(0))
.andExpect(jsonPath("$.data.rate_code_price").value(nullValue()))
.andExpect(jsonPath("$.data.can_update").value(true))
.andExpect(jsonPath("$.data.can_cancel").value(true))
.andExpect(jsonPath("$.data.hard_validation_warnings[0].code").value("OPERA_PROJECTION_UNAVAILABLE"));
}
private SourceMessageCaptureResult captureSourceMessage(String externalMessageId) {
return captureService.capture(new CaptureSourceMessageCommand(
HOTEL_ID,
"AGENTBUS",
"EMAIL",
externalMessageId,
"thread-" + externalMessageId,
"frame-" + externalMessageId,
"session-ai-query",
Instant.parse("2026-07-07T08:00:00Z"),
"guest@example.test",
"M002 AI Query",
"Please handle booking message.",
"<html><body>Please handle booking message.</body></html>",
"{\"source\":{\"external_message_id\":\"" + externalMessageId + "\"}}",
"agentbus-outlook-v1",
List.of()
));
}
private void insertActiveGroupOrder(Long orderId, Long sourceMessageId, String groupCode) {
jdbcTemplate.update("""
INSERT INTO workflow_reservation_order (
id, hotel_id, order_key_type, order_business_key, active_business_key,
temporary_order_code, order_status, business_key_source, display_name,
source_message_id, version, created_at, updated_at
)
VALUES (?, ?, 'GROUP_CODE', ?, ?, ?, 'ACTIVE', 'AI_CANDIDATE', ?, ?, 0,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", orderId, HOTEL_ID, groupCode, groupCode, "TMP-" + orderId, groupCode, sourceMessageId);
}
private void insertTransition(Long transitionId, Long sourceMessageId, Integer sourceEventIndex, String groupCode) {
jdbcTemplate.update("""
INSERT INTO workflow_reservation_ai_transition (
id, hotel_id, batch_id, source_message_id, source_event_index, array_index,
execution_order, catalog_code, skill_id, result_type, ai_task_type,
system_task_type, task_card_type, task_subtype, current_or_history,
group_code, item_payload_sha256, item_idempotency_key, blocked_until_parent_completed,
ai_payload_json, case_keys_json, extracted_fields_json, created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, 1, 1, 'S02', 'update_booking_amendment_skill',
'normal_task', 'Update Booking', 'UPDATE_BOOKING', 'UPDATE_BOOKING',
'update_stay_dates', 'current', ?, ?, ?, 0, '{}', '{}', '{}',
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", transitionId, HOTEL_ID, transitionId - 1, sourceMessageId, sourceEventIndex, groupCode,
"0".repeat(64), "1".repeat(64));
}
private void insertTask(Long taskId, Long orderId, Long sourceMessageId, Long transitionId, String taskStatus) {
jdbcTemplate.update("""
INSERT INTO workflow_reservation_task (
id, hotel_id, order_id, source_message_id, ai_transition_id,
result_type, ai_task_type, system_task_type, task_card_type, task_subtype,
task_status, queue_participation, execution_order, blocked_until_parent_completed,
version, created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, 'normal_task', 'Update Booking', 'UPDATE_BOOKING',
'UPDATE_BOOKING', 'update_stay_dates', ?, 1, 1, 0, 0,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", taskId, HOTEL_ID, orderId, sourceMessageId, transitionId, taskStatus);
}
}