合并 feat/continue-deal-card-20261009:报价发出后继续工单重发成交卡;空运锁舱补参与看板滚动一并收下。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -160,7 +160,7 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
"source_label": "TMS 标准报价",
|
||||
"quoteId": "QT-AIR-1",
|
||||
"airOptions": [
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ", "airlineName": "南航"}
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ", "airlineName": "南航", "flightNo": "CZ327"}
|
||||
],
|
||||
}
|
||||
if quote:
|
||||
@@ -203,6 +203,67 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertEqual(classify_air_group_text("帮我锁舱 WO202609160001", allow_network=False), INTENT_LOCK)
|
||||
self.assertEqual(classify_air_group_text("WO202609160001释放舱位", allow_network=False), INTENT_RELEASE)
|
||||
|
||||
def test_airline_fee_wording_is_quote_not_adjust(self) -> None:
|
||||
"""航线写「空运费改为900」是手工报价,不能先判成调价。比例和改总价仍是调价。"""
|
||||
from agent.routing.air_group_intent import INTENT_ADJUST, INTENT_QUOTE
|
||||
|
||||
self.assertEqual(
|
||||
classify_air_group_text("空运费改为900USD", allow_network=False),
|
||||
INTENT_QUOTE,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text("报关费改为300CNY", allow_network=False),
|
||||
INTENT_QUOTE,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text("空运费 9900CNY", allow_network=False),
|
||||
INTENT_QUOTE,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text("空运费加10%", allow_network=False),
|
||||
INTENT_ADJUST,
|
||||
)
|
||||
self.assertEqual(
|
||||
classify_air_group_text("总销售价改为180", allow_network=False),
|
||||
INTENT_ADJUST,
|
||||
)
|
||||
|
||||
def test_airline_rewritten_fee_does_not_say_only_sales(self) -> None:
|
||||
"""已有报价后,航线再写费用金额,不能提示只有销售能调价。"""
|
||||
wo = self._create_air()
|
||||
handle_air_group(
|
||||
self._msg(sender="air1", text=wo, chat="wr_air_1", mid="fee0"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.groups.sent.clear()
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text="空运费改为900USD", chat="wr_air_1", mid="fee1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertNotEqual(phase, "adjust_not_sales")
|
||||
self.assertNotIn("只有这张单的销售能调价", "\n".join(self._texts()))
|
||||
self.assertIn(phase, {"wait_tms_choice", "quoted"})
|
||||
|
||||
def test_sales_percent_still_adjusts(self) -> None:
|
||||
"""销售说加百分之几,仍是调价,不会被改成航线报价。"""
|
||||
wo = self._create_air()
|
||||
handle_air_group(
|
||||
self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="adj0"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.groups.sent.clear()
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="sales1", text="空运费加10%", chat="wr_air_1", mid="adj1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertNotEqual(phase, "not_airline")
|
||||
self.assertNotIn("只有这张单的销售能调价", "\n".join(self._texts()))
|
||||
self.assertIn(phase, {"group_adjusted", "adjust_need_fee", "adjust_none", "adjust_need_lines"})
|
||||
|
||||
def test_air_group_brief_matches_sea_sections(self) -> None:
|
||||
text = copy.air_group_brief(
|
||||
work_order_no="WO202609160009",
|
||||
@@ -373,6 +434,51 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertIn("费用合计:CNY 200040", tms_block)
|
||||
self.assertEqual(tms_block.count("费用合计:"), 1)
|
||||
|
||||
def test_air_group_brief_keeps_sole_air_freight_line(self) -> None:
|
||||
"""TMS 只有空运费、金额等于总价时,群摘要必须写出这一行,不能只剩费用合计。"""
|
||||
tms = {
|
||||
"source": "TMS",
|
||||
"source_label": "TMS标准报价",
|
||||
"total": "1833.7",
|
||||
"freight": "1833.70",
|
||||
"currency": "CNY",
|
||||
"valid_until": "2026-10-21",
|
||||
"fee_lines": '[{"name":"空运费","amount":"CNY 1833.70"}]',
|
||||
"feeItems": [
|
||||
{
|
||||
"feeName": "空运费",
|
||||
"feeCode": "AIR_FREIGHT",
|
||||
"amount": 1833.70,
|
||||
"unitPrice": 11.00,
|
||||
"quantity": 166.7,
|
||||
"currency": "CNY",
|
||||
}
|
||||
],
|
||||
"airOptions": [
|
||||
{
|
||||
"originAirportCode": "CGO",
|
||||
"destinationAirportCode": "KUL",
|
||||
"salePrice": 1833.70,
|
||||
"currency": "CNY",
|
||||
"departureCharges": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
text = copy.air_group_brief(
|
||||
work_order_no="WO202610080020",
|
||||
facts={"起运地": "CGO", "目的地": "吉隆坡", "originCode": "CGO"},
|
||||
quote=tms,
|
||||
collab_facts={},
|
||||
has_price=True,
|
||||
tms_quote=tms,
|
||||
)
|
||||
tms_block = text.split("TMS报价:", 1)[1]
|
||||
self.assertIn("空运费:CNY 1833.70", tms_block)
|
||||
self.assertLess(tms_block.find("空运费:"), tms_block.find("费用合计:"))
|
||||
self.assertIn("费用合计:CNY 1833.7", tms_block)
|
||||
self.assertEqual(tms_block.count("费用合计:"), 1)
|
||||
self.assertEqual(tms_block.count("费用合计:"), 1)
|
||||
|
||||
def test_work_order_plus_lost_reason_is_deal_not_activate(self) -> None:
|
||||
"""WO@助手 未成交,原因… 必须当关单,不能只激活。"""
|
||||
from agent.routing.air_group_intent import INTENT_ACTIVATE, INTENT_LOST
|
||||
@@ -636,6 +742,45 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(phase, "need_work_order")
|
||||
|
||||
def test_activated_multi_quote_follows_without_work_order(self) -> None:
|
||||
"""多段激活后,报价不必再带工单号,也不要回「请先 @我」。"""
|
||||
import json
|
||||
|
||||
created = self.ledger.create_ticket(
|
||||
sender_id="sales1",
|
||||
business_line="MULTI",
|
||||
facts={
|
||||
"segments_json": json.dumps(
|
||||
[
|
||||
{"index": 1, "mode": "LAND", "facts": {"起运港": "北京", "目的港": "上海"}},
|
||||
{"index": 2, "mode": "AIR", "facts": {"起运港": "上海", "目的港": "北宁"}},
|
||||
],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
},
|
||||
)
|
||||
wo = created["work_order_no"]
|
||||
self.ledger.bind_collab_group(
|
||||
work_order_no=wo,
|
||||
chat_id="wr_multi",
|
||||
member_ids=["sales1", "air1"],
|
||||
product_ids=["air1"],
|
||||
product_names=["HangXian"],
|
||||
)
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text="空运费 100USD", chat="wr_multi", mid="mq1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="product_quote",
|
||||
)
|
||||
self.assertEqual(phase, "segment_quoted")
|
||||
joined = "\n".join(self._texts())
|
||||
self.assertNotIn("请先 @我", joined)
|
||||
self.assertNotIn("并发送要跟的工单号", joined)
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
air_quote = (ticket.quote or {}).get("segment_quotes", {}).get("AIR") or {}
|
||||
self.assertTrue(air_quote.get("total") or air_quote.get("fee_rows"))
|
||||
|
||||
def test_sales_can_fill_battery(self) -> None:
|
||||
wo = self._create_air()
|
||||
handle_air_group(
|
||||
@@ -944,35 +1089,146 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "lock_incomplete")
|
||||
joined = "\n".join(self._texts())
|
||||
self.assertIn("舱位编号", joined)
|
||||
self.assertFalse(any("执行成功" in t for t in self._texts()))
|
||||
|
||||
def test_manual_quote_with_option_airline_can_lock(self) -> None:
|
||||
"""人工价补齐舱位+航司+航班后可锁;计费重用工单毛重兜底。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "100",
|
||||
"source": "manual",
|
||||
"source_label": "产品报价",
|
||||
"currency": "CNY",
|
||||
},
|
||||
status="已报价",
|
||||
)
|
||||
self.assertEqual(phase, "wait_lock_params")
|
||||
phase = handle_air_group(
|
||||
self._msg(
|
||||
sender="air1",
|
||||
text=f"{wo}锁舱,锁AIR-OPT01 CZ 航班CZ327",
|
||||
chat="wr_air_lock_manual2",
|
||||
text="AIR-OPT01 CZ 航班CZ327",
|
||||
chat="wr_air_lock_manual",
|
||||
mid="lm2",
|
||||
),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_pick_option",
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertTrue(any("执行成功" in t for t in self._texts()))
|
||||
|
||||
def test_lock_missing_params_asks_and_does_not_call_tms(self) -> None:
|
||||
"""缺舱位、航司、航班号时只请补,不锁舱、不报 TMS 失败、不通知 IT。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "CNY 1800",
|
||||
"source": "TMS",
|
||||
"source_label": "TMS 标准报价",
|
||||
"quoteId": "QT-AIR-23",
|
||||
}
|
||||
)
|
||||
calls: list[dict] = []
|
||||
orig = self.ledger.lock_cabin
|
||||
|
||||
def spy(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return orig(**kwargs)
|
||||
|
||||
self.ledger.lock_cabin = spy # type: ignore
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_ask", mid="ask1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "wait_lock_params")
|
||||
self.assertEqual(calls, [])
|
||||
asked = self._texts()[-1]
|
||||
self.assertEqual(
|
||||
asked,
|
||||
"锁舱参数不完整,缺少:舱位编号、航司、航班号。请补充后再锁,例如:AIR-OPT01,南航,航班CZ327。",
|
||||
)
|
||||
self.assertNotIn("已通知", asked)
|
||||
self.assertNotIn("锁舱失败", asked)
|
||||
phase = handle_air_group(
|
||||
self._msg(
|
||||
sender="air1",
|
||||
text="AIR-OPT01 CZ 航班CZ327",
|
||||
chat="wr_air_lock_ask",
|
||||
mid="ask2",
|
||||
),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_pick_option",
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0].get("option_no"), "AIR-OPT01")
|
||||
self.assertEqual(calls[0].get("airline"), "CZ")
|
||||
self.assertEqual(calls[0].get("flight_no"), "CZ327")
|
||||
|
||||
def test_lock_accepts_free_wording_not_only_sample_format(self) -> None:
|
||||
"""补舱位不限格式。点名舱位、航司、航班号,换顺序、用航司中文名,也要锁。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "CNY 1800",
|
||||
"source": "TMS",
|
||||
"source_label": "TMS 标准报价",
|
||||
"quoteId": "QT-AIR-24",
|
||||
}
|
||||
)
|
||||
calls: list[dict] = []
|
||||
orig = self.ledger.lock_cabin
|
||||
|
||||
def spy(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return orig(**kwargs)
|
||||
|
||||
self.ledger.lock_cabin = spy # type: ignore
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_free", mid="free1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "wait_lock_params")
|
||||
self.assertEqual(calls, [])
|
||||
phase = handle_air_group(
|
||||
self._msg(
|
||||
sender="air1",
|
||||
text="航班号:CZ327,航司南航,舱位编号是AIR-OPT01",
|
||||
chat="wr_air_lock_free",
|
||||
mid="free2",
|
||||
),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertEqual(calls[0].get("option_no"), "AIR-OPT01")
|
||||
self.assertEqual(calls[0].get("airline"), "CZ")
|
||||
self.assertEqual(calls[0].get("flight_no"), "CZ327")
|
||||
|
||||
def test_lock_sentence_with_labeled_params_locks_immediately(self) -> None:
|
||||
"""同一句里已经说齐三项,直接锁,不再要求按示例格式重说。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "CNY 1800",
|
||||
"source": "manual",
|
||||
"source_label": "产品报价",
|
||||
}
|
||||
)
|
||||
calls: list[dict] = []
|
||||
orig = self.ledger.lock_cabin
|
||||
|
||||
def spy(**kwargs):
|
||||
calls.append(kwargs)
|
||||
return orig(**kwargs)
|
||||
|
||||
self.ledger.lock_cabin = spy # type: ignore
|
||||
phase = handle_air_group(
|
||||
self._msg(
|
||||
sender="air1",
|
||||
text=f"{wo}锁舱,舱位 AIR-OPT02,东方航空,航班号 MU5123",
|
||||
chat="wr_air_lock_once",
|
||||
mid="once1",
|
||||
),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertTrue(any("执行成功" in t for t in self._texts()))
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0].get("option_no"), "AIR-OPT02")
|
||||
self.assertEqual(calls[0].get("airline"), "MU")
|
||||
self.assertEqual(calls[0].get("flight_no"), "MU5123")
|
||||
self.assertFalse(any("锁舱参数不完整" in t for t in self._texts()))
|
||||
|
||||
def test_lock_success_and_release_after_deal_blocked(self) -> None:
|
||||
wo = self._create_air()
|
||||
@@ -1060,8 +1316,8 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
"source": "TMS",
|
||||
"quoteId": "QT-AIR-2",
|
||||
"airOptions": [
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ"},
|
||||
{"optionNo": "AIR-OPT-02", "airlineCode": "MU"},
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ", "flightNo": "CZ327"},
|
||||
{"optionNo": "AIR-OPT-02", "airlineCode": "MU", "flightNo": "MU5123"},
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -1071,7 +1327,7 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "wait_lock_option")
|
||||
self.assertEqual(phase, "wait_lock_params")
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text="AIR-OPT-02", chat="wr_air_1", mid="m2"),
|
||||
flow=self.flow,
|
||||
@@ -1166,6 +1422,31 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertIn("请确认成交状态", texts)
|
||||
self.assertFalse(any(x.get("kind") == "file" for x in added))
|
||||
|
||||
def test_reactivate_quoted_tms_file_is_deal_not_sheet(self) -> None:
|
||||
"""TMS 价的报价单已经发出、停在成交跟进:再发工单号只回成交跟进。"""
|
||||
wo = self._create_air(
|
||||
quote={
|
||||
"total": "CNY 3500.70",
|
||||
"source": "TMS",
|
||||
"source_label": "TMS 标准报价",
|
||||
"quoteId": "QT-AIR-FILE",
|
||||
"quoteFileIssued": True,
|
||||
"airOptions": [
|
||||
{"optionNo": "AIR-OPT-01", "airlineCode": "CZ", "flightNo": "CZ327"}
|
||||
],
|
||||
}
|
||||
)
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="react-deal"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertEqual(phase, "activated_deal")
|
||||
texts = "\n".join(self._texts())
|
||||
self.assertIn("请确认成交状态", texts)
|
||||
self.assertNotIn("询价字段:", texts)
|
||||
self.assertFalse(any(x.get("kind") == "file" for x in self.groups.sent))
|
||||
|
||||
def test_image_rejected(self) -> None:
|
||||
wo = self._create_air()
|
||||
handle_air_group(
|
||||
@@ -1396,6 +1677,38 @@ class AirGroupCollabTests(unittest.TestCase):
|
||||
self.assertNotIn("报价选项不存在", joined)
|
||||
self.assertNotIn("与工单不匹配", joined)
|
||||
|
||||
def test_lock_fail_does_not_block_airline_quote(self) -> None:
|
||||
"""锁舱失败后,航线再报手工价必须收下,不能回 TMS 接口异常。"""
|
||||
wo = self._create_air()
|
||||
handle_air_group(
|
||||
self._msg(sender="air1", text=wo, chat="wr_air_lock_quote", mid="lq0"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.ledger._cabin_lock_result = {
|
||||
"ok": False,
|
||||
"error": "lock_fail",
|
||||
"failReason": "该航班日期不存在可锁定的配载记录",
|
||||
}
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_quote", mid="lq1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="air_lock",
|
||||
)
|
||||
self.assertEqual(phase, "system_exception")
|
||||
self.groups.sent.clear()
|
||||
phase = handle_air_group(
|
||||
self._msg(sender="air1", text="空运费改为900USD", chat="wr_air_lock_quote", mid="lq2"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent="product_quote",
|
||||
)
|
||||
self.assertNotEqual(phase, "system_exception_paused")
|
||||
joined = "\n".join(self._texts())
|
||||
self.assertNotIn("TMS接口异常", joined)
|
||||
self.assertIn(phase, {"wait_tms_choice", "quoted"})
|
||||
|
||||
def test_lock_without_quote_still_shows_tms_reason(self) -> None:
|
||||
"""TMS 拒绝锁舱时原样回失败原因,不要改写成选项不匹配。"""
|
||||
wo = self._create_air()
|
||||
|
||||
@@ -106,6 +106,82 @@ class AirRouteOptionsTests(unittest.TestCase):
|
||||
self.assertEqual(sess.facts.get("起运港"), "深圳")
|
||||
self.assertIn("SZX-CRK", "\n".join(replies[-2:]))
|
||||
|
||||
def test_hangzhou_query_uses_airport_and_skips_other_cities(self) -> None:
|
||||
"""写了杭州就不能按目的地把南京、深圳、郑州拿来选。"""
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy.air_text_flow import AirTextInquiryFlow
|
||||
from agent.schema.tms_air_query import assemble_air_query
|
||||
|
||||
ready = assemble_air_query({"起运港": "杭州", "目的港": "吉隆坡"})
|
||||
self.assertEqual(ready["facts"].get("originCode"), "HGH")
|
||||
self.assertEqual(ready["facts"].get("destinationCode"), "KUL")
|
||||
|
||||
class Ledger(MemoryLedger):
|
||||
def query_tms(self, **kwargs):
|
||||
out = super().query_tms(**kwargs)
|
||||
quote = out["quote"]
|
||||
quote["total"] = "1750.35"
|
||||
quote["currency"] = "CNY"
|
||||
quote["airOptions"] = [
|
||||
{"originAirportCode": "NKG", "destinationAirportCode": "KUL", "salePrice": 1750.35, "currency": "CNY"},
|
||||
{"originAirportCode": "SZX", "destinationAirportCode": "KUL", "salePrice": 1833.7, "currency": "CNY"},
|
||||
{"originAirportCode": "CGO", "destinationAirportCode": "KUL", "salePrice": 1833.7, "currency": "CNY"},
|
||||
]
|
||||
return out
|
||||
|
||||
flow = AirTextInquiryFlow(Ledger())
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> None:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
|
||||
facts = {
|
||||
"起运港": "杭州",
|
||||
"目的港": "吉隆坡",
|
||||
"品名": "化妆品",
|
||||
"件数": "12",
|
||||
"毛重": "21",
|
||||
"体积": "1",
|
||||
"包装方式": "散货",
|
||||
}
|
||||
self.assertEqual(
|
||||
flow.on_text(sender_id="u_hgh", text="空运", reply=reply, injected_facts=facts, injected_mode="AIR"),
|
||||
"wait_confirm",
|
||||
)
|
||||
phase = flow.on_text(sender_id="u_hgh", text="确定", reply=reply)
|
||||
self.assertEqual(phase, "tms_miss")
|
||||
self.assertFalse(any("条线路" in x for x in replies))
|
||||
self.assertEqual(flow.session_of("u_hgh").facts.get("originCode"), "HGH")
|
||||
|
||||
def test_stated_origin_quotes_directly(self) -> None:
|
||||
"""起运地已经是珠海时,即使 TMS 还带回别的城市,也不再让销售选线。"""
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy.air_text_flow import AirTextInquiryFlow
|
||||
|
||||
flow = AirTextInquiryFlow(MemoryLedger())
|
||||
replies: list[str] = []
|
||||
|
||||
def reply(text: str, extra=None) -> None:
|
||||
replies.append(text)
|
||||
_ = extra
|
||||
|
||||
facts = {
|
||||
"起运港": "珠海",
|
||||
"目的港": "克拉克",
|
||||
"品名": "茶叶",
|
||||
"件数": "10",
|
||||
"毛重": "100",
|
||||
"体积": "2",
|
||||
"包装方式": "托盘",
|
||||
"tms_multi": "1",
|
||||
}
|
||||
flow.on_text(sender_id="u_zuh", text="空运", reply=reply, injected_facts=facts, injected_mode="AIR")
|
||||
phase = flow.on_text(sender_id="u_zuh", text="确定", reply=reply)
|
||||
self.assertEqual(phase, "wait_collab")
|
||||
self.assertFalse(any("条线路" in x for x in replies))
|
||||
self.assertEqual(flow.session_of("u_zuh").facts.get("起运港"), "珠海")
|
||||
|
||||
def test_reselect_after_adopt_voids_old_file(self) -> None:
|
||||
"""采用后再改选:旧报价单作废,回到方案卡重新采用。"""
|
||||
ledger = MemoryLedger()
|
||||
@@ -331,7 +407,10 @@ class AirRouteOptionsTests(unittest.TestCase):
|
||||
self.assertIn("closeWindow", ok)
|
||||
self.assertIn("wecom-jssdk", ok)
|
||||
self.assertIn("jsapi-signature", ok)
|
||||
self.assertNotIn("window.close()", ok)
|
||||
mobile = ok.split("function closeMobile()")[1].split("function closeDesktop()")[0]
|
||||
desktop = ok.split("function closeDesktop()")[1].split("function boot()")[0]
|
||||
self.assertNotIn("window.close()", mobile)
|
||||
self.assertIn("window.close()", desktop)
|
||||
self.assertNotIn("closeWindow", dead)
|
||||
|
||||
|
||||
|
||||
@@ -900,6 +900,10 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
self.assertNotIn("引用历史工单号", sub)
|
||||
self.assertNotIn("工单状态", sub)
|
||||
self.assertTrue(sub.endswith(copy.collab_hint("WO202609140001")))
|
||||
self.assertIn(
|
||||
"请手动将相关方拉到空运原有协同群,并在空运协同群中发送“ WO202609140001@询价小助手”,谢谢。",
|
||||
sub,
|
||||
)
|
||||
self.assertNotIn("内部协同", json.dumps(card, ensure_ascii=False))
|
||||
|
||||
def test_long_fee_lines_go_to_text_not_clipped_card(self) -> None:
|
||||
@@ -1555,7 +1559,8 @@ class AirTextInquiryTests(unittest.TestCase):
|
||||
source="test",
|
||||
)
|
||||
self.assertEqual(replay, "wait_deal")
|
||||
self.assertNotIn("咨询成交状态", "\n".join(again))
|
||||
self.assertIn("咨询成交状态", "\n".join(again))
|
||||
self.assertNotIn("请稍候", "\n".join(again))
|
||||
self.assertEqual(http.session_of("u-pdf-x").phase, "wait_deal")
|
||||
|
||||
def test_deal_click_works_while_http_still_wait_file(self) -> None:
|
||||
|
||||
@@ -108,6 +108,12 @@ class AttachmentUploadH5Tests(unittest.TestCase):
|
||||
self.assertIn("closeWindow", ok)
|
||||
self.assertIn("wecom-jssdk", ok)
|
||||
self.assertIn("jsapi-signature", ok)
|
||||
self.assertIn("function isPhone()", ok)
|
||||
self.assertIn("WeixinJSBridge.invoke", ok)
|
||||
mobile = ok.split("function closeMobile()")[1].split("function closeDesktop()")[0]
|
||||
desktop = ok.split("function closeDesktop()")[1].split("function boot()")[0]
|
||||
self.assertNotIn("window.close()", mobile)
|
||||
self.assertIn("window.close()", desktop)
|
||||
self.assertNotIn("closeWindow", page)
|
||||
self.assertNotIn("closeWindow", dead)
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ class ContinueTicketTests(unittest.TestCase):
|
||||
sender_id="u6", work_order_no=wo, reply=reply
|
||||
)
|
||||
self.assertEqual(phase, "wait_deal")
|
||||
self.assertFalse(any("成交" in x or copy.BTN_DEAL in x for x in replies))
|
||||
self.assertTrue(any("【成交】【未成交】【协商中】" in x for x in replies))
|
||||
|
||||
def test_resume_spoken_deal_closes(self) -> None:
|
||||
"""报价文件已出后,原话写工单号加已成交,要入账并回复,不能没反应。"""
|
||||
|
||||
@@ -11,6 +11,7 @@ from zoneinfo import ZoneInfo
|
||||
from agent.policy.deal_followup import (
|
||||
SHANGHAI,
|
||||
air_route_keys,
|
||||
is_workday,
|
||||
leader_matches,
|
||||
next_followup_at,
|
||||
shift_workdays,
|
||||
@@ -20,33 +21,42 @@ from agent.policy.deal_followup import (
|
||||
|
||||
class TestWorkdays(unittest.TestCase):
|
||||
def test_skip_weekend(self) -> None:
|
||||
# 2026-09-18 周五,顺延 3 个工作日 → 21、22、23,第四个工作日 24
|
||||
self.assertEqual(shift_workdays(date(2026, 9, 18), 3), date(2026, 9, 23))
|
||||
self.assertEqual(shift_workdays(date(2026, 9, 18), 4), date(2026, 9, 24))
|
||||
# 普通周末仍跳过。2026-11-13 周五 → 16、17、18 三个工作日,第四个是 19 日。
|
||||
self.assertEqual(shift_workdays(date(2026, 11, 13), 3), date(2026, 11, 18))
|
||||
self.assertEqual(shift_workdays(date(2026, 11, 13), 4), date(2026, 11, 19))
|
||||
|
||||
def test_sept20_makeup_pulls_followup_forward(self) -> None:
|
||||
# 2026-09-18 周五。9 月 20 日周日调休上班,第四个工作日是 23 日,不是 24 日。
|
||||
self.assertEqual(shift_workdays(date(2026, 9, 18), 4), date(2026, 9, 23))
|
||||
|
||||
def test_skip_national_day(self) -> None:
|
||||
# 2026-09-30 周三,国庆休 → 第四个工作日落到节后
|
||||
# 2026-09-30 周三。10 月 1–7 日放假,8 日上班,10 日周六调休。
|
||||
# 节后第 1 个工作日 8 日,第 4 个是 12 日周一。
|
||||
fourth = shift_workdays(date(2026, 9, 30), 4)
|
||||
self.assertGreaterEqual(fourth, date(2026, 10, 9))
|
||||
self.assertNotEqual(fourth.weekday(), 5)
|
||||
self.assertNotEqual(fourth.weekday(), 6)
|
||||
self.assertEqual(fourth, date(2026, 10, 12))
|
||||
|
||||
def test_oct8_works_and_sept20_is_makeup(self) -> None:
|
||||
# 国办发明电〔2025〕7号:10 月 8 日正常上班,9 月 20 日周日调休上班。
|
||||
self.assertTrue(is_workday(date(2026, 10, 8)))
|
||||
self.assertTrue(is_workday(date(2026, 9, 20)))
|
||||
self.assertFalse(is_workday(date(2026, 10, 7)))
|
||||
|
||||
def test_next_followup_is_9am(self) -> None:
|
||||
when = next_followup_at(datetime(2026, 9, 18, 15, 30, tzinfo=SHANGHAI))
|
||||
self.assertEqual(when.hour, 9)
|
||||
self.assertEqual(when.minute, 0)
|
||||
self.assertEqual(when.tzinfo, SHANGHAI)
|
||||
self.assertEqual(when.date(), date(2026, 9, 24))
|
||||
self.assertEqual(when.date(), date(2026, 9, 23))
|
||||
|
||||
def test_skip_weekend_keep_weekday_night(self) -> None:
|
||||
start = datetime(2026, 9, 18, 17, 0, tzinfo=SHANGHAI) # 周五
|
||||
now = datetime(2026, 9, 21, 17, 0, tzinfo=SHANGHAI) # 周一
|
||||
start = datetime(2026, 11, 13, 17, 0, tzinfo=SHANGHAI) # 周五
|
||||
now = datetime(2026, 11, 16, 17, 0, tzinfo=SHANGHAI) # 周一
|
||||
# 周五 17–24 = 7h,周末不算,周一 0–17 = 17h
|
||||
self.assertAlmostEqual(workday_elapsed_hours(start, now), 24.0, places=4)
|
||||
|
||||
def test_rest_day_start_counts_from_next_workday(self) -> None:
|
||||
start = datetime(2026, 9, 19, 10, 0, tzinfo=SHANGHAI) # 周六
|
||||
now = datetime(2026, 9, 21, 10, 0, tzinfo=SHANGHAI) # 周一
|
||||
start = datetime(2026, 11, 14, 10, 0, tzinfo=SHANGHAI) # 周六
|
||||
now = datetime(2026, 11, 16, 10, 0, tzinfo=SHANGHAI) # 周一
|
||||
self.assertAlmostEqual(workday_elapsed_hours(start, now), 10.0, places=4)
|
||||
|
||||
def test_fractional_quarter_hour(self) -> None:
|
||||
|
||||
@@ -193,11 +193,11 @@ class TestDealOutcome(unittest.TestCase):
|
||||
self.assertEqual(copy.lost_reason_ack("客户不要了"), "已记下未成交原因:客户不要了。")
|
||||
self.assertEqual(copy.lost_reason_ack("价格高。"), "已记下未成交原因:价格高。")
|
||||
|
||||
def test_lost_reason_ack_fills_ticket_no_from_bot_reply(self) -> None:
|
||||
"""后台文案带 {{ticket_no}} 时必须填实值,禁止原样打出占位符。"""
|
||||
from agent.policy import bot_reply
|
||||
def test_lost_reason_ack_fills_ticket_no(self) -> None:
|
||||
"""后台模板带工单号时,必须换成真实单号,不能把占位符原样发出去。"""
|
||||
from agent.policy.bot_reply import override_catalog
|
||||
|
||||
bot_reply.override_catalog(
|
||||
override_catalog(
|
||||
{
|
||||
"lost.reason_ack": (
|
||||
"已将未成交原因“{{lost_reason}}”记录在后台,"
|
||||
@@ -206,12 +206,12 @@ class TestDealOutcome(unittest.TestCase):
|
||||
}
|
||||
)
|
||||
try:
|
||||
text = copy.lost_reason_ack("换时间", work_order_no="WO202610070018")
|
||||
self.assertIn("WO202610070018", text)
|
||||
self.assertNotIn("{{ticket_no}}", text)
|
||||
self.assertIn("换时间", text)
|
||||
self.assertEqual(
|
||||
copy.lost_reason_ack("客户不需要了", work_order_no="WO202609100001"),
|
||||
"已将未成交原因“客户不需要了”记录在后台,工单WO202609100001已关闭,智能体不再跟进该工单,请悉知。",
|
||||
)
|
||||
finally:
|
||||
bot_reply.override_catalog(None)
|
||||
override_catalog(None)
|
||||
|
||||
def test_harvest_inline_lost_reason(self) -> None:
|
||||
self.assertEqual(harvest_inline_lost_reason("未成交,原因客户不想要了"), "客户不想要了")
|
||||
|
||||
@@ -20,6 +20,7 @@ from agent.policy.event_exception import (
|
||||
WAIT_PRODUCT_QUOTE,
|
||||
conversation_of,
|
||||
format_timeout_hours,
|
||||
mentions_for_role,
|
||||
pick_app_targets,
|
||||
pick_mentions,
|
||||
timeout_code,
|
||||
@@ -270,6 +271,163 @@ class TestPoll(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(n2, 0)
|
||||
|
||||
def test_air_group_names_airline_when_members_missing(self) -> None:
|
||||
"""空运群没记下成员时,超时要 @ 航线姓名,不能写成相关人员。"""
|
||||
import importlib.util
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy.deal_followup import SHANGHAI
|
||||
|
||||
path = os.path.join(ROOT, "agent", "jobs", "event_exception_poll.py")
|
||||
spec = importlib.util.spec_from_file_location("ee_poll_air", path)
|
||||
self.assertIsNotNone(spec)
|
||||
self.assertIsNotNone(spec.loader)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
book = MemoryLedger()
|
||||
book.set_sea_staff(
|
||||
[
|
||||
{
|
||||
"wecomId": "sales1",
|
||||
"name": "罗小华",
|
||||
"roleCode": "sales",
|
||||
"status": "active",
|
||||
},
|
||||
{
|
||||
"wecomId": "air1",
|
||||
"name": "吴季林",
|
||||
"roleCode": "air",
|
||||
"status": "active",
|
||||
},
|
||||
]
|
||||
)
|
||||
created = book.create_ticket(sender_id="sales1", business_line="AIR", facts={"起运港": "南京"})
|
||||
wo = created["work_order_no"]
|
||||
book.start_event_wait(work_order_no=wo, wait_kind="air_quote")
|
||||
ticket = book.get_ticket(work_order_no=wo)
|
||||
ticket.collab_chat_id = "wr-air"
|
||||
ticket.collab_member_ids = []
|
||||
ticket.event_wait_started_at = (datetime.now(SHANGHAI) - timedelta(hours=3)).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
book._timeouts["air_route"] = 0.25
|
||||
groups: list[dict] = []
|
||||
|
||||
def send_group(*, chat_id: str, content: str, business_line: str = "") -> bool:
|
||||
groups.append({"chat_id": chat_id, "content": content, "line": business_line})
|
||||
return True
|
||||
|
||||
n = mod.poll_event_exceptions(
|
||||
ledger=book,
|
||||
now=datetime.now(SHANGHAI),
|
||||
send_group=send_group,
|
||||
notify_ready=False,
|
||||
)
|
||||
self.assertEqual(n, 1)
|
||||
self.assertEqual(len(groups), 1)
|
||||
self.assertEqual(
|
||||
groups[0]["content"],
|
||||
f"航线响应超时\n关联询价工单 {wo}。@吴季林已超过 0.25 工作小时 无响应,事件异常已记录工单,请及时处理。",
|
||||
)
|
||||
self.assertNotIn("相关人员", groups[0]["content"])
|
||||
|
||||
def test_sales_timeout_mentions_sales_not_airline(self) -> None:
|
||||
"""销售超时只 @ 本单销售。私聊也带 @,不点航线,不写相关人员。"""
|
||||
import importlib.util
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy.deal_followup import SHANGHAI
|
||||
|
||||
path = os.path.join(ROOT, "agent", "jobs", "event_exception_poll.py")
|
||||
spec = importlib.util.spec_from_file_location("ee_poll_sales", path)
|
||||
self.assertIsNotNone(spec)
|
||||
self.assertIsNotNone(spec.loader)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
book = MemoryLedger()
|
||||
book.set_sea_staff(
|
||||
[
|
||||
{"wecomId": "sales1", "name": "罗小华", "roleCode": "sales", "status": "active"},
|
||||
{"wecomId": "air1", "name": "吴季林", "roleCode": "air", "status": "active"},
|
||||
]
|
||||
)
|
||||
created = book.create_ticket(sender_id="sales1", business_line="AIR", facts={})
|
||||
wo = created["work_order_no"]
|
||||
book.start_event_wait(work_order_no=wo, wait_kind="adopt")
|
||||
ticket = book.get_ticket(work_order_no=wo)
|
||||
ticket.event_wait_started_at = (datetime.now(SHANGHAI) - timedelta(hours=3)).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
book._timeouts["air_sales"] = 0.25
|
||||
private: list[dict] = []
|
||||
|
||||
def enqueue(**kwargs):
|
||||
private.append(kwargs)
|
||||
return True, ""
|
||||
|
||||
def send_group(**kwargs) -> bool:
|
||||
raise AssertionError("销售采用超时不应发到群")
|
||||
|
||||
n = mod.poll_event_exceptions(
|
||||
ledger=book,
|
||||
now=datetime.now(SHANGHAI),
|
||||
enqueue=enqueue,
|
||||
send_group=send_group,
|
||||
notify_ready=False,
|
||||
)
|
||||
self.assertEqual(n, 1)
|
||||
self.assertEqual(len(private), 1)
|
||||
self.assertEqual(
|
||||
private[0]["content"],
|
||||
f"销售响应超时\n关联询价工单 {wo}。@罗小华已超过 0.25 工作小时 无响应,事件异常已记录工单,请及时处理。",
|
||||
)
|
||||
self.assertNotIn("吴季林", private[0]["content"])
|
||||
self.assertNotIn("相关人员", private[0]["content"])
|
||||
|
||||
created_deal = book.create_ticket(sender_id="sales1", business_line="AIR", facts={"目的港": "胡志明"})
|
||||
deal_no = created_deal["work_order_no"]
|
||||
book.start_event_wait(work_order_no=deal_no, wait_kind="deal")
|
||||
deal = book.get_ticket(work_order_no=deal_no)
|
||||
deal.collab_chat_id = "wr-air"
|
||||
deal.event_wait_started_at = ticket.event_wait_started_at
|
||||
groups: list[dict] = []
|
||||
|
||||
def send_deal_group(*, chat_id: str, content: str, business_line: str = "") -> bool:
|
||||
groups.append({"chat_id": chat_id, "content": content})
|
||||
return True
|
||||
|
||||
n_deal = mod.poll_event_exceptions(
|
||||
ledger=book,
|
||||
now=datetime.now(SHANGHAI),
|
||||
enqueue=enqueue,
|
||||
send_group=send_deal_group,
|
||||
notify_ready=False,
|
||||
)
|
||||
self.assertEqual(n_deal, 1)
|
||||
self.assertEqual(
|
||||
groups[0]["content"],
|
||||
f"销售响应超时\n关联询价工单 {deal_no}。@罗小华已超过 0.25 工作小时 无响应,事件异常已记录工单,请及时处理。",
|
||||
)
|
||||
self.assertNotIn("吴季林", groups[0]["content"])
|
||||
|
||||
|
||||
class TestMentionsForRole(unittest.TestCase):
|
||||
def test_keeps_active_named_role_only(self) -> None:
|
||||
rows = mentions_for_role(
|
||||
[
|
||||
{"wecomId": "a1", "name": "吴季林", "roleCode": "air", "status": "active"},
|
||||
{"wecomId": "s1", "name": "罗小华", "roleCode": "sales", "status": "active"},
|
||||
{"wecomId": "a2", "name": "停用航线", "roleCode": "air", "status": "disabled"},
|
||||
{"wecomId": "a3", "name": "", "roleCode": "air", "status": "active"},
|
||||
],
|
||||
role_code="air",
|
||||
)
|
||||
self.assertEqual([row["name"] for row in rows], ["吴季林"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -436,6 +436,29 @@ class ClarifyPasteTests(unittest.TestCase):
|
||||
self.assertNotIn("包装方式", facts)
|
||||
self.assertNotEqual(facts.get("包装方式"), "托盘、散货")
|
||||
|
||||
def test_blank_cargo_ready_does_not_take_next_item_number(self) -> None:
|
||||
"""货好时间空着时,下一行「4.提货地址」的编号不能写进货好时间。示例行也不算填了。"""
|
||||
text = """
|
||||
1.包装类型(必填):托盘
|
||||
(如:托盘、散货)
|
||||
2.货源地(非必填):
|
||||
(如:东莞)
|
||||
3.货好时间(非必填):
|
||||
(如:随时可提)
|
||||
4.提货地址(非必填):
|
||||
(如:东莞仓库)
|
||||
5.收货地址(非必填):
|
||||
(如:马尼拉)
|
||||
6.贸易条款(非必填):
|
||||
(如:DDP、DAP、CIF、CIP)
|
||||
"""
|
||||
facts = extract_inquiry_snapshot(text, allow_b=False)["facts"]
|
||||
self.assertEqual(facts.get("包装方式") or facts.get("包装类型"), "托盘")
|
||||
self.assertNotIn("货好时间", facts)
|
||||
self.assertNotIn("提货地址", facts)
|
||||
self.assertNotIn("货源地", facts)
|
||||
self.assertNotIn("贸易条款", facts)
|
||||
|
||||
|
||||
class ImageOptionalClipTests(unittest.TestCase):
|
||||
def test_numbered_extra_lines_do_not_enter_fields(self) -> None:
|
||||
|
||||
@@ -317,6 +317,31 @@ class LandGroupCollabTests(unittest.TestCase):
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertEqual(ticket.status, "已报价")
|
||||
|
||||
def test_product_fee_change_is_quote_not_adjust(self) -> None:
|
||||
"""产品写「报关费改为300」是手工报价。已有报价后也不能被拦成只有销售能调价。"""
|
||||
from agent.routing.group_intent import classify_group_text
|
||||
|
||||
self.assertEqual(classify_group_text("报关费改为300CNY", allow_network=False), INTENT_QUOTE)
|
||||
self.assertEqual(classify_group_text("运费加 10%", allow_network=False), INTENT_ADJUST)
|
||||
wo, _ = self._open(option_text="国内运输拼车 国内长途/零担 急件")
|
||||
handle_group_collab(
|
||||
self._msg(sender="p_dom", text="运费 1000", mid="q_fee0"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
injected_intent=INTENT_QUOTE,
|
||||
injected_quote={"total": "1000", "source_label": "产品报价"},
|
||||
)
|
||||
self.groups.sent.clear()
|
||||
phase = handle_group_collab(
|
||||
self._msg(sender="p_dom", text="报关费改为300CNY", mid="q_fee1"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertNotEqual(phase, "adjust_not_sales")
|
||||
self.assertNotIn("只有这张单的销售能调价", self._group_texts())
|
||||
ticket = self.ledger.get_ticket(work_order_no=wo)
|
||||
self.assertTrue(ticket.quote)
|
||||
|
||||
def test_product_cannot_adjust(self) -> None:
|
||||
self._open(option_text="国内运输拼车 国内长途/零担 急件")
|
||||
self.groups.sent.clear()
|
||||
@@ -327,7 +352,13 @@ class LandGroupCollabTests(unittest.TestCase):
|
||||
injected_intent=INTENT_ADJUST,
|
||||
)
|
||||
self.assertEqual(phase, "adjust_not_sales")
|
||||
self.assertIn("只有这张单的销售能调价", self._group_texts())
|
||||
self.assertIn("只有这张单的销售能调价。产品请继续报价,价格未改。", self._group_texts())
|
||||
|
||||
def test_air_room_adjust_names_airline(self) -> None:
|
||||
self.assertEqual(
|
||||
copy.adjust_only_sales(air_room=True),
|
||||
"只有这张单的销售能调价。航线请继续报价,价格未改。",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -11,6 +11,10 @@ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from agent.channel.wecom.models import InboundMessage
|
||||
from agent.handlers.group_collab import handle_group_inbound
|
||||
from agent.ledger.memory_ledger import MemoryLedger
|
||||
from agent.policy.multi_group_ops import deal_text, handle_multi_inbound
|
||||
from agent.policy.multi_segments import Segment
|
||||
@@ -387,7 +391,8 @@ class QuoteTests(unittest.TestCase):
|
||||
self.assertEqual(len(self.client.files), 2)
|
||||
self.assertTrue(self.ledger.last_render.get("ok"))
|
||||
|
||||
def test_lock_on_air_segment_does_not_wait_for_other_quotes(self) -> None:
|
||||
def test_lock_on_air_segment_says_multi_unsupported(self) -> None:
|
||||
"""多段含空运时航线锁舱,原因写成暂不支持,不再说不是空运单。"""
|
||||
created = self.ledger.create_ticket(
|
||||
sender_id="sales",
|
||||
business_line="MULTI",
|
||||
@@ -412,9 +417,128 @@ class QuoteTests(unittest.TestCase):
|
||||
text=f"{no}锁舱",
|
||||
sheet_fn=self.sheet,
|
||||
)
|
||||
self.assertEqual(phase, "lock_ok")
|
||||
self.assertEqual(phase, "multi_lock_unsupported")
|
||||
joined = "\n".join(self.client.texts)
|
||||
self.assertIn(f"工单{no} 空运锁舱失败。", joined)
|
||||
self.assertIn("操作人:航线甲", joined)
|
||||
self.assertIn("锁舱失败的原因:多段询价,暂时不支持锁舱", joined)
|
||||
self.assertNotIn("不是空运单,不能锁舱", joined)
|
||||
self.assertNotIn("已通知IT", joined)
|
||||
self.assertNotIn("请稍等", joined)
|
||||
self.assertFalse(any("其他段报完" in text for text in self.client.texts))
|
||||
|
||||
|
||||
class _Bot:
|
||||
"""替掉真实桥,记下这一次群里实际会发出去的字。"""
|
||||
|
||||
made: list["_Bot"] = []
|
||||
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
_ = args, kwargs
|
||||
self.texts: list[str] = []
|
||||
_Bot.made.append(self)
|
||||
|
||||
def send_group(self, *, chat_id: str, content: str, mention_userids: list[str] | None = None) -> dict:
|
||||
_ = chat_id, mention_userids
|
||||
self.texts.append(content)
|
||||
return {"ok": True}
|
||||
|
||||
def send_group_file(self, **kwargs: object) -> dict:
|
||||
_ = kwargs
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
class ArchiveSenderTests(unittest.TestCase):
|
||||
"""会话存档听到的报价:含空运走机器人,海运陆运仍走应用自己建的群。"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
_Bot.made = []
|
||||
self.ledger = MemoryLedger()
|
||||
self.app = _Client()
|
||||
self.flow = _Flow(self.ledger, self.app)
|
||||
|
||||
def _message(self, chat_id: str, sender_id: str, text: str) -> InboundMessage:
|
||||
return InboundMessage(
|
||||
sender_id=sender_id,
|
||||
message_id="m1",
|
||||
content=text,
|
||||
chat_id=chat_id,
|
||||
chat_type="group",
|
||||
)
|
||||
|
||||
def test_air_multi_quote_goes_through_bot(self) -> None:
|
||||
created = self.ledger.create_ticket(
|
||||
sender_id="sales",
|
||||
business_line="MULTI",
|
||||
facts={
|
||||
"segments_json": json.dumps(
|
||||
[
|
||||
{"index": 1, "mode": "LAND", "facts": {"线路类别": "国内长途/零担"}},
|
||||
{"index": 2, "mode": "AIR", "facts": {"目的港": "北宁"}},
|
||||
],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
},
|
||||
)
|
||||
self.ledger.upsert_staff({"wecomId": "sales", "name": "销售甲", "roleCode": "sales", "status": "active"})
|
||||
self.ledger.upsert_staff({"wecomId": "air1", "name": "航线甲", "roleCode": "air", "status": "active"})
|
||||
no = created["work_order_no"]
|
||||
self.ledger.bind_collab_group(
|
||||
work_order_no=no,
|
||||
chat_id="bot-room",
|
||||
member_ids=["sales", "air1"],
|
||||
product_names=["航线甲"],
|
||||
product_ids=["air1"],
|
||||
)
|
||||
with patch("agent.policy.multi_group_ops.send_filled_sheet", return_value={"ok": True}):
|
||||
with patch("agent.channel.aibot.reply.AibotReplyClient", _Bot):
|
||||
phase = handle_group_inbound(
|
||||
self._message("bot-room", "air1", "空运费 9900CNY"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertEqual(phase, "segment_quoted")
|
||||
self.assertFalse(self.app.texts)
|
||||
self.assertTrue(_Bot.made)
|
||||
self.assertTrue(any("报价单已发出" in text for text in _Bot.made[-1].texts))
|
||||
|
||||
def test_sea_land_quote_stays_on_app_client(self) -> None:
|
||||
created = self.ledger.create_ticket(
|
||||
sender_id="sales",
|
||||
business_line="MULTI",
|
||||
facts={
|
||||
"segments_json": json.dumps(
|
||||
[
|
||||
{"index": 1, "mode": "SEA", "facts": {"起运港": "广州港", "目的港": "巴生港"}},
|
||||
{"index": 2, "mode": "LAND", "facts": {"线路类别": "国内长途/零担"}},
|
||||
],
|
||||
ensure_ascii=False,
|
||||
)
|
||||
},
|
||||
)
|
||||
self.ledger.set_sea_staff(
|
||||
[{"wecomId": "sea1", "name": "海产", "roleCode": "sea", "status": "active", "routes": "海运·全部线路"}]
|
||||
)
|
||||
self.ledger.upsert_staff({"wecomId": "sales", "name": "销售甲", "roleCode": "sales", "status": "active"})
|
||||
no = created["work_order_no"]
|
||||
self.ledger.bind_collab_group(
|
||||
work_order_no=no,
|
||||
chat_id="app-room",
|
||||
member_ids=["sales", "sea1"],
|
||||
product_names=["海产"],
|
||||
product_ids=["sea1"],
|
||||
)
|
||||
with patch("agent.policy.multi_group_ops.send_filled_sheet", return_value={"ok": True}):
|
||||
with patch("agent.channel.aibot.reply.AibotReplyClient", _Bot):
|
||||
phase = handle_group_inbound(
|
||||
self._message("app-room", "sea1", "海运费 100CNY"),
|
||||
flow=self.flow,
|
||||
ledger=self.ledger,
|
||||
)
|
||||
self.assertEqual(phase, "segment_quoted")
|
||||
self.assertFalse(_Bot.made)
|
||||
self.assertTrue(any("报价单已发出" in text for text in self.app.texts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1602,7 +1602,7 @@ class SeaContinueTests(unittest.TestCase):
|
||||
phase = handle_continue_thread(msg, thread_id=wo, store=store)
|
||||
self.assertEqual(phase, "wait_deal")
|
||||
joined = "\n".join(item.content for item in store._outbox.values())
|
||||
self.assertFalse(any(x in joined for x in (copy.BTN_DEAL, "成交")))
|
||||
self.assertIn("【成交】【未成交】【协商中】", joined)
|
||||
self.assertNotIn(copy.BTN_SKIP_COLLAB, joined)
|
||||
|
||||
|
||||
|
||||
@@ -379,6 +379,9 @@ class TestConfirmSystemException(unittest.TestCase):
|
||||
)
|
||||
card = queued[0]["payload"]["template_card"]
|
||||
values = {row["keyname"]: row["value"] for row in card["horizontal_content_list"]}
|
||||
self.assertEqual(values["报错原因"], "LOCK_FAIL")
|
||||
self.assertNotIn("错误码", values)
|
||||
self.assertIn("报错原因:LOCK_FAIL", queued[0]["content"])
|
||||
self.assertEqual(values["影响范围"], "锁舱失败,工单状态保持不变")
|
||||
self.assertEqual(card["main_title"]["title"], "系统异常通知")
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class TmsLandQueryTest(unittest.TestCase):
|
||||
got = assemble_land_query(facts)
|
||||
self.assertTrue(got["ok"])
|
||||
out = got["outbound"]
|
||||
self.assertEqual(out["landTransportType"], "CHINA_HK")
|
||||
self.assertEqual(out["landTransportType"], "中港")
|
||||
self.assertEqual(out["routeCategory"], "SOUTHEAST_ASIA")
|
||||
self.assertNotIn("landRouteCategory", out)
|
||||
self.assertEqual(out["vehicleType"], "9米6")
|
||||
@@ -52,6 +52,7 @@ class TmsLandQueryTest(unittest.TestCase):
|
||||
got = assemble_land_query(facts)
|
||||
self.assertTrue(got["ok"])
|
||||
out = got["outbound"]
|
||||
self.assertEqual(out["landTransportType"], "中港")
|
||||
self.assertNotIn("vehicleType", out)
|
||||
self.assertNotIn("quantity", out)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user