""" 入站选路:陆运书签/本句陆运交给 LandTextInquiryFlow;海运立即建单不变。 """ from __future__ import annotations import os import sys import unittest ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if ROOT not in sys.path: sys.path.insert(0, ROOT) os.environ.setdefault("YTD_ENV", "test") os.environ.setdefault("REDIS_BACKEND", "memory") os.environ.setdefault("CHECKPOINT_BACKEND", "memory") os.environ.setdefault("MESSAGE_STORE_BACKEND", "memory") os.environ.setdefault("LLM_DATA_USAGE_CONFIRMED", "false") os.environ.setdefault("LLM_ALLOW_NETWORK", "false") os.environ.setdefault("LEDGER_BACKEND", "memory") from agent.channel.queue import MemoryMessageStore, get_message_store from agent.channel.wecom.models import InboundMessage from agent.handlers.text_inquiry import handle_text_inquiry from agent.policy.air_text_flow import reset_air_text_flow_for_test from agent.policy.land_text_flow import get_land_text_flow, reset_land_text_flow_for_test from agent.policy.sea_text_flow import reset_sea_text_flow_for_test from agent.routing.decision import RouteDecision from agent.routing.dispatch import dispatch_inbound from tests.test_sea_text_inquiry import COMPLETE_SEA class LandDispatchTests(unittest.TestCase): def setUp(self) -> None: reset_sea_text_flow_for_test() reset_land_text_flow_for_test() reset_air_text_flow_for_test() self.store = MemoryMessageStore() def _outbox_text(self) -> str: return "\n".join(i.content for i in self.store._outbox.values()) def test_handler_routes_land_to_option_sheet(self) -> None: msg = InboundMessage(sender_id="ld1", message_id="m-land-1", content="陆运询价") phase = handle_text_inquiry( msg, injected_facts={"起运港": "广州", "目的港": "深圳", "品名": "衣服"}, injected_mode="LAND", store=self.store, ) self.assertEqual(phase, "need_land_options") body = self._outbox_text() self.assertIn("运输类型", body) self.assertIn("线路类别", body) self.assertIn("运输分类", body) self.assertNotIn("请补充陆运类型:", body) def test_air_owner_forwards_land_to_option_sheet(self) -> None: from agent.policy.air_text_flow import get_air_text_flow replies: list[str] = [] phase = get_air_text_flow().on_text( sender_id="ld-air", text="陆运询价", reply=lambda t, extra=None: replies.append(t) or "", injected_facts={"起运港": "广州", "目的港": "深圳", "品名": "衣服"}, injected_mode="LAND", ) self.assertEqual(phase, "need_land_options") text = "\n".join(replies) self.assertIn("线路类别", text) self.assertIn("运输分类", text) self.assertNotIn("请补充陆运类型:", text) def test_land_session_survives_confirm(self) -> None: msg1 = InboundMessage(sender_id="ld2", message_id="m-l2a", content="陆运") facts = { "起运港": "广州", "目的港": "深圳", "品名": "衣服", "体积": "100CBM", "毛重": "50kg", } phase = handle_text_inquiry( msg1, injected_facts=facts, injected_mode="LAND", store=self.store, ) # 补选项走同一 sender msg2 = InboundMessage( sender_id="ld2", message_id="m-l2b", content="国内运输拼车 国内长途/零担 急件", ) phase = handle_text_inquiry( msg2, injected_facts=facts, injected_mode="LAND", store=self.store, ) self.assertEqual(phase, "wait_confirm") msg3 = InboundMessage(sender_id="ld2", message_id="m-l2c", content="确定") phase = handle_text_inquiry(msg3, store=self.store) self.assertEqual(phase, "wait_collab") def test_confirm_misrouted_as_other_still_creates(self) -> None: store = get_message_store() facts = { "起运港": "广州", "目的港": "深圳", "品名": "衣服", "体积": "100CBM", "毛重": "50kg", } handle_text_inquiry( InboundMessage(sender_id="ld3", message_id="m-l3a", content="陆运"), injected_facts=facts, injected_mode="LAND", store=store, ) handle_text_inquiry( InboundMessage( sender_id="ld3", message_id="m-l3b", content="国内运输拼车 国内长途/零担 急件", ), injected_facts=facts, injected_mode="LAND", store=store, ) self.assertEqual(get_land_text_flow().session_of("ld3").phase, "wait_confirm") name = dispatch_inbound( InboundMessage(sender_id="ld3", message_id="m-l3c", content="确定"), RouteDecision(intent="ordinary_text_other"), ) self.assertEqual(name, "ordinary_text_inquiry") sess = get_land_text_flow().session_of("ld3") self.assertEqual(sess.phase, "wait_collab") self.assertTrue((sess.work_order_no or "").startswith("WO")) bodies = "\n".join(i.content for i in store._outbox.values()) self.assertIn(sess.work_order_no, bodies) last = list(store._outbox.values())[-1].content self.assertNotIn("你好,我是询价机器人", last) def test_confirm_after_memory_lost_still_creates(self) -> None: store = get_message_store() facts = { "起运港": "广州", "目的港": "深圳", "品名": "衣服", "体积": "100CBM", "毛重": "50kg", } handle_text_inquiry( InboundMessage(sender_id="ld4", message_id="m-l4a", content="陆运"), injected_facts=facts, injected_mode="LAND", store=store, ) handle_text_inquiry( InboundMessage( sender_id="ld4", message_id="m-l4b", content="国内运输拼车 国内长途/零担 急件", ), injected_facts=facts, injected_mode="LAND", store=store, ) self.assertEqual(get_land_text_flow().session_of("ld4").phase, "wait_confirm") reset_land_text_flow_for_test() self.assertIsNone(get_land_text_flow().session_of("ld4")) name = dispatch_inbound( InboundMessage(sender_id="ld4", message_id="m-l4c", content="确定"), RouteDecision(intent="ordinary_text_other"), ) self.assertEqual(name, "ordinary_text_inquiry") sess = get_land_text_flow().session_of("ld4") self.assertEqual(sess.phase, "wait_collab") self.assertTrue((sess.work_order_no or "").startswith("WO")) last = list(store._outbox.values())[-1].content self.assertNotIn("你好,我是询价机器人", last) def test_tms_retry_misrouted_as_other(self) -> None: from tests.test_land_text_inquiry import CountingLedger class Flaky(CountingLedger): def __init__(self) -> None: super().__init__() self.query_calls = 0 def query_tms(self, **kwargs): self.query_calls += 1 if self.query_calls == 1: return { "ok": False, "classification": "TMS_TECH_FAILURE", "has_price": False, "quote": {}, } return super().query_tms(**kwargs) land = reset_land_text_flow_for_test() land.ledger = Flaky() facts = { "起运港": "广州", "目的港": "深圳", "品名": "衣服", "体积": "100CBM", "毛重": "50kg", } handle_text_inquiry( InboundMessage(sender_id="ld5", message_id="m-l5a", content="陆运"), injected_facts=facts, injected_mode="LAND", store=self.store, ) handle_text_inquiry( InboundMessage( sender_id="ld5", message_id="m-l5b", content="国内运输拼车 国内长途/零担 急件", ), injected_facts=facts, injected_mode="LAND", store=self.store, ) handle_text_inquiry( InboundMessage(sender_id="ld5", message_id="m-l5c", content="确定"), store=self.store, ) self.assertEqual(get_land_text_flow().session_of("ld5").phase, "tms_tech") wo = get_land_text_flow().session_of("ld5").work_order_no name = dispatch_inbound( InboundMessage(sender_id="ld5", message_id="m-l5d", content="确定"), RouteDecision(intent="ordinary_text_other"), ) self.assertEqual(name, "ordinary_text_inquiry") sess = get_land_text_flow().session_of("ld5") self.assertEqual(sess.phase, "wait_collab") self.assertEqual(sess.work_order_no, wo) last = list(self.store._outbox.values())[-1].content self.assertNotIn("你好,我是询价机器人", last) def test_sea_still_creates_immediately(self) -> None: msg = InboundMessage(sender_id="s-sea", message_id="m-sea-keep", content="海运询价") phase = handle_text_inquiry( msg, injected_facts=COMPLETE_SEA, injected_mode="SEA", store=self.store, ) self.assertEqual(phase, "wait_collab") if __name__ == "__main__": unittest.main()