""" 空运群内协同:激活当前工单、补字段、航线报价、锁舱/释放、成交。 """ 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.channel.wecom.models import InboundMessage from agent.handlers.air_group_collab import handle_air_group from agent.ledger.memory_ledger import MemoryLedger from agent.policy.air_text_flow import AirTextInquiryFlow from agent.policy import inquiry_copy as copy from agent.routing.air_group_intent import ( INTENT_LOCK, INTENT_RELEASE, classify_air_group_text, ) from agent.routing.dispatch import dispatch_inbound from agent.routing.decision import RouteDecision class FakeBotGroup: def __init__(self) -> None: self.sent: list[dict] = [] def send_group(self, *, chat_id: str, content: str, mention_userids=None): self.sent.append({"chat_id": chat_id, "content": content, "kind": "text"}) def send_group_file(self, *, chat_id: str, filename: str, filepath: str = "", file_b64: str = ""): self.sent.append({"chat_id": chat_id, "filename": filename, "kind": "file"}) return {"ok": True} class AirGroupCollabTests(unittest.TestCase): def setUp(self) -> None: from agent.redis_coord.air_inbound import reset_air_inbound_for_tests reset_air_inbound_for_tests() self.ledger = MemoryLedger() self.groups = FakeBotGroup() self.flow = AirTextInquiryFlow(ledger=self.ledger, group_client=self.groups) self.ledger.upsert_staff( { "name": "WuJiLin", "wecomId": "sales1", "roleCode": "sales", "status": "active", } ) self.ledger.upsert_staff( { "name": "HangXian", "wecomId": "air1", "roleCode": "air", "status": "active", } ) self.ledger.set_quote_templates( [ { "templateId": "air-default", "name": "空运报价", "bizType": "空运", "isDefault": True, }, { "templateId": "sea-default", "name": "海运报价", "bizType": "海运", "isDefault": True, }, ] ) def _create_air(self, *, sender="sales1", quote=None, status="已报价") -> str: created = self.ledger.create_ticket( sender_id=sender, business_line="AIR", facts={ "起运地": "上海", "目的地": "洛杉矶", "品名": "配件", "件数": "3", "毛重": "100", "体积": "2", "包装类型": "纸箱", "报价日期": "2026-09-16", }, ) wo = created["work_order_no"] if quote is None: quote = { "total": "USD 800", "source": "TMS", "source_label": "TMS 标准报价", "quoteId": "QT-AIR-1", "airOptions": [ {"optionNo": "AIR-OPT-01", "airlineCode": "CZ", "airlineName": "南航"} ], } if quote: self.ledger.upsert_quote(work_order_no=wo, quote=quote, to_status=status) return wo def _msg(self, *, sender: str, text: str, chat: str, mid: str) -> InboundMessage: return InboundMessage( sender_id=sender, message_id=mid, content=text, chat_id=chat, chat_type="group", raw={"source": "aibot"}, ) def _texts(self) -> list[str]: return [x.get("content") or "" for x in self.groups.sent] def test_intent_lock_is_not_hardcoded(self) -> None: 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_air_group_brief_matches_sea_sections(self) -> None: text = copy.air_group_brief( work_order_no="WO202609160009", facts={ "起运地": "ZUH", "目的地": "广州", "品名": "服务器", "件数": "1", "毛重": "10", "体积": "1", "包装类型": "纸箱", "报价日期": "2026-09-16", }, quote=None, collab_facts={}, has_price=False, ) self.assertIn("工单号:WO202609160009", text) self.assertIn("询价字段:\n运输方式:空运", text) self.assertIn("起运地:ZUH", text) self.assertIn("\n\n协同补充字段:\n是否含电:-", text) self.assertIn("是否含磁:-", text) self.assertIn("\n\nTMS报价:\nTMS 暂无匹配报价", text) self.assertNotIn("当前工单:", text) self.assertNotIn("贸易条款", text) def test_work_order_only_is_activate_not_quote(self) -> None: from agent.routing.air_group_intent import INTENT_ACTIVATE self.assertEqual( classify_air_group_text("WO202609160002", allow_network=False), INTENT_ACTIVATE, ) self.assertEqual( classify_air_group_text("@询价小助手 WO202609160002", allow_network=False), INTENT_ACTIVATE, ) from unittest.mock import patch with patch( "agent.routing.air_group_intent._classify_with_model", side_effect=AssertionError("纯工单号不应再打模型"), ): self.assertEqual( classify_air_group_text( "@询价小助手 WO202609160002", allow_network=True, ), INTENT_ACTIVATE, ) def test_dispatch_aibot_goes_air(self) -> None: from agent.routing.dispatch import dispatch_inbound as _disp wo = self._create_air() msg = self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="d1") # 单测注入 flow,避免打到进程单例 import agent.handlers.air_group_collab as air_mod orig = air_mod.get_air_text_flow def _fake(): return self.flow air_mod.get_air_text_flow = _fake # type: ignore try: name = _disp(msg, RouteDecision(intent="ordinary_text_other")) finally: air_mod.get_air_text_flow = orig # type: ignore self.assertEqual(name, "air_group_collab") def test_activate_and_switch(self) -> None: wo1 = self._create_air() wo2 = self._create_air() phase = handle_air_group( self._msg(sender="sales1", text=wo1, chat="wr_air_1", mid="a1"), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "activated") joined = "\n".join(self._texts()) self.assertIn(f"工单号:{wo1}", joined) self.assertIn("询价字段:", joined) self.assertIn("运输方式:空运", joined) self.assertIn("协同补充字段:", joined) self.assertIn("TMS报价:", joined) self.assertNotIn("当前工单:", joined) self.assertNotIn("询价要点:", joined) self.assertTrue(any("@WuJiLin" in t and "@HangXian" in t for t in self._texts())) self.assertEqual(getattr(self.ledger, "last_render", {}).get("templateId"), "air-default") kinds = [x.get("kind") for x in self.groups.sent] self.assertLess(kinds.index("text"), kinds.index("file")) self.assertEqual(sum(1 for x in self.groups.sent if x.get("kind") == "file"), 1) phase = handle_air_group( self._msg(sender="air1", text=wo2, chat="wr_air_1", mid="a2"), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "activated") self.assertEqual(self.ledger.find_by_collab_chat(chat_id="wr_air_1").work_order_no, wo2) def test_activate_prefills_magnet(self) -> None: created = self.ledger.create_ticket( sender_id="sales1", business_line="AIR", facts={ "起运地": "上海", "目的地": "洛杉矶", "品名": "配件", "件数": "3", "毛重": "100", "体积": "2", "包装类型": "纸箱", "报价日期": "2026-09-16", "是否含磁": "是", }, ) wo = created["work_order_no"] self.ledger.upsert_quote( work_order_no=wo, quote={"total": "USD 800", "source": "TMS", "quoteId": "QT-AIR-1"}, to_status="已报价", ) handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="pre1"), flow=self.flow, ledger=self.ledger, ) joined = "\n".join(self._texts()) self.assertIn("是否含磁:是", joined) self.assertIn("是否含电:-", joined) def test_second_group_occupied(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="o1"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_2", mid="o2"), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "occupied") self.assertTrue(any("另一个群" in t for t in self._texts())) def test_need_work_order_when_empty(self) -> None: phase = handle_air_group( self._msg(sender="sales1", text="空运费100USD", chat="wr_air_1", mid="n1"), flow=self.flow, ledger=self.ledger, injected_intent="product_quote", ) self.assertEqual(phase, "need_work_order") def test_sales_can_fill_battery(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="f0"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( self._msg(sender="sales1", text="含电 不含磁", chat="wr_air_1", mid="f1"), flow=self.flow, ledger=self.ledger, injected_intent="collab_fields", ) self.assertEqual(phase, "fields_saved") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.collab_facts.get("是否含电"), "是") self.assertEqual(ticket.collab_facts.get("是否含磁"), "否") joined = "\n".join(self._texts()) self.assertIn("工单号:", joined) self.assertIn("询价字段:", joined) self.assertIn("是否含电:是", joined) self.assertIn("是否含磁:否", joined) self.assertNotIn("已记下", joined) self.assertTrue(any("@WuJiLin" in t and "@HangXian" in t for t in self._texts())) self.assertEqual(getattr(self.ledger, "last_render", {}).get("templateId"), "air-default") fill = dict(getattr(self.ledger, "last_render", {}).get("facts") or {}) self.assertEqual(fill.get("是否含电") or fill.get("has_battery"), "是") self.assertEqual(fill.get("是否含磁") or fill.get("has_magnet"), "否") texts = [i for i, x in enumerate(self.groups.sent) if x.get("kind") == "text" and "工单号:" in (x.get("content") or "")] files = [i for i, x in enumerate(self.groups.sent) if x.get("kind") == "file"] self.assertTrue(texts and files) self.assertLess(texts[-1], files[-1]) self.assertEqual(len(files), 2) def test_sales_quote_rejected(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="q0"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( self._msg(sender="sales1", text="空运费 100USD", chat="wr_air_1", mid="q1"), flow=self.flow, ledger=self.ledger, injected_intent="product_quote", ) self.assertEqual(phase, "not_airline") def test_airline_quote_asks_tms_choice(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="air1", text=wo, chat="wr_air_1", mid="t0"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( self._msg(sender="air1", text="空运费改为900USD", chat="wr_air_1", mid="t1"), flow=self.flow, ledger=self.ledger, injected_intent="product_quote", injected_quote={"total": "USD 900", "source_label": "航线报价"}, ) self.assertEqual(phase, "wait_tms_choice") self.assertTrue(any("在 TMS 基础上改" in t or "在TMS基础上改" in t for t in self._texts())) ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.quote.get("total"), "USD 800") self.assertNotEqual(ticket.quote.get("source_label"), "航线报价") phase = handle_air_group( self._msg(sender="air1", text="在TMS基础上改", chat="wr_air_1", mid="t2"), flow=self.flow, ledger=self.ledger, injected_intent="quote_keep_tms", ) self.assertEqual(phase, "quoted") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.quote.get("total"), "USD 900") self.assertEqual(ticket.quote.get("quoteId"), "QT-AIR-1") self.assertEqual(ticket.quote.get("source_label"), "航线在TMS上改") self.assertFalse(ticket.collab_facts.get("__pending_air_quote")) joined = "\n".join(self._texts()) self.assertIn("工单号:", joined) self.assertIn("询价字段:", joined) self.assertIn("人工报价:", joined) self.assertIn("报价单已发送,请确认成交状态", joined) self.assertEqual(getattr(self.ledger, "last_render", {}).get("templateId"), "air-default") self.assertTrue(any(x.get("kind") == "file" for x in self.groups.sent)) def test_airline_quote_without_tms_sends_brief_sheet_deal(self) -> None: wo = self._create_air(quote={}, status="询价中") handle_air_group( self._msg(sender="air1", text=wo, chat="wr_air_1", mid="nq0"), 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="nq1"), flow=self.flow, ledger=self.ledger, injected_intent="product_quote", injected_quote={"total": "USD 900", "source_label": "航线报价"}, ) self.assertEqual(phase, "quoted") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.status, "已报价") self.assertEqual(ticket.quote.get("total"), "USD 900") joined = "\n".join(self._texts()) self.assertIn("工单号:", joined) self.assertIn("询价字段:", joined) self.assertIn("TMS 暂无匹配报价", joined) self.assertIn("人工报价:", joined) self.assertIn("报价单已发送,请确认成交状态", joined) self.assertNotIn("航线报价已记下", joined) self.assertEqual(getattr(self.ledger, "last_render", {}).get("templateId"), "air-default") self.assertTrue(any(x.get("kind") == "file" for x in self.groups.sent)) def test_quote_same_sentence_saves_battery(self) -> None: wo = self._create_air(quote={}, status="询价中") handle_air_group( self._msg(sender="air1", text=wo, chat="wr_air_1", mid="qs0"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( self._msg(sender="air1", text="含电 空运费900USD", chat="wr_air_1", mid="qs1"), flow=self.flow, ledger=self.ledger, injected_intent="product_quote", injected_quote={"total": "USD 900", "source_label": "航线报价"}, ) self.assertEqual(phase, "quoted") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.collab_facts.get("是否含电"), "是") self.assertIn("是否含电:是", "\n".join(self._texts())) def test_confirm_tms_saves_sheet_and_deal(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="air1", text=wo, chat="wr_air_1", mid="ct0"), flow=self.flow, ledger=self.ledger, ) self.groups.sent.clear() phase = handle_air_group( self._msg(sender="air1", text="确认报价", chat="wr_air_1", mid="ct1"), flow=self.flow, ledger=self.ledger, injected_intent="quote_confirm_tms", ) self.assertEqual(phase, "quote_confirmed") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.quote.get("source_label"), "航线确认TMS") self.assertTrue(any(f"工单{wo}的1个附件已保存到后台工单" in t for t in self._texts())) self.assertTrue(any("报价单已发送,请确认成交状态" in t for t in self._texts())) def test_stranger_cannot_fill_fields(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="sf0"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( self._msg(sender="other1", text="含电 不含磁", chat="wr_air_1", mid="sf1"), flow=self.flow, ledger=self.ledger, injected_intent="collab_fields", ) self.assertEqual(phase, "fields_denied") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertFalse(ticket.collab_facts.get("是否含电")) def test_sales_deal(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="c0"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( self._msg(sender="sales1", text="成交", chat="wr_air_1", mid="c1"), flow=self.flow, ledger=self.ledger, injected_intent="deal_won", ) self.assertEqual(phase, "done") self.assertEqual(self.ledger.get_ticket(work_order_no=wo).status, "已成交") def test_lock_requires_work_order(self) -> None: phase = handle_air_group( self._msg(sender="air1", text="锁舱", chat="wr_air_1", mid="l0"), flow=self.flow, ledger=self.ledger, injected_intent="air_lock", ) self.assertEqual(phase, "cabin_need_work_order") self.assertTrue(any("带上工单号" in t for t in self._texts())) def test_lock_without_tms_quote(self) -> None: wo = self._create_air(quote={}, status="询价中") phase = handle_air_group( self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_1", mid="l1"), 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.assertFalse(any("还没有可锁的 TMS 报价" in t for t in self._texts())) def test_manual_quote_without_quote_id_can_lock(self) -> None: wo = self._create_air( quote={ "total": "100", "source": "manual", "source_label": "产品报价", }, status="已报价", ) phase = handle_air_group( self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_lock_manual", mid="lm1"), 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())) def test_lock_success_and_release_after_deal_blocked(self) -> None: wo = self._create_air() phase = handle_air_group( self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_1", mid="l2"), 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())) handle_air_group( self._msg(sender="sales1", text="成交", chat="wr_air_1", mid="l3"), flow=self.flow, ledger=self.ledger, injected_intent="deal_won", ) phase = handle_air_group( self._msg(sender="air1", text=f"{wo}释放舱位", chat="wr_air_1", mid="l4"), flow=self.flow, ledger=self.ledger, injected_intent="air_release", ) self.assertEqual(phase, "release_deal_blocked") self.assertTrue(any("已成交" in t for t in self._texts())) def test_lost_can_release(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_1", mid="r0"), flow=self.flow, ledger=self.ledger, injected_intent="air_lock", ) handle_air_group( self._msg(sender="sales1", text="未成交", chat="wr_air_1", mid="r1"), flow=self.flow, ledger=self.ledger, injected_intent="deal_lost", ) phase = handle_air_group( self._msg(sender="air1", text=f"{wo}释放舱位", chat="wr_air_1", mid="r2"), flow=self.flow, ledger=self.ledger, injected_intent="air_release", ) self.assertEqual(phase, "release_ok") def test_sales_cannot_lock(self) -> None: wo = self._create_air() phase = handle_air_group( self._msg(sender="sales1", text=f"{wo}锁舱", chat="wr_air_1", mid="s1"), flow=self.flow, ledger=self.ledger, injected_intent="air_lock", ) self.assertEqual(phase, "not_airline") def test_dual_role_can_lock(self) -> None: self.ledger.upsert_staff( { "name": "WuJiLin", "wecomId": "sales1", "roleCode": "air", "status": "active", } ) wo = self._create_air() phase = handle_air_group( self._msg(sender="sales1", text=f"{wo}锁舱", chat="wr_air_1", mid="s2"), flow=self.flow, ledger=self.ledger, injected_intent="air_lock", ) self.assertEqual(phase, "lock_ok") def test_multi_option_asks(self) -> None: wo = self._create_air( quote={ "total": "USD 800", "source": "TMS", "quoteId": "QT-AIR-2", "airOptions": [ {"optionNo": "AIR-OPT-01", "airlineCode": "CZ"}, {"optionNo": "AIR-OPT-02", "airlineCode": "MU"}, ], } ) phase = handle_air_group( self._msg(sender="air1", text=f"{wo}锁舱", chat="wr_air_1", mid="m1"), flow=self.flow, ledger=self.ledger, injected_intent="air_lock", ) self.assertEqual(phase, "wait_lock_option") phase = handle_air_group( self._msg(sender="air1", text="AIR-OPT-02", chat="wr_air_1", mid="m2"), flow=self.flow, ledger=self.ledger, injected_intent="air_pick_option", ) self.assertEqual(phase, "lock_ok") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.collab_facts.get("__air_lock_option"), "AIR-OPT-02") def _file(self, *, sender: str, filename: str, chat: str, mid: str, text: str = "") -> InboundMessage: return InboundMessage( sender_id=sender, message_id=mid, content=text, msg_type="file", chat_id=chat, chat_type="group", media={"filename": filename, "media_id": f"mid-{mid}"}, raw={"source": "aibot"}, ) def test_file_needs_work_order(self) -> None: phase = handle_air_group( self._file(sender="air1", filename="航线报价.xlsx", chat="wr_air_1", mid="af0"), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "need_work_order") def test_sales_excel_archived_not_quote(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="af1"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( self._file(sender="sales1", filename="对账单.xlsx", chat="wr_air_1", mid="af2"), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "file_archived") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.attachments[0]["file_name"], "对账单.xlsx") self.assertNotEqual(ticket.quote.get("source_label"), "航线附件报价") self.assertTrue(any(f"工单{wo}的1个附件已保存到后台工单" in t for t in self._texts())) def test_airline_excel_quotes_and_keeps_tms_id(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="air1", text=wo, chat="wr_air_1", mid="af3"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( self._file(sender="air1", filename="航线报价.pdf", chat="wr_air_1", mid="af4"), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "airline_file_quote") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.attachments[0]["file_name"], "航线报价.pdf") self.assertEqual(ticket.quote.get("source_label"), "航线附件报价") self.assertEqual(ticket.quote.get("quoteId"), "QT-AIR-1") self.assertEqual(ticket.status, "已报价") self.assertTrue(any(f"工单{wo}的1个附件已保存到后台工单" in t for t in self._texts())) self.assertTrue(any("报价单已发送,请确认成交状态" in t for t in self._texts())) def test_image_rejected(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="air1", text=wo, chat="wr_air_1", mid="af5"), flow=self.flow, ledger=self.ledger, ) phase = handle_air_group( InboundMessage( sender_id="air1", message_id="af6", content="", msg_type="image", chat_id="wr_air_1", chat_type="group", media={"filename": "现场.jpg", "media_id": "mid-img"}, raw={"source": "aibot"}, ), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "file_type_rejected") self.assertFalse(self.ledger.get_ticket(work_order_no=wo).attachments) def test_archive_file_without_at_quotes(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="air1", text=wo, chat="wr_air_1", mid="ar0"), flow=self.flow, ledger=self.ledger, ) msg = self._file(sender="air1", filename="航线报价.xlsx", chat="wr_air_1", mid="ar1") msg = InboundMessage( sender_id=msg.sender_id, message_id=msg.message_id, content=msg.content, msg_type="file", chat_id=msg.chat_id, chat_type="group", media=dict(msg.media), raw={"source": "wshoto_archive"}, ) import agent.handlers.air_group_collab as air_mod orig = air_mod.get_air_text_flow def _fake(): return self.flow air_mod.get_air_text_flow = _fake # type: ignore try: name = dispatch_inbound(msg, RouteDecision(intent="ordinary_text_other")) finally: air_mod.get_air_text_flow = orig # type: ignore self.assertEqual(name, "air_group_collab") ticket = self.ledger.get_ticket(work_order_no=wo) self.assertEqual(ticket.quote.get("source_label"), "航线附件报价") self.assertTrue(any(f"工单{wo}的1个附件已保存到后台工单" in t for t in self._texts())) self.assertTrue(any("成交" in t or "未成交" in t or "协商" in t for t in self._texts())) def test_archive_text_ignored_until_at(self) -> None: wo = self._create_air() handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="ar2"), flow=self.flow, ledger=self.ledger, ) msg = self._msg(sender="sales1", text="成交", chat="wr_air_1", mid="ar3") msg = InboundMessage( sender_id=msg.sender_id, message_id=msg.message_id, content=msg.content, chat_id=msg.chat_id, chat_type="group", raw={"source": "wshoto_archive"}, ) import agent.handlers.air_group_collab as air_mod orig = air_mod.get_air_text_flow def _fake(): return self.flow air_mod.get_air_text_flow = _fake # type: ignore try: name = dispatch_inbound(msg, RouteDecision(intent="ordinary_text_other")) finally: air_mod.get_air_text_flow = orig # type: ignore self.assertEqual(name, "air_archive_text_ignore") self.assertEqual(self.ledger.get_ticket(work_order_no=wo).status, "已报价") def test_archive_at_work_order_activates(self) -> None: wo = self._create_air() msg = InboundMessage( sender_id="sales1", message_id="ar-at-1", content=f"@询价小助手 {wo}", chat_id="wr_air_new", chat_type="group", raw={"source": "wshoto_archive"}, ) import agent.handlers.air_group_collab as air_mod import agent.policy.air_text_flow as flow_mod orig = air_mod.get_air_text_flow orig2 = flow_mod.get_air_text_flow def _fake(): return self.flow air_mod.get_air_text_flow = _fake # type: ignore flow_mod.get_air_text_flow = _fake # type: ignore try: name = dispatch_inbound(msg, RouteDecision(intent="ordinary_text_other")) finally: air_mod.get_air_text_flow = orig # type: ignore flow_mod.get_air_text_flow = orig2 # type: ignore self.assertEqual(name, "air_group_collab") self.assertTrue(any(wo in t for t in self._texts())) def test_file_with_work_order_activates(self) -> None: wo = self._create_air() phase = handle_air_group( self._file( sender="air1", filename="航线报价.xlsx", chat="wr_air_1", mid="af7", text=wo, ), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "airline_file_quote") self.assertEqual(self.ledger.find_by_collab_chat(chat_id="wr_air_1").work_order_no, wo) def test_archive_same_sentence_different_msgid_sends_one_sheet(self) -> None: wo = self._create_air() phase = handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_dup2", mid="bot-mid-012"), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "activated") self.assertEqual(sum(1 for x in self.groups.sent if x.get("kind") == "file"), 1) phase = handle_air_group( InboundMessage( sender_id="sales1", message_id="wshoto:archive-mid-999", content=f"@询价小助手 {wo}", chat_id="wr_air_dup2", chat_type="group", raw={"source": "wshoto_archive", "msgid": "archive-mid-999"}, ), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "duplicate_inbound") self.assertEqual(sum(1 for x in self.groups.sent if x.get("kind") == "file"), 1) def test_aibot_and_archive_same_msgid_sends_one_sheet(self) -> None: wo = self._create_air() import agent.handlers.air_group_collab as air_mod import agent.policy.air_text_flow as flow_mod orig = air_mod.get_air_text_flow orig2 = flow_mod.get_air_text_flow def _fake(): return self.flow air_mod.get_air_text_flow = _fake # type: ignore flow_mod.get_air_text_flow = _fake # type: ignore try: aibot = self._msg(sender="sales1", text=wo, chat="wr_air_dup", mid="same-mid-011") name = dispatch_inbound(aibot, RouteDecision(intent="ordinary_text_other")) self.assertEqual(name, "air_group_collab") files_after_bot = sum(1 for x in self.groups.sent if x.get("kind") == "file") self.assertEqual(files_after_bot, 1) archive = InboundMessage( sender_id="sales1", message_id="wshoto:same-mid-011", content=f"@询价小助手 {wo}", chat_id="wr_air_dup", chat_type="group", raw={"source": "wshoto_archive", "msgid": "same-mid-011"}, ) name = dispatch_inbound(archive, RouteDecision(intent="ordinary_text_other")) self.assertEqual(name, "air_group_collab") self.assertEqual(sum(1 for x in self.groups.sent if x.get("kind") == "file"), 1) finally: air_mod.get_air_text_flow = orig # type: ignore flow_mod.get_air_text_flow = orig2 # type: ignore def test_sea_ticket_rejected(self) -> None: created = self.ledger.create_ticket( sender_id="sales1", business_line="SEA", facts={"起运港": "上海", "目的港": "洛杉矶", "品名": "配件", "货量": "1x40HQ", "整柜或拼柜": "整柜", "箱型箱量": "1x40HQ", "报价日期": "2026-09-16"}, ) wo = created["work_order_no"] phase = handle_air_group( self._msg(sender="sales1", text=wo, chat="wr_air_1", mid="sea1"), flow=self.flow, ledger=self.ledger, ) self.assertEqual(phase, "not_air") if __name__ == "__main__": unittest.main()