Files
inquiry_robot/inquiry-agent/tests/test_sensitive_cargo.py
T
jillion886andCursor a0c705683f 私聊改口成普通货后不再被上一句违禁词拦住。
销售说运送衣服、改成衣服时,按新品名继续询价。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 20:30:53 +08:00

208 lines
7.8 KiB
Python

"""
私聊建单前违禁品:词表、改口、不建单。
不连企微、TMS、主账。群聊不走这条拦截。
"""
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)
os.environ.setdefault("YTD_ENV", "test")
os.environ.setdefault("REDIS_BACKEND", "memory")
os.environ.setdefault("CHECKPOINT_BACKEND", "memory")
os.environ.setdefault("MESSAGE_STORE_BACKEND", "memory")
os.environ.setdefault("LLM_DATA_USAGE_CONFIRMED", "false")
os.environ.setdefault("LLM_ALLOW_NETWORK", "false")
os.environ.setdefault("LEDGER_BACKEND", "memory")
from agent.ledger.memory_ledger import MemoryLedger
from agent.policy.sensitive_cargo import BLOCK_REPLY, contains_blocked, judge
from agent.policy.sea_text_flow import SeaTextInquiryFlow, reset_sea_text_flow_for_test
COMPLETE_SEA = {
"起运港": "上海",
"目的港": "洛杉矶",
"品名": "普货",
"货量": "20吨",
"整柜或拼柜": "整柜",
"箱型箱量": "1x40HQ",
"贸易条款": "FOB",
"运输分类": "Port to Port",
}
class WordListTests(unittest.TestCase):
def test_real_weapons_and_drugs_block(self) -> None:
for word in ("炸弹", "枪支", "手枪", "子弹", "海洛因", "火炮"):
self.assertTrue(contains_blocked(f"上海到洛杉矶,品名{word}"), word)
def test_toys_and_fireworks_pass(self) -> None:
for word in ("玩具枪", "水枪", "仿真枪", "气枪", "烟花", "礼炮", "鞭炮"):
self.assertFalse(contains_blocked(f"品名{word}"), word)
def test_toy_prefix_does_not_hide_real_gun(self) -> None:
self.assertTrue(contains_blocked("玩具枪支"))
def test_ordinary_dangerous_goods_pass(self) -> None:
self.assertFalse(contains_blocked("这票是危险品,含电,锂电池"))
class JudgeTests(unittest.TestCase):
def test_stray_sentence_sticks_until_cargo_rename(self) -> None:
facts = {"品名": "服装", "起运港": "上海"}
blocked, hold, _edits = judge(text="客户提到有炸弹", facts=dict(facts), hold="")
self.assertTrue(blocked)
self.assertIn("炸弹", hold)
still, hold2, _edits = judge(text="重量100KGS", facts=dict(facts), hold=hold)
self.assertTrue(still)
self.assertEqual(hold2, hold)
released, hold3, edits = judge(text="品名改为服装", facts=dict(facts), hold=hold)
self.assertFalse(released)
self.assertEqual(hold3, "")
self.assertEqual(edits.get("品名"), "服装")
def test_rename_sentence_that_still_says_the_word_stays_blocked(self) -> None:
blocked, hold, _edits = judge(text="不是炸弹,品名服装", facts={"品名": "服装"}, hold="")
self.assertTrue(blocked)
self.assertIn("炸弹", hold)
def test_origin_and_remark_remain_after_cargo_rename(self) -> None:
facts = {"品名": "炸弹", "起运港": "上海", "备注": "内有枪支"}
blocked, hold, _edits = judge(text="品名改为服装", facts=facts, hold="品名炸弹")
self.assertTrue(blocked)
self.assertEqual(hold, "")
self.assertEqual(facts["品名"], "服装")
self.assertIn("枪支", facts["备注"])
def test_remark_can_be_rewritten(self) -> None:
facts = {"品名": "服装", "备注": "枪支"}
blocked, hold, _edits = judge(text="备注改为无", facts=facts, hold="")
self.assertFalse(blocked)
self.assertEqual(facts["备注"], "无")
self.assertEqual(hold, "")
class SeaPrivateFlowTests(unittest.TestCase):
def setUp(self) -> None:
reset_sea_text_flow_for_test()
self.ledger = MemoryLedger()
self.flow = SeaTextInquiryFlow(self.ledger)
self.replies: list[str] = []
def _reply(self, text: str, extra=None) -> None:
_ = extra
self.replies.append(text)
def test_private_text_blocks_before_ticket(self) -> None:
facts = dict(COMPLETE_SEA)
facts["品名"] = "炸弹"
phase = self.flow.on_text(
sender_id="s-bomb",
text="海运 上海到洛杉矶 品名炸弹",
reply=self._reply,
injected_facts=facts,
injected_mode="SEA",
)
self.assertEqual(phase, "sensitive_block")
self.assertEqual(self.replies, [BLOCK_REPLY])
self.assertEqual(self.ledger._tickets, {})
sess = self.flow.session_of("s-bomb")
self.assertFalse((sess.work_order_no or "").strip())
def test_file_facts_block_even_if_caption_is_plain(self) -> None:
facts = dict(COMPLETE_SEA)
facts["品名"] = "枪支"
phase = self.flow.on_text(
sender_id="s-file",
text="海运报价单",
reply=self._reply,
injected_facts=facts,
injected_mode="SEA",
)
self.assertEqual(phase, "sensitive_block")
self.assertEqual(self.ledger._tickets, {})
def test_rename_then_confirm_creates_ticket(self) -> None:
facts = dict(COMPLETE_SEA)
facts["品名"] = "炸弹"
self.flow.on_text(
sender_id="s-fix",
text="海运 品名炸弹 上海到洛杉矶",
reply=self._reply,
injected_facts=facts,
injected_mode="SEA",
)
self.replies.clear()
fixed = dict(COMPLETE_SEA)
phase = self.flow.on_text(
sender_id="s-fix",
text="品名改为服装",
reply=self._reply,
injected_facts=fixed,
injected_mode="SEA",
)
self.assertNotEqual(phase, "sensitive_block")
self.assertNotIn(BLOCK_REPLY, self.replies)
self.replies.clear()
created = self.flow.on_text(sender_id="s-fix", text="确定", reply=self._reply)
self.assertNotEqual(created, "sensitive_block")
self.assertTrue(self.ledger._tickets)
joined = "\n".join(self.replies)
self.assertNotIn(BLOCK_REPLY, joined)
def test_oral_restatement_clears_previous_bomb(self) -> None:
"""销售不说「品名改为」,直接重说运送衣服,或说改成衣服。"""
hold = "10月30日,从广州去深圳,运送炸弹,请报价"
facts = {"品名": "炸弹", "起运港": "广州"}
for text in (
"10月30日,从广州去深圳,运送衣服,请报价",
"炸弹错了,给我改成衣服",
"炸弹换衣服",
):
blocked, new_hold, edits = judge(text=text, facts=dict(facts), hold=hold)
self.assertFalse(blocked, text)
self.assertEqual(new_hold, "", text)
self.assertEqual(edits.get("品名"), "衣服", text)
def test_weight_alone_does_not_clear(self) -> None:
facts = dict(COMPLETE_SEA)
facts["品名"] = "弹药"
self.flow.on_text(
sender_id="s-hold",
text="海运 品名弹药",
reply=self._reply,
injected_facts=facts,
injected_mode="SEA",
)
self.replies.clear()
phase = self.flow.on_text(
sender_id="s-hold",
text="重量 20吨",
reply=self._reply,
injected_facts=facts,
injected_mode="SEA",
)
self.assertEqual(phase, "sensitive_block")
self.assertEqual(self.replies, [BLOCK_REPLY])
self.assertEqual(self.ledger._tickets, {})
def test_toy_gun_can_continue(self) -> None:
facts = dict(COMPLETE_SEA)
facts["品名"] = "玩具枪"
phase = self.flow.on_text(
sender_id="s-toy",
text="海运 品名玩具枪 上海到洛杉矶",
reply=self._reply,
injected_facts=facts,
injected_mode="SEA",
)
self.assertEqual(phase, "wait_confirm")
self.assertNotIn(BLOCK_REPLY, self.replies)