落地陆运私聊文字询价:选项核对后再建单查价,并按线路拉产品进群。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
陆运核对卡与无产品话术。
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
from agent.policy import inquiry_copy as copy
|
||||
|
||||
|
||||
class LandCopyTest(unittest.TestCase):
|
||||
def test_domestic_card_no_customs_no_wo(self):
|
||||
text = copy.land_confirm_card(
|
||||
{
|
||||
"运输类型": "国内运输拼车",
|
||||
"线路类别": "国内长途/零担",
|
||||
"运输分类": "急件",
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
"报价日期": "2026-09-17",
|
||||
}
|
||||
)
|
||||
self.assertIn("始发站(必填):广州", text)
|
||||
self.assertNotIn("通关口岸", text)
|
||||
self.assertNotIn("WO", text)
|
||||
self.assertIn('请回复“确定”进行TMS查价,内容有误时,请重新发内容。', text)
|
||||
self.assertIn("运输类型", text)
|
||||
self.assertIn("国内运输拼车", text)
|
||||
|
||||
def test_cross_empty_customs(self):
|
||||
text = copy.land_confirm_card(
|
||||
{
|
||||
"运输类型": "跨境集拼",
|
||||
"线路类别": "东南亚",
|
||||
"运输分类": "急件",
|
||||
"起运港": "广州",
|
||||
"目的港": "河内",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
"报价日期": "2026-09-17",
|
||||
}
|
||||
)
|
||||
self.assertIn("通关口岸(非必填):", text)
|
||||
|
||||
def test_hk_shows_load(self):
|
||||
text = copy.land_confirm_card(
|
||||
{
|
||||
"运输类型": "中港",
|
||||
"线路类别": "东南亚",
|
||||
"运输分类": "急件",
|
||||
"中港装载方式": "零担/集拼",
|
||||
"起运港": "广州",
|
||||
"目的港": "香港",
|
||||
"品名": "衣服",
|
||||
"车型/数量": "9米6/1",
|
||||
"报价日期": "2026-09-17",
|
||||
}
|
||||
)
|
||||
self.assertIn("零担/集拼", text)
|
||||
self.assertIn("车型/数量(必填):9米6/1", text)
|
||||
self.assertNotIn("\n数量(必填)", text)
|
||||
|
||||
def test_hk_full_truck_hides_vehicle(self):
|
||||
text = copy.land_confirm_card(
|
||||
{
|
||||
"运输类型": "中港",
|
||||
"线路类别": "东南亚",
|
||||
"运输分类": "急件",
|
||||
"中港装载方式": "整车",
|
||||
"起运港": "广州",
|
||||
"目的港": "香港",
|
||||
"品名": "衣服",
|
||||
"报价日期": "2026-09-17",
|
||||
}
|
||||
)
|
||||
self.assertIn("整车", text)
|
||||
self.assertNotIn("车型/数量", text)
|
||||
|
||||
def test_domestic_ftl_one_field(self):
|
||||
text = copy.land_confirm_card(
|
||||
{
|
||||
"运输类型": "国内运输整车",
|
||||
"线路类别": "国内长途/零担",
|
||||
"运输分类": "急件",
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"车型/数量": "9米6/1",
|
||||
"报价日期": "2026-09-17",
|
||||
}
|
||||
)
|
||||
self.assertIn("车型/数量(必填):9米6/1", text)
|
||||
self.assertNotIn("\n数量(必填)", text)
|
||||
|
||||
def test_no_staff_copy(self):
|
||||
self.assertIn("陆运产品岗", copy.land_group_no_staff())
|
||||
|
||||
def test_inquiry_card_lists_land_fields_and_wo(self):
|
||||
text = copy.inquiry_card(
|
||||
work_order_no="WO202609170006",
|
||||
facts={
|
||||
"运输类型": "国内运输拼车",
|
||||
"线路类别": "国内长途/零担",
|
||||
"运输分类": "急件",
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
"报价日期": "2026-09-17",
|
||||
},
|
||||
transport_mode="陆运",
|
||||
)
|
||||
self.assertIn("工单号:WO202609170006", text)
|
||||
self.assertIn("运输方式:陆运", text)
|
||||
self.assertIn("运输类型:国内运输拼车", text)
|
||||
self.assertIn("始发站:广州", text)
|
||||
self.assertIn("货物品名:衣服", text)
|
||||
self.assertIn("工单已创建,正在查询TMS报价。", text)
|
||||
self.assertNotIn("件数", text)
|
||||
|
||||
def test_scheme_card_uses_land_fields_not_sea(self):
|
||||
facts = {
|
||||
"运输类型": "国内运输拼车",
|
||||
"线路类别": "国内城配/拖车/打包",
|
||||
"运输分类": "普件,常用线路/常规车型",
|
||||
"起运港": "泉州市",
|
||||
"目的港": "南京市",
|
||||
"品名": "通信机柜及线缆配件",
|
||||
"体积": "10CBM",
|
||||
"毛重": "10KG",
|
||||
"报价日期": "2026-09-17",
|
||||
}
|
||||
quote = {"total": "CNY 1200", "eta": "3天", "valid_until": "2026-09-20"}
|
||||
text = copy.scheme_card(
|
||||
work_order_no="WO202609170011",
|
||||
status="已报价",
|
||||
facts=facts,
|
||||
quote=quote,
|
||||
transport_mode="陆运",
|
||||
)
|
||||
self.assertIn("运输方式:陆运", text)
|
||||
self.assertIn("运输类型:国内运输拼车", text)
|
||||
self.assertIn("始发站:泉州市", text)
|
||||
self.assertIn("货物品名:通信机柜及线缆配件", text)
|
||||
self.assertIn("体积:10CBM", text)
|
||||
self.assertNotIn("整柜或拼柜", text)
|
||||
self.assertNotIn("箱型箱量", text)
|
||||
self.assertNotIn("件数:", text)
|
||||
self.assertNotIn("包装类型:", text)
|
||||
rows = {
|
||||
x["keyname"]: x["value"]
|
||||
for x in copy.scheme_horizontal_rows(
|
||||
work_order_no="WO202609170011",
|
||||
facts=facts,
|
||||
quote=quote,
|
||||
transport_mode="陆运",
|
||||
)
|
||||
}
|
||||
self.assertIn("陆运 泉州市→南京市", rows["线路"])
|
||||
self.assertIn("国内运输拼车", rows["类型"])
|
||||
self.assertIn("国内城配/拖车/打包", rows["类型"])
|
||||
self.assertIn("通信机柜及线缆配件", rows["货物"])
|
||||
self.assertIn("10CBM", rows["货物"])
|
||||
self.assertNotIn("航线", rows)
|
||||
self.assertNotIn("箱型", rows)
|
||||
self.assertNotIn("包装", rows)
|
||||
self.assertNotIn("空运", "".join(rows.values()))
|
||||
self.assertNotIn("海运", "".join(rows.values()))
|
||||
|
||||
def test_scheme_card_ftl_shows_vehicle_combo(self):
|
||||
facts = {
|
||||
"运输类型": "国内运输整车",
|
||||
"线路类别": "国内长途/零担",
|
||||
"运输分类": "急件",
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"车型/数量": "9米6/1",
|
||||
}
|
||||
rows = {
|
||||
x["keyname"]: x["value"]
|
||||
for x in copy.scheme_horizontal_rows(
|
||||
work_order_no="WO202609170012",
|
||||
facts=facts,
|
||||
quote={},
|
||||
transport_mode="陆运",
|
||||
)
|
||||
}
|
||||
self.assertIn("9米6/1", rows["货物"])
|
||||
self.assertNotIn("CBM", rows["货物"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
入站选路:陆运书签/本句陆运交给 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()
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
陆运五套询价必填校验。不连 TMS。
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
from agent.schema.field_validate import display_name, validate_required_fields
|
||||
|
||||
|
||||
class LandFieldValidateTest(unittest.TestCase):
|
||||
def test_ltl_missing_volume(self):
|
||||
got = validate_required_fields(
|
||||
facts={"起运港": "广州", "目的港": "深圳", "品名": "衣服", "毛重": "50kg"},
|
||||
business_line="LAND",
|
||||
land_subtype="国内运输拼车",
|
||||
)
|
||||
self.assertFalse(got["ok"])
|
||||
self.assertIn("体积", got["missing"])
|
||||
|
||||
def test_hk_lcl_still_needs_vehicle(self):
|
||||
got = validate_required_fields(
|
||||
facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "香港",
|
||||
"品名": "衣服",
|
||||
"体积": "10CBM",
|
||||
"毛重": "50kg",
|
||||
"中港装载方式": "零担/集拼",
|
||||
},
|
||||
business_line="LAND",
|
||||
land_subtype="中港",
|
||||
)
|
||||
self.assertIn("车型/数量", got["missing"])
|
||||
self.assertNotIn("数量", got["missing"])
|
||||
self.assertNotIn("车型", got["missing"])
|
||||
|
||||
def test_hk_full_truck_skips_vehicle(self):
|
||||
got = validate_required_fields(
|
||||
facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "香港",
|
||||
"品名": "衣服",
|
||||
"中港装载方式": "整车",
|
||||
},
|
||||
business_line="LAND",
|
||||
land_subtype="中港",
|
||||
)
|
||||
self.assertTrue(got["ok"])
|
||||
self.assertNotIn("车型/数量", got["missing"])
|
||||
|
||||
def test_domestic_ftl_one_field(self):
|
||||
got = validate_required_fields(
|
||||
facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"车型/数量": "9米6/1",
|
||||
},
|
||||
business_line="LAND",
|
||||
land_subtype="国内运输整车",
|
||||
)
|
||||
self.assertTrue(got["ok"])
|
||||
self.assertEqual(got["facts"]["车型/数量"], "9米6/1")
|
||||
|
||||
def test_cross_ftl_folds_split_keys(self):
|
||||
got = validate_required_fields(
|
||||
facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "河内",
|
||||
"品名": "衣服",
|
||||
"车型": "9米6",
|
||||
"数量": "1",
|
||||
},
|
||||
business_line="LAND",
|
||||
land_subtype="跨境整车",
|
||||
)
|
||||
self.assertTrue(got["ok"])
|
||||
self.assertEqual(got["facts"]["车型/数量"], "9米6/1")
|
||||
|
||||
def test_quote_date_defaults(self):
|
||||
got = validate_required_fields(
|
||||
facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
business_line="LAND",
|
||||
land_subtype="国内运输拼车",
|
||||
)
|
||||
self.assertTrue(got["ok"])
|
||||
self.assertRegex(got["facts"]["报价日期"], r"^\d{4}-\d{2}-\d{2}$")
|
||||
|
||||
def test_land_display_names(self):
|
||||
self.assertEqual(display_name("起运港", "LAND"), "始发站")
|
||||
self.assertEqual(display_name("品名", "LAND"), "货物品名")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
陆运选项联动单测:类型/线路/分类/中港装载,选错只纠正该项。
|
||||
|
||||
不连企微、主账、TMS。
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
from agent.schema.land_options import (
|
||||
harvest_land_options,
|
||||
looks_like_confirm,
|
||||
option_prompt,
|
||||
validate_land_options,
|
||||
)
|
||||
|
||||
|
||||
class LandOptionsTest(unittest.TestCase):
|
||||
def test_domestic_ltl_rejects_sea_route(self):
|
||||
got = validate_land_options(
|
||||
{
|
||||
"运输类型": "国内运输拼车",
|
||||
"线路类别": "东南亚",
|
||||
"运输分类": "急件",
|
||||
}
|
||||
)
|
||||
self.assertFalse(got["ok"])
|
||||
msg = got["errors"][0]["message"]
|
||||
self.assertIn("国内长途/零担", msg)
|
||||
self.assertIn("国内城配/拖车/打包", msg)
|
||||
|
||||
def test_followup_route_class_keeps_type(self):
|
||||
cur = harvest_land_options(
|
||||
"国内城配/拖车/打包;普件,常用线路/常规车型",
|
||||
current={"运输类型": "国内运输拼车"},
|
||||
)
|
||||
self.assertEqual(cur["运输类型"], "国内运输拼车")
|
||||
self.assertEqual(cur["线路类别"], "国内城配/拖车/打包")
|
||||
self.assertEqual(cur["运输分类"], "普件,常用线路/常规车型")
|
||||
|
||||
def test_model_land_does_not_wipe_type(self):
|
||||
dropped = harvest_land_options(
|
||||
"国内城配/拖车/打包;普件,常用线路/常规车型",
|
||||
current={"运输类型": "陆运"},
|
||||
)
|
||||
self.assertFalse(dropped.get("运输类型"))
|
||||
cur = harvest_land_options(
|
||||
"国内城配/拖车/打包;普件,常用线路/常规车型",
|
||||
current={"运输类型": "国内运输拼车"},
|
||||
)
|
||||
junk = harvest_land_options("陆运", current=cur)
|
||||
self.assertEqual(junk["运输类型"], "国内运输拼车")
|
||||
self.assertEqual(junk["线路类别"], "国内城配/拖车/打包")
|
||||
cur = harvest_land_options("广州到深圳国内拼车衣服100方")
|
||||
self.assertEqual(cur["运输类型"], "国内运输拼车")
|
||||
self.assertFalse(cur.get("线路类别"))
|
||||
|
||||
def test_full_prompt_when_empty(self):
|
||||
text = option_prompt(current={})
|
||||
self.assertIn("运输类型", text)
|
||||
self.assertIn("线路类别", text)
|
||||
self.assertIn("运输分类", text)
|
||||
self.assertIn("国内运输拼车", text)
|
||||
self.assertIn("整车", text)
|
||||
self.assertNotIn("请补充陆运类型:", text)
|
||||
|
||||
def test_copy_alias_lists_all_three(self):
|
||||
from agent.policy import inquiry_copy as copy
|
||||
|
||||
text = copy.ASK_LAND_SUBTYPE
|
||||
self.assertIn("运输类型", text)
|
||||
self.assertIn("线路类别", text)
|
||||
self.assertIn("运输分类", text)
|
||||
self.assertNotIn("请补充陆运类型:国内运输拼车", text)
|
||||
|
||||
def test_narrow_prompt_when_type_known(self):
|
||||
text = option_prompt(current={"运输类型": "国内运输拼车"})
|
||||
self.assertIn("国内长途/零担", text)
|
||||
self.assertNotIn("请选择运输类型", text)
|
||||
|
||||
def test_hk_needs_load(self):
|
||||
got = validate_land_options(
|
||||
{
|
||||
"运输类型": "中港",
|
||||
"线路类别": "东南亚",
|
||||
"运输分类": "急件",
|
||||
}
|
||||
)
|
||||
self.assertIn("中港装载方式", got["missing"])
|
||||
|
||||
def test_confirm_intent(self):
|
||||
self.assertTrue(looks_like_confirm("确认"))
|
||||
self.assertTrue(looks_like_confirm("可以查了"))
|
||||
self.assertTrue(looks_like_confirm("查价"))
|
||||
self.assertFalse(looks_like_confirm("广州到深圳衣服"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,536 @@
|
||||
"""
|
||||
陆运私聊:选项补齐 → 货物补问 → 无工单号核对卡。确定建单见后续用例。
|
||||
|
||||
不连企微 / 真实 TMS / 8180。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
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.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.land_text_flow import LandTextInquiryFlow
|
||||
|
||||
|
||||
class CountingLedger(MemoryLedger):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.create_calls = 0
|
||||
self.last_created_no = ""
|
||||
|
||||
def create_ticket(self, **kwargs):
|
||||
self.create_calls += 1
|
||||
out = super().create_ticket(**kwargs)
|
||||
self.last_created_no = str(out.get("work_order_no") or "")
|
||||
return out
|
||||
|
||||
|
||||
class LandTextInquiryFrontTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.ledger = CountingLedger()
|
||||
self.flow = LandTextInquiryFlow(self.ledger)
|
||||
self.replies: list[str] = []
|
||||
|
||||
def _reply(self, text: str, extra=None) -> str:
|
||||
self.replies.append(text)
|
||||
return ""
|
||||
|
||||
def _joined(self) -> str:
|
||||
return "\n".join(self.replies)
|
||||
|
||||
def test_all_options_missing_lists_sheet(self) -> None:
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="陆运广州到深圳衣服",
|
||||
reply=self._reply,
|
||||
injected_facts={"起运港": "广州", "目的港": "深圳", "品名": "衣服"},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "need_land_options")
|
||||
self.assertEqual(self.ledger.create_calls, 0)
|
||||
text = self._joined()
|
||||
self.assertIn("国内运输拼车", text)
|
||||
self.assertIn("线路类别", text)
|
||||
self.assertIn("运输分类", text)
|
||||
self.assertNotIn("请补充陆运类型:", text)
|
||||
|
||||
def test_known_type_does_not_reask_type(self) -> None:
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="广州到深圳国内拼车衣服100方",
|
||||
reply=self._reply,
|
||||
injected_facts={"起运港": "广州", "目的港": "深圳", "品名": "衣服"},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "need_land_options")
|
||||
self.assertNotIn("请选择运输类型", self._joined())
|
||||
self.assertIn("国内长途/零担", self._joined())
|
||||
|
||||
def test_wrong_route_keeps_type(self) -> None:
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="国内运输拼车,线路东南亚",
|
||||
reply=self._reply,
|
||||
injected_facts={"起运港": "广州", "目的港": "深圳", "品名": "衣服"},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "need_land_options")
|
||||
self.assertIn("国内长途/零担", self._joined())
|
||||
sess = self.flow.session_of("u1")
|
||||
self.assertEqual(sess.facts.get("运输类型"), "国内运输拼车")
|
||||
|
||||
def test_hk_lcl_asks_vehicle(self) -> None:
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="中港 东南亚 急件 零担",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "香港",
|
||||
"品名": "衣服",
|
||||
"体积": "10CBM",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "clarify")
|
||||
self.assertIn("车型/数量", self._joined())
|
||||
self.assertNotIn("\n数量:", self._joined())
|
||||
self.assertNotIn(copy.LAND_CONFIRM_TAIL, self._joined())
|
||||
self.assertEqual(self.ledger.create_calls, 0)
|
||||
|
||||
def test_hk_full_truck_skips_vehicle(self) -> None:
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="中港 东南亚 急件 整车",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "香港",
|
||||
"品名": "衣服",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "wait_confirm")
|
||||
self.assertNotIn("车型/数量", self._joined())
|
||||
self.assertEqual(self.ledger.create_calls, 0)
|
||||
|
||||
def test_domestic_complete_card_no_ticket(self) -> None:
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="国内运输拼车 国内长途/零担 急件",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "wait_confirm")
|
||||
self.assertEqual(self.ledger.create_calls, 0)
|
||||
today = datetime.now(ZoneInfo("Asia/Shanghai")).date().isoformat()
|
||||
joined = self._joined()
|
||||
self.assertIn(today, joined)
|
||||
self.assertNotIn("通关口岸", joined)
|
||||
self.assertNotIn("WO", joined)
|
||||
self.assertIn(copy.LAND_CONFIRM_TAIL, joined)
|
||||
|
||||
def test_cross_complete_shows_empty_customs(self) -> None:
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="跨境集拼 东南亚 急件",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "河内",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "wait_confirm")
|
||||
self.assertIn("通关口岸(非必填):", self._joined())
|
||||
|
||||
def test_missing_volume_no_confirm(self) -> None:
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="国内运输拼车 国内长途/零担 急件",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "clarify")
|
||||
self.assertNotIn(copy.LAND_CONFIRM_TAIL, self._joined())
|
||||
|
||||
|
||||
class LandTextConfirmTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.ledger = CountingLedger()
|
||||
self.flow = LandTextInquiryFlow(self.ledger)
|
||||
self.replies: list[str] = []
|
||||
|
||||
def _reply(self, text: str, extra=None) -> str:
|
||||
self.replies.append(text)
|
||||
return ""
|
||||
|
||||
def _to_confirm(self) -> str:
|
||||
return self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="国内运输拼车 国内长途/零担 急件",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
|
||||
def test_confirm_creates_ticket_and_queries(self) -> None:
|
||||
self.assertEqual(self._to_confirm(), "wait_confirm")
|
||||
self.replies.clear()
|
||||
phase = self.flow.on_text(sender_id="u1", text="确定", reply=self._reply)
|
||||
self.assertEqual(phase, "wait_collab")
|
||||
self.assertTrue(self.ledger.last_created_no.startswith("WO"))
|
||||
self.assertIn(self.ledger.last_created_no, "\n".join(self.replies))
|
||||
ticket = self.ledger.get_ticket(work_order_no=self.ledger.last_created_no)
|
||||
self.assertEqual(ticket.facts.get("运输类型"), "国内运输拼车")
|
||||
self.assertEqual(ticket.facts.get("线路类别"), "国内长途/零担")
|
||||
joined = "\n".join(self.replies)
|
||||
self.assertIn("工单号:" + self.ledger.last_created_no, joined)
|
||||
self.assertIn("运输方式:陆运", joined)
|
||||
self.assertIn("运输类型:国内运输拼车", joined)
|
||||
self.assertIn("始发站:广州", joined)
|
||||
self.assertIn("工单已创建,正在查询TMS报价。", joined)
|
||||
self.assertNotIn("工单已创建:" + self.ledger.last_created_no, joined)
|
||||
|
||||
def test_tms_fail_then_confirm_retries_same_ticket(self) -> None:
|
||||
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)
|
||||
|
||||
ledger = Flaky()
|
||||
flow = LandTextInquiryFlow(ledger)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
return ""
|
||||
|
||||
flow.on_text(
|
||||
sender_id="u1",
|
||||
text="国内运输拼车 国内长途/零担 急件",
|
||||
reply=reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
phase = flow.on_text(sender_id="u1", text="确定", reply=reply)
|
||||
self.assertEqual(phase, "tms_tech")
|
||||
self.assertEqual(ledger.create_calls, 1)
|
||||
wo = ledger.last_created_no
|
||||
replies.clear()
|
||||
flow._by_sender.clear()
|
||||
flow._by_ticket.clear()
|
||||
phase = flow.on_text(sender_id="u1", text="确定", reply=reply)
|
||||
self.assertEqual(phase, "wait_collab")
|
||||
self.assertEqual(ledger.create_calls, 1)
|
||||
self.assertEqual(ledger.query_calls, 2)
|
||||
self.assertEqual(flow.session_of("u1").work_order_no, wo)
|
||||
self.assertIn(wo, "\n".join(replies))
|
||||
self.assertNotIn("你好,我是询价机器人", "\n".join(replies))
|
||||
|
||||
def test_confirm_synonyms(self) -> None:
|
||||
for word in ("确认", "查价", "可以查了"):
|
||||
ledger = CountingLedger()
|
||||
flow = LandTextInquiryFlow(ledger)
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> str:
|
||||
replies.append(text)
|
||||
return ""
|
||||
|
||||
flow.on_text(
|
||||
sender_id="u1",
|
||||
text="国内运输拼车 国内长途/零担 急件",
|
||||
reply=reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
phase = flow.on_text(sender_id="u1", text=word, reply=reply)
|
||||
self.assertEqual(phase, "wait_collab", msg=word)
|
||||
|
||||
def test_resend_changes_type_requeries_fields(self) -> None:
|
||||
self.assertEqual(self._to_confirm(), "wait_confirm")
|
||||
self.replies.clear()
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="改成国内整车",
|
||||
reply=self._reply,
|
||||
)
|
||||
self.assertEqual(phase, "clarify")
|
||||
self.assertTrue(any("车型" in x for x in self.replies))
|
||||
self.assertEqual(self.ledger.create_calls, 0)
|
||||
|
||||
def test_new_land_after_quoted_does_not_recurse(self) -> None:
|
||||
self.assertEqual(self._to_confirm(), "wait_confirm")
|
||||
phase = self.flow.on_text(sender_id="u1", text="确定", reply=self._reply)
|
||||
self.assertEqual(phase, "wait_collab")
|
||||
self.replies.clear()
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="陆运,国内运输拼车,泉州市到南京市,通信机柜及线缆配件,10CBM,10KG",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "泉州",
|
||||
"目的港": "南京",
|
||||
"品名": "通信机柜及线缆配件",
|
||||
"体积": "10CBM",
|
||||
"毛重": "10KG",
|
||||
"运输类型": "国内运输拼车",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "need_land_options")
|
||||
self.assertTrue(self.replies)
|
||||
self.assertIn("线路类别", "\n".join(self.replies))
|
||||
|
||||
def test_new_land_after_tms_miss_does_not_recurse(self) -> None:
|
||||
self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="国内运输拼车 国内长途/零担 急件",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "无价",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
phase = self.flow.on_text(sender_id="u1", text="确定", reply=self._reply)
|
||||
self.assertEqual(phase, "tms_miss")
|
||||
self.replies.clear()
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="陆运,国内运输拼车,泉州市到南京市,通信机柜及线缆配件,10CBM,10KG",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "泉州",
|
||||
"目的港": "南京",
|
||||
"品名": "通信机柜及线缆配件",
|
||||
"体积": "10CBM",
|
||||
"毛重": "10KG",
|
||||
"运输类型": "国内运输拼车",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "need_land_options")
|
||||
self.assertIn("线路类别", "\n".join(self.replies))
|
||||
|
||||
def test_followup_city_route_does_not_reask_type(self) -> None:
|
||||
"""
|
||||
已选国内运输拼车后,用户只补线路+分类。
|
||||
模型若把运输类型写成「陆运」,不得整张清单重问。
|
||||
"""
|
||||
self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="陆运,国内运输拼车,泉州市到南京市,通信机柜及线缆配件,10CBM,10KG",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "泉州",
|
||||
"目的港": "南京",
|
||||
"品名": "通信机柜及线缆配件",
|
||||
"体积": "10CBM",
|
||||
"毛重": "10KG",
|
||||
"运输类型": "国内运输拼车",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(self.flow.session_of("u1").phase, "need_land_options")
|
||||
self.replies.clear()
|
||||
phase = self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="国内城配/拖车/打包;普件,常用线路/常规车型",
|
||||
reply=self._reply,
|
||||
injected_facts={"品名": "通信机柜及线缆配件", "运输类型": "陆运"},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.assertEqual(phase, "wait_confirm")
|
||||
text = "\n".join(self.replies)
|
||||
self.assertNotIn("请一次选择:运输类型", text)
|
||||
self.assertIn("国内运输拼车", text)
|
||||
self.assertIn("国内城配/拖车/打包", text)
|
||||
self.assertIn("普件,常用线路/常规车型", text)
|
||||
|
||||
|
||||
class FakeGroupClient:
|
||||
"""单测假企微建群:不打外网。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.created: list[dict] = []
|
||||
self.sent: list[dict] = []
|
||||
|
||||
def create_group(self, *, name: str, userids: list[str]) -> dict:
|
||||
self.created.append({"name": name, "userids": list(userids)})
|
||||
return {"ok": True, "chat_id": "wr_land_1", "error": ""}
|
||||
|
||||
def add_group_members(self, *, chat_id: str, userids: list[str]) -> dict:
|
||||
return {"ok": True}
|
||||
|
||||
def send_group(self, *, chat_id: str, content: str, mention_userids: list | None = None) -> dict:
|
||||
self.sent.append({"chat_id": chat_id, "content": content, "kind": "text"})
|
||||
return {"ok": True}
|
||||
|
||||
def send_group_file(self, *, chat_id: str, filename: str, filepath: str = "", file_b64: str = "") -> dict:
|
||||
self.sent.append({"chat_id": chat_id, "filename": filename, "kind": "file"})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
class LandTextPullTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.ledger = CountingLedger()
|
||||
self.ledger.set_quote_templates(
|
||||
[
|
||||
{
|
||||
"templateId": "mem-default-land",
|
||||
"name": "陆运默认",
|
||||
"bizType": "陆运",
|
||||
"keywords": [],
|
||||
"isDefault": True,
|
||||
}
|
||||
]
|
||||
)
|
||||
self.groups = FakeGroupClient()
|
||||
self.flow = LandTextInquiryFlow(self.ledger, group_client=self.groups)
|
||||
self.replies: list[str] = []
|
||||
|
||||
def _reply(self, text: str, extra=None) -> str:
|
||||
self.replies.append(text)
|
||||
return ""
|
||||
|
||||
def _to_quoted(self) -> None:
|
||||
self.flow.on_text(
|
||||
sender_id="u1",
|
||||
text="国内运输拼车 国内长途/零担 急件",
|
||||
reply=self._reply,
|
||||
injected_facts={
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
},
|
||||
injected_mode="LAND",
|
||||
)
|
||||
self.flow.on_text(sender_id="u1", text="确定", reply=self._reply)
|
||||
self.replies.clear()
|
||||
|
||||
def test_pull_only_matching_route(self) -> None:
|
||||
self.ledger.set_land_staff(
|
||||
[
|
||||
{"roleCode": "land", "status": "active", "wecomId": "p_sea", "name": "甲", "routes": "东南亚"},
|
||||
{"roleCode": "land", "status": "active", "wecomId": "p_dom", "name": "乙", "routes": "国内长途/零担"},
|
||||
]
|
||||
)
|
||||
self._to_quoted()
|
||||
phase = self.flow.on_button(
|
||||
sender_id="u1",
|
||||
action=copy.BTN_PULL_COLLAB,
|
||||
reply=self._reply,
|
||||
)
|
||||
self.assertEqual(phase, "collab_group")
|
||||
userids = self.groups.created[0]["userids"]
|
||||
self.assertIn("p_dom", userids)
|
||||
self.assertNotIn("p_sea", userids)
|
||||
brief = "\n".join(x.get("content") or "" for x in self.groups.sent if x.get("kind") == "text")
|
||||
self.assertIn("运输类型", brief)
|
||||
self.assertIn("国内运输拼车", brief)
|
||||
self.assertIn("国内长途/零担", brief)
|
||||
self.assertIn("急件", brief)
|
||||
|
||||
def test_pull_no_staff(self) -> None:
|
||||
self.ledger.set_land_staff([])
|
||||
self._to_quoted()
|
||||
phase = self.flow.on_button(
|
||||
sender_id="u1",
|
||||
action=copy.BTN_PULL_COLLAB,
|
||||
reply=self._reply,
|
||||
)
|
||||
self.assertIn("陆运产品岗", "\n".join(self.replies))
|
||||
self.assertEqual(self.groups.created, [])
|
||||
self.assertEqual(phase, "wait_collab")
|
||||
|
||||
def test_land_role_is_product_in_group(self) -> None:
|
||||
from agent.policy.sea_group_ops import is_product
|
||||
|
||||
self.ledger.set_land_staff(
|
||||
[
|
||||
{
|
||||
"roleCode": "land",
|
||||
"status": "active",
|
||||
"wecomId": "u1",
|
||||
"name": "销产",
|
||||
"routes": "国内长途/零担",
|
||||
}
|
||||
]
|
||||
)
|
||||
self._to_quoted()
|
||||
ticket = self.ledger.get_ticket(work_order_no=self.ledger.last_created_no)
|
||||
self.assertTrue(is_product(ticket, "u1", flow=self.flow))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
内存账本:陆运按线路类别整词匹配产品岗。
|
||||
"""
|
||||
|
||||
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("LEDGER_BACKEND", "memory")
|
||||
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
|
||||
|
||||
class MemoryLandStaffTest(unittest.TestCase):
|
||||
def test_match_route_token(self):
|
||||
book = MemoryLedger()
|
||||
book.set_land_staff(
|
||||
[
|
||||
{"roleCode": "land", "status": "active", "wecomId": "a", "name": "甲", "routes": "东南亚"},
|
||||
{"roleCode": "land", "status": "active", "wecomId": "b", "name": "乙", "routes": "国内长途/零担"},
|
||||
{"roleCode": "land", "status": "active", "wecomId": "c", "name": "丙", "routes": "中港/中亚/中欧,东南亚"},
|
||||
]
|
||||
)
|
||||
hits = book.match_land_staff(route_category="东南亚")
|
||||
names = [x.get("name") for x in hits]
|
||||
self.assertEqual(names, ["甲", "丙"])
|
||||
|
||||
def test_sea_staff_not_pulled(self):
|
||||
book = MemoryLedger()
|
||||
book.set_land_staff(
|
||||
[{"roleCode": "sea", "status": "active", "wecomId": "s", "name": "海", "routes": "东南亚"}]
|
||||
)
|
||||
self.assertEqual(book.match_land_staff(route_category="东南亚"), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -81,6 +81,109 @@ class QuoteTemplateMatchTests(unittest.TestCase):
|
||||
)
|
||||
self.assertIsNone(hit)
|
||||
|
||||
def test_land_pinche_not_stolen_by_generic_land_keyword(self) -> None:
|
||||
hit = pick_quote_template(
|
||||
templates=[
|
||||
{
|
||||
"templateId": "hk",
|
||||
"name": "中港",
|
||||
"bizType": "陆运",
|
||||
"keywords": ["中港", "陆运"],
|
||||
"isDefault": False,
|
||||
},
|
||||
{
|
||||
"templateId": "ltl",
|
||||
"name": "国内拼车",
|
||||
"bizType": "陆运",
|
||||
"keywords": ["拼车", "零担"],
|
||||
"isDefault": False,
|
||||
},
|
||||
{
|
||||
"templateId": "d1",
|
||||
"name": "陆运默认",
|
||||
"bizType": "陆运",
|
||||
"keywords": ["普件"],
|
||||
"isDefault": True,
|
||||
},
|
||||
],
|
||||
business_line="LAND",
|
||||
facts={
|
||||
"运输类型": "国内运输拼车",
|
||||
"线路类别": "国内城配/拖车/打包",
|
||||
"运输分类": "普件,常用线路/常规车型",
|
||||
"起运港": "泉州市",
|
||||
"目的港": "南京市",
|
||||
},
|
||||
)
|
||||
self.assertIsNotNone(hit)
|
||||
self.assertEqual(hit.match_type, "keyword")
|
||||
self.assertEqual(hit.name, "国内拼车")
|
||||
|
||||
def test_land_cross_ftl_beats_domestic_ftl(self) -> None:
|
||||
hit = pick_quote_template(
|
||||
templates=[
|
||||
{
|
||||
"templateId": "dom",
|
||||
"name": "国内整车",
|
||||
"bizType": "陆运",
|
||||
"keywords": ["整车", "国内"],
|
||||
"isDefault": False,
|
||||
},
|
||||
{
|
||||
"templateId": "xb",
|
||||
"name": "跨境整车",
|
||||
"bizType": "陆运",
|
||||
"keywords": ["跨境", "整车"],
|
||||
"isDefault": False,
|
||||
},
|
||||
],
|
||||
business_line="LAND",
|
||||
facts={"运输类型": "跨境整车", "线路类别": "东南亚"},
|
||||
)
|
||||
self.assertIsNotNone(hit)
|
||||
self.assertEqual(hit.name, "跨境整车")
|
||||
|
||||
def test_land_falls_back_to_default(self) -> None:
|
||||
hit = pick_quote_template(
|
||||
templates=[
|
||||
{
|
||||
"templateId": "hk",
|
||||
"name": "中港",
|
||||
"bizType": "陆运",
|
||||
"keywords": ["中港"],
|
||||
"isDefault": False,
|
||||
},
|
||||
{
|
||||
"templateId": "d1",
|
||||
"name": "陆运默认",
|
||||
"bizType": "陆运",
|
||||
"keywords": ["急件"],
|
||||
"isDefault": True,
|
||||
},
|
||||
],
|
||||
business_line="LAND",
|
||||
facts={"运输类型": "国内运输拼车", "运输分类": "普件,常用线路/常规车型"},
|
||||
)
|
||||
self.assertIsNotNone(hit)
|
||||
self.assertEqual(hit.match_type, "default")
|
||||
self.assertEqual(hit.name, "陆运默认")
|
||||
|
||||
def test_land_none_without_default(self) -> None:
|
||||
hit = pick_quote_template(
|
||||
templates=[
|
||||
{
|
||||
"templateId": "hk",
|
||||
"name": "中港",
|
||||
"bizType": "陆运",
|
||||
"keywords": ["中港"],
|
||||
"isDefault": False,
|
||||
},
|
||||
],
|
||||
business_line="LAND",
|
||||
facts={"运输类型": "国内运输拼车"},
|
||||
)
|
||||
self.assertIsNone(hit)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
陆运 TMS 出站拼参:分类和中港装载方式不得带出。
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
from agent.schema.tms_land_query import assemble_land_query
|
||||
|
||||
|
||||
class TmsLandQueryTest(unittest.TestCase):
|
||||
def test_outbound_omits_class_and_hk_load(self):
|
||||
facts = {
|
||||
"运输类型": "中港",
|
||||
"线路类别": "东南亚",
|
||||
"运输分类": "急件",
|
||||
"中港装载方式": "零担/集拼",
|
||||
"起运港": "广州",
|
||||
"目的港": "香港",
|
||||
"品名": "衣服",
|
||||
"车型/数量": "9米6/1",
|
||||
"通关口岸": "皇岗",
|
||||
}
|
||||
got = assemble_land_query(facts)
|
||||
self.assertTrue(got["ok"])
|
||||
out = got["outbound"]
|
||||
self.assertEqual(out["landTransportType"], "CHINA_HK")
|
||||
self.assertEqual(out["routeCategory"], "SOUTHEAST_ASIA")
|
||||
self.assertNotIn("landRouteCategory", out)
|
||||
self.assertEqual(out["vehicleType"], "9米6")
|
||||
self.assertEqual(str(out["quantity"]), "1")
|
||||
self.assertEqual(out["customsPort"], "皇岗")
|
||||
self.assertNotIn("运输分类", out)
|
||||
self.assertNotIn("landClass", out)
|
||||
self.assertNotIn("hkLoadType", out)
|
||||
self.assertNotIn("中港装载方式", out)
|
||||
|
||||
def test_hk_full_truck_omits_vehicle(self):
|
||||
facts = {
|
||||
"运输类型": "中港",
|
||||
"线路类别": "东南亚",
|
||||
"中港装载方式": "整车",
|
||||
"起运港": "广州",
|
||||
"目的港": "香港",
|
||||
"品名": "衣服",
|
||||
}
|
||||
got = assemble_land_query(facts)
|
||||
self.assertTrue(got["ok"])
|
||||
out = got["outbound"]
|
||||
self.assertNotIn("vehicleType", out)
|
||||
self.assertNotIn("quantity", out)
|
||||
|
||||
def test_domestic_ftl_splits_combo(self):
|
||||
facts = {
|
||||
"运输类型": "国内运输整车",
|
||||
"线路类别": "国内长途/零担",
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"车型/数量": "9米6*2",
|
||||
}
|
||||
out = assemble_land_query(facts)["outbound"]
|
||||
self.assertEqual(out["vehicleType"], "9米6")
|
||||
self.assertEqual(out["quantity"], "2")
|
||||
|
||||
def test_domestic_has_no_customs(self):
|
||||
facts = {
|
||||
"运输类型": "国内运输拼车",
|
||||
"线路类别": "国内长途/零担",
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
}
|
||||
out = assemble_land_query(facts)["outbound"]
|
||||
self.assertNotIn("customsPort", out)
|
||||
self.assertIn("volumeCbm", out)
|
||||
self.assertEqual(out["landTransportType"], "DOMESTIC_LTL")
|
||||
self.assertEqual(out["routeCategory"], "DOMESTIC_LONG_LTL")
|
||||
|
||||
def test_wo011_pinche_city_dist_uses_tms_enums(self):
|
||||
facts = {
|
||||
"运输类型": "国内运输拼车",
|
||||
"线路类别": "国内城配/拖车/打包",
|
||||
"起运港": "泉州市",
|
||||
"目的港": "南京市",
|
||||
"品名": "通信机柜及线缆配件",
|
||||
"体积": "10CBM",
|
||||
"毛重": "10KG",
|
||||
}
|
||||
got = assemble_land_query(facts)
|
||||
self.assertTrue(got["ok"])
|
||||
out = got["outbound"]
|
||||
self.assertEqual(out["landTransportType"], "DOMESTIC_LTL")
|
||||
self.assertEqual(out["routeCategory"], "DOMESTIC_CITY_DIST")
|
||||
self.assertNotIn("landRouteCategory", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user