Files
inquiry_robot/inquiry-agent/tests/test_event_exception.py
T

264 lines
9.8 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.policy import inquiry_copy as copy
from agent.policy.event_exception import (
WAIT_ADOPT,
WAIT_AIR_QUOTE,
WAIT_DEAL,
WAIT_PRODUCT_QUOTE,
conversation_of,
format_timeout_hours,
pick_app_targets,
pick_mentions,
timeout_code,
)
class TestTimeoutCode(unittest.TestCase):
def test_sales_unified(self) -> None:
self.assertEqual(
timeout_code(wait_kind=WAIT_ADOPT, business_line="SEA", transport_class="急件"),
"sea_sales",
)
self.assertEqual(
timeout_code(wait_kind=WAIT_DEAL, business_line="AIR", transport_class=""),
"air_sales",
)
self.assertEqual(
timeout_code(wait_kind="lost_reason", business_line="LAND", transport_class="急件"),
"land_sales",
)
def test_air_route(self) -> None:
self.assertEqual(
timeout_code(wait_kind=WAIT_AIR_QUOTE, business_line="AIR", transport_class=""),
"air_route",
)
def test_sea_and_land_product_class(self) -> None:
self.assertEqual(
timeout_code(
wait_kind=WAIT_PRODUCT_QUOTE, business_line="SEA", transport_class="Port to Port"
),
"sea_product_ptp",
)
self.assertEqual(
timeout_code(wait_kind=WAIT_PRODUCT_QUOTE, business_line="SEA", transport_class="其他"),
"sea_product_other",
)
self.assertEqual(
timeout_code(wait_kind=WAIT_PRODUCT_QUOTE, business_line="LAND", transport_class="急件"),
"land_hk_express",
)
self.assertEqual(
timeout_code(wait_kind=WAIT_PRODUCT_QUOTE, business_line="SEA", transport_class=""),
"",
)
def test_hours_format(self) -> None:
self.assertEqual(format_timeout_hours(0.25), "0.25")
self.assertEqual(format_timeout_hours(48), "48")
class TestMentions(unittest.TestCase):
def test_product_all_unanswered(self) -> None:
sales = {
"wecomId": "s1",
"name": "销售甲",
"roleCode": "sales",
"notifyEventException": 1,
}
members = [
{"wecomId": "p1", "name": "张三", "roleCode": "sea", "notifyEventException": 1},
{"wecomId": "p2", "name": "李四", "roleCode": "sea", "notifyEventException": 0},
{"wecomId": "s1", "name": "销售甲", "roleCode": "sales", "notifyEventException": 1},
]
hit = pick_mentions(
wait_kind=WAIT_PRODUCT_QUOTE,
sales=sales,
members=members,
business_line="SEA",
)
self.assertEqual([p["name"] for p in hit], ["张三", "李四"])
apps = pick_app_targets(hit)
self.assertEqual([p["wecomId"] for p in apps], ["p1"])
def test_adopt_is_private(self) -> None:
self.assertEqual(conversation_of(wait_kind=WAIT_ADOPT, collab_chat_id="wrxxx"), "private")
def test_deal_uses_group_when_present(self) -> None:
self.assertEqual(conversation_of(wait_kind=WAIT_DEAL, collab_chat_id="wrxxx"), "group")
self.assertEqual(conversation_of(wait_kind=WAIT_DEAL, collab_chat_id=""), "private")
class TestMemoryEventWait(unittest.TestCase):
def test_same_kind_keeps_start(self) -> None:
from agent.ledger.memory_ledger import MemoryLedger
book = MemoryLedger()
created = book.create_ticket(sender_id="s1", business_line="SEA", facts={"运输分类": "其他"})
wo = created["work_order_no"]
book.start_event_wait(work_order_no=wo, wait_kind="product_quote")
first = book.get_ticket(work_order_no=wo).event_wait_started_at
book.start_event_wait(work_order_no=wo, wait_kind="product_quote")
self.assertEqual(book.get_ticket(work_order_no=wo).event_wait_started_at, first)
def test_record_appends_and_alerts(self) -> None:
from agent.ledger.memory_ledger import MemoryLedger
book = MemoryLedger()
created = book.create_ticket(sender_id="s1", business_line="AIR", facts={})
wo = created["work_order_no"]
book.start_event_wait(work_order_no=wo, wait_kind="air_quote")
book.record_event_exception(work_order_no=wo, event_exception="航线响应超时")
book.record_event_exception(work_order_no=wo, event_exception="销售响应超时")
t = book.get_ticket(work_order_no=wo)
self.assertEqual(t.event_exception, "航线响应超时;销售响应超时")
self.assertEqual(t.event_wait_alerted, 1)
self.assertEqual(book.list_event_waits(), [])
class TestEventExceptionCopy(unittest.TestCase):
def test_group_air(self) -> None:
text = copy.event_exception_session_text(
exception_type="航线响应超时",
work_order_no="WO202606180003",
names=["张三"],
hours=0.25,
mention=True,
)
self.assertEqual(
text,
"航线响应超时\n关联询价工单 WO202606180003。@张三已超过 0.25 工作小时 无响应,事件异常已记录工单,请及时处理。",
)
def test_group_two_names(self) -> None:
text = copy.event_exception_session_text(
exception_type="产品响应超时",
work_order_no="WO1",
names=["张三", "李四"],
hours=2,
mention=True,
)
self.assertIn("@张三 @李四已超过 2 工作小时", text)
def test_private_sales_no_at(self) -> None:
text = copy.event_exception_session_text(
exception_type="销售响应超时",
work_order_no="WO1",
names=["王五"],
hours=48,
mention=False,
)
self.assertIn("王五已超过 48 工作小时", text)
self.assertNotIn("@王五", text)
def test_test_prefix(self) -> None:
"""测服会话和应用卡片都不加 [test]。"""
text = copy.event_exception_session_text(
exception_type="销售响应超时",
work_order_no="WO1",
names=["王五"],
hours=1,
mention=False,
test_prefix=True,
)
self.assertFalse(text.startswith("[test]"))
self.assertNotIn("[test]", text)
payload = copy.event_exception_alert_payload(
card_title="销售响应异常",
work_order_no="WO1",
hours=1,
test_prefix=True,
)
title = (payload.get("template_card") or {}).get("main_title", {}).get("title") or ""
self.assertEqual(title, "销售响应异常")
self.assertNotIn("[test]", title)
def test_app_card(self) -> None:
payload = copy.event_exception_alert_payload(
card_title="销售响应异常",
work_order_no="WO202609090199",
hours=48,
)
self.assertEqual(payload["wecom_app"], "notify")
card = payload["template_card"]
self.assertEqual(card["source"]["desc"], "询价机器人事件告警")
self.assertEqual(card["main_title"]["title"], "销售响应异常")
self.assertEqual(card["main_title"]["desc"], "关联询价工单 WO202609090199")
self.assertIn("您已超过 48 工作小时无响应", card["sub_title_text"])
class TestPoll(unittest.TestCase):
def test_fires_once_on_timeout(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", path)
self.assertIsNotNone(spec)
self.assertIsNotNone(spec.loader)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
poll_event_exceptions = mod.poll_event_exceptions
book = MemoryLedger()
book.set_sea_staff(
[
{
"wecomId": "p1",
"name": "张三",
"roleCode": "sea",
"notifyEventException": 1,
"status": "active",
}
]
)
created = book.create_ticket(sender_id="s1", business_line="SEA", facts={"运输分类": "其他"})
wo = created["work_order_no"]
book.start_event_wait(work_order_no=wo, wait_kind="product_quote")
t = book.get_ticket(work_order_no=wo)
t.collab_chat_id = "wr1"
t.collab_member_ids = ["p1"]
t.event_wait_started_at = (datetime.now(SHANGHAI) - timedelta(hours=3)).strftime(
"%Y-%m-%d %H:%M:%S"
)
book._timeouts["sea_product_other"] = 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
now = datetime.now(SHANGHAI)
n = poll_event_exceptions(
ledger=book, now=now, send_group=send_group, notify_ready=False
)
self.assertEqual(n, 1)
self.assertEqual(book.get_ticket(work_order_no=wo).event_exception, "产品响应超时")
self.assertTrue(groups)
self.assertIn("@张三", groups[0]["content"])
n2 = poll_event_exceptions(
ledger=book, now=now, send_group=send_group, notify_ready=False
)
self.assertEqual(n2, 0)
if __name__ == "__main__":
unittest.main()