Files
inquiry_robot/inquiry-agent/tests/test_land_text_inquiry.py

780 lines
29 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("Step1: 请复制以下内容,粘贴到输入框;", text)
self.assertIn("---请复制以下内容---", text)
self.assertNotIn("请补充陆运类型:", text)
self.assertFalse(any(x.get("msgtype") == "image" for x in self.extras))
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("Step1: 请复制以下内容,粘贴到输入框;", self._joined())
self.assertIn("---请复制以下内容---", self._joined())
self.assertFalse(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("体积(CBM)(必填)", 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)
wo = ledger.last_created_no
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))
class LandSkipAfterRestartTests(unittest.TestCase):
def test_skip_collab_after_restart_from_ledger(self) -> None:
"""
陆运已报价主账也会收成 wait_adopt。进程无书签时点「不需要协同」仍须出方案卡。
"""
ledger = MemoryLedger()
facts = {
"起运港": "广州",
"目的港": "深圳",
"品名": "衣服",
"体积": "100CBM",
"毛重": "50kg",
"运输类型": "国内运输拼车",
"线路类别": "国内长途/零担",
"运输分类": "急件",
}
created = ledger.create_ticket(
sender_id="u-land-skip", business_line="LAND", facts=facts
)
wo = created["work_order_no"]
ledger.upsert_quote(
work_order_no=wo,
quote={"total": "CNY 800", "source_label": "TMS 标准报价"},
to_status="已报价",
)
view = ledger.get_for_agent(work_order_no=wo, sender_id="u-land-skip")
self.assertEqual(view.get("wait_phase"), "wait_adopt")
restarted = LandTextInquiryFlow(ledger)
replies: list[str] = []
def reply(text: str, extra=None) -> str:
replies.append(text)
_ = extra
return ""
phase = restarted.on_button(
sender_id="u-land-skip",
action=copy.BTN_SKIP_COLLAB,
reply=reply,
card_meta={
"event_key": f"skip_collab:{wo}",
"task_id": f"price-{wo}-land",
"response_code": "rc-land-skip",
},
)
self.assertEqual(phase, "wait_adopt")
joined = "\n".join(replies)
self.assertIn("已跳过内部协同", joined)
self.assertIn(f"{wo}报价方案", joined)
class LandContinuePullCardTests(unittest.TestCase):
def test_tms_miss_resume_shows_pull_card(self) -> None:
"""陆运没查到价时继续工单,要再出「是否拉产品进群」卡,不能出空运无价卡。"""
flow = LandTextInquiryFlow(MemoryLedger())
flow._save(
FlowSession(
sender_id="land-miss",
thread_id="land-miss",
phase="tms_miss",
business_line="LAND",
work_order_no="WO202610040004",
status="询价中",
allowed=(copy.BTN_PULL_COLLAB,),
)
)
replies: list[str] = []
extras: list[dict] = []
def reply(text: str, extra=None) -> str:
replies.append(text)
extras.append(dict(extra) if extra else {})
return ""
phase = flow.resume_historical(
sender_id="land-miss", work_order_no="WO202610040004", reply=reply
)
self.assertEqual(phase, "tms_miss")
joined = "\n".join(replies)
self.assertIn("是否需要拉内部产品人员进群", joined)
self.assertIn(copy.BTN_PULL_COLLAB, joined)
buttons = (extras[-1].get("template_card") or {}).get("button_list") or []
self.assertTrue(any(copy.BTN_PULL_COLLAB == b.get("text") for b in buttons))
def test_priced_resume_shows_pull_card(self) -> None:
"""陆运已有价、还在问要不要拉群时,继续工单要出带拉群按钮的价格卡。"""
flow = LandTextInquiryFlow(MemoryLedger())
flow._save(
FlowSession(
sender_id="land-hit",
thread_id="land-hit",
phase="wait_collab",
business_line="LAND",
work_order_no="WO202610040004",
status="已报价",
quote={"total": "CNY 800", "source_label": "TMS 标准报价"},
allowed=(copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB),
)
)
replies: list[str] = []
def reply(text: str, extra=None) -> str:
replies.append(text)
_ = extra
return ""
phase = flow.resume_historical(
sender_id="land-hit", work_order_no="WO202610040004", reply=reply
)
self.assertEqual(phase, "wait_collab")
joined = "\n".join(replies)
self.assertIn("是否拉产品人员进群协同", joined)
self.assertIn(copy.BTN_PULL_COLLAB, joined)
if __name__ == "__main__":
unittest.main()