实现 SuperAgent 查询上下文接口
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user