885 lines
35 KiB
Python
885 lines
35 KiB
Python
"""
|
|
空运群内协同:激活当前工单、补字段、航线报价、锁舱/释放、成交。
|
|
"""
|
|
|
|
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()
|