成交走询价消息服务;陆运/海运私聊下一句能记下原因;协商中回执写明顺延三个工作日和下次 9:00。 Co-authored-by: Cursor <cursoragent@cursor.com>
670 lines
25 KiB
Python
670 lines
25 KiB
Python
"""
|
|
陆运私聊:选项补齐 → 货物补问 → 无工单号核对卡。确定建单见后续用例。
|
|
|
|
不连企微 / 真实 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.air_text_flow import FlowSession
|
|
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] = []
|
|
self.extras: list[dict] = []
|
|
|
|
def _reply(self, text: str, extra=None) -> str:
|
|
self.replies.append(text)
|
|
self.extras.append(dict(extra or {}))
|
|
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.assertIn("中港整车", text)
|
|
self.assertIn("填写样例见图", text)
|
|
self.assertNotIn("请补充陆运类型:", text)
|
|
img = [x for x in self.extras if x.get("msgtype") == "image"]
|
|
self.assertEqual(len(img), 1)
|
|
self.assertTrue(os.path.isfile(str(img[0].get("filepath") or "")))
|
|
|
|
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())
|
|
self.assertIn("填写样例见图", self._joined())
|
|
self.assertTrue(any(x.get("msgtype") == "image" for x in self.extras))
|
|
|
|
def test_unlinked_route_still_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())
|
|
self.assertNotIn("填写样例", self._joined())
|
|
sess = self.flow.session_of("u1")
|
|
self.assertEqual(sess.facts.get("运输类型"), "国内运输拼车")
|
|
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.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_wait_confirm_can_fill_customs_port(self) -> None:
|
|
"""核对卡空着通关口岸时,再发「通关口岸:皇岗」必须写进下一张卡。"""
|
|
self.flow.on_text(
|
|
sender_id="u1",
|
|
text="跨境集拼 东南亚 急件",
|
|
reply=self._reply,
|
|
injected_facts={
|
|
"起运港": "广州",
|
|
"目的港": "河内",
|
|
"品名": "衣服",
|
|
"体积": "100CBM",
|
|
"毛重": "50kg",
|
|
},
|
|
injected_mode="LAND",
|
|
)
|
|
self.replies.clear()
|
|
phase = self.flow.on_text(
|
|
sender_id="u1",
|
|
text="通关口岸:皇岗",
|
|
reply=self._reply,
|
|
)
|
|
self.assertEqual(phase, "wait_confirm")
|
|
self.assertIn("通关口岸(非必填):皇岗", self._joined())
|
|
self.assertEqual(self.flow.session_of("u1").facts.get("通关口岸"), "皇岗")
|
|
|
|
def test_wait_confirm_fills_customs_from_card_label(self) -> None:
|
|
"""销售按卡片原样写「通关口岸(非必填):皇岗」也要收下。"""
|
|
self.flow.on_text(
|
|
sender_id="u1",
|
|
text="中港整车 中港/中亚/中欧 急件",
|
|
reply=self._reply,
|
|
injected_facts={
|
|
"起运港": "广州",
|
|
"目的港": "香港",
|
|
"品名": "衣服",
|
|
},
|
|
injected_mode="LAND",
|
|
)
|
|
self.replies.clear()
|
|
phase = self.flow.on_text(
|
|
sender_id="u1",
|
|
text="通关口岸(非必填):皇岗",
|
|
reply=self._reply,
|
|
)
|
|
self.assertEqual(phase, "wait_confirm")
|
|
self.assertEqual(self.flow.session_of("u1").facts.get("通关口岸"), "皇岗")
|
|
|
|
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())
|
|
self.assertIn("体积(必填)", self._joined())
|
|
self.assertNotIn("通关口岸", 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_pull_rejects_unlisted_pair(self) -> None:
|
|
self.ledger.set_land_staff(
|
|
[
|
|
{"roleCode": "land", "status": "active", "wecomId": "p_sea", "name": "甲", "routes": "东南亚"},
|
|
]
|
|
)
|
|
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()
|
|
phase = self.flow.on_button(
|
|
sender_id="u1",
|
|
action=copy.BTN_PULL_COLLAB,
|
|
reply=self._reply,
|
|
)
|
|
self.assertEqual(self.groups.created, [])
|
|
self.assertIn("拉群失败,请排查填写内容,并重新发起工单。", "\n".join(self.replies))
|
|
self.assertNotEqual(phase, "collab_group")
|
|
|
|
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))
|
|
|
|
|
|
class LandLostReasonTests(unittest.TestCase):
|
|
"""陆运私聊未成交后,下一句任意文字要记下原因,不能当新询价丢掉。"""
|
|
|
|
def test_land_private_next_text_is_lost_reason(self) -> None:
|
|
ledger = MemoryLedger()
|
|
created = ledger.create_ticket(
|
|
sender_id="WuJiLin",
|
|
business_line="LAND",
|
|
facts={"起运地": "泉州", "目的地": "南京", "线路类别": "国内城配/拖车/打包"},
|
|
)
|
|
wo = str(created.get("work_order_no") or "")
|
|
ledger.transition(work_order_no=wo, to_status="已报价")
|
|
flow = LandTextInquiryFlow(ledger)
|
|
flow._save(
|
|
FlowSession(
|
|
sender_id="WuJiLin",
|
|
thread_id="WuJiLin",
|
|
phase="wait_deal",
|
|
business_line="LAND",
|
|
work_order_no=wo,
|
|
allowed=(copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE),
|
|
)
|
|
)
|
|
replies: list[str] = []
|
|
phase = flow.on_button(
|
|
sender_id="WuJiLin",
|
|
action=copy.BTN_LOST,
|
|
reply=lambda t, extra=None: replies.append(t),
|
|
)
|
|
self.assertEqual(phase, "wait_lost_reason")
|
|
self.assertEqual(ledger.get_ticket(work_order_no=wo).status, "未成交")
|
|
phase = flow.on_text(
|
|
sender_id="WuJiLin",
|
|
text="价格高",
|
|
reply=lambda t, extra=None: replies.append(t),
|
|
)
|
|
self.assertEqual(phase, "done")
|
|
self.assertEqual(ledger.get_ticket(work_order_no=wo).lost_reason, "价格高")
|
|
self.assertTrue(any(t == "已记下未成交原因:价格高。" for t in replies))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|