Files
ai-kefu-admin/tests/test_touchstone.py
T
浅念 7502176190 feat: 配置API(分组校验管线/整份版本/回滚/重置出厂/试金石/平台设置)
- config_validation.validate_group: 九组校验管线(类型/键 allowlist/数值
  范围/prompts 大小上限 50KB·100KB/wall 全量交 compile_wall/push.prefix
  跨租户查重 spec E1),错误清单中文带字段路径
- config_routes: GET/PUT config(整组替换原样入版本,MAX+1,热 reload)/
  reset(按 Tenant.profile 取出厂档案组)/versions 列表·详情/rollback
  (旧版复制为新版本,历史不可变)/wall/test 试金石(逐句判定,不落库
  不 bump)/settings 平台设置 CRUD(仅超管,resolver 接线归 Phase 3)
- db.write_config_version: 版本写入公共路径(创建向导与配置 API 单一
  事实源);Tenant 落 profile 列(重置出厂按它取对应档案)
- _profile_snapshot/_blankify 迁 tenant_source(两路由模块共用);
  db_source_app/super_client 夹具收拢 conftest(两测试文件共用)
- 测试: 519 → 537 全绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 10:07:40 +08:00

137 lines
5.9 KiB
Python

"""试金石(wall/test,Phase 2 Task 6):逐句墙判定预览。
复用 test_wall_execution_tenant 的同款姿势——boundary_filter 纯函数 +
jc/auto 两墙的词表差异做判别。判定优先级与 conversation.py 执行侧一致:
价格墙 → 零号码墙 → 再索要墙(仅已留资)。
红线:不落库、不 bump 版本(reset/编辑器共用的预览入口)。
"""
from app.engine.services import boundary_filter
from app.platform.tenant_source import FileTenantSource
from tests.conftest import SEED_DIR
from tests.test_tenant_admin_api import _auth, _create
WALL_TEST = "/api/platform/tenants/{slug}/wall/test"
# 判别句选取依据(两墙 diff 实证,勿随手改文案——判别力依赖词表差异):
# - PRICE 句撞 jc 独有的 [€£]\s?\d 组(auto 词表无此组);
# - EMAIL 句撞零号码墙 any_email(替换句 = jc no_contact["web"]);
# - CLEAN 句三墙全放行。
PRICE = "The total is €500 for one set."
EMAIL = "Sure, you can also email me at sales@example.com anytime."
CLEAN = "We ship worldwide every day."
REASK = "What's your WhatsApp number?"
AUTO_WALL = FileTenantSource(SEED_DIR).read_tenant("auto").snapshot["wall"]
JC_WALL = FileTenantSource(SEED_DIR).read_tenant("jiancai").snapshot["wall"]
def _post(client, slug, payload):
return client.post(WALL_TEST.format(slug=slug),
headers=_auth(client), json=payload)
def _config_version(client, slug):
r = client.get(f"/api/platform/tenants/{slug}/config",
headers=_auth(client))
assert r.status_code == 200
return r.json()["version"]
def test_touchstone_verdicts(super_client):
_create(super_client, slug="demo")
version_before = _config_version(super_client, "demo")
r = _post(super_client, "demo", {"sentences": [PRICE, EMAIL, CLEAN]})
assert r.status_code == 200, r.text
body = r.json()
assert body["wall_source"] == "current"
results = body["results"]
assert len(results) == 3
# 价格墙: blocked + 替换话术来自 jc deflections 池(en 首句)
assert results[0]["action"] == "blocked"
assert results[0]["reason"] == "boundary"
assert results[0]["replacement"] == boundary_filter.DEFLECTIONS_EN[0]
# 零号码墙: replaced + 替换句精确等于 jc no_contact["web"]
assert results[1]["action"] == "replaced"
assert results[1]["reason"] == "no_contact"
assert results[1]["replacement"] == boundary_filter.NO_CONTACT_WEB
# 干净句: pass,无替换
assert results[2]["action"] == "pass"
assert results[2]["reason"] is None
assert results[2]["replacement"] is None
# 不落库不 bump 版本
assert _config_version(super_client, "demo") == version_before
def test_touchstone_has_contact_and_source_variants(super_client):
"""已留资/app 渠道两开关照搬执行侧语义:替换句换池/换变体,再索要墙启用。"""
_create(super_client, slug="demo")
# has_contact=True: 价格墙替换句来自 deflections_contact 池
r = _post(super_client, "demo", {"sentences": [PRICE, EMAIL, REASK],
"has_contact": True})
results = r.json()["results"]
assert results[0]["replacement"] == boundary_filter.DEFLECTIONS_EN_CONTACT[0]
# 零号码墙替换句换 already 变体(已留资不再索要)
assert results[1]["replacement"] == boundary_filter.NO_CONTACT_ALREADY
# 再索要句被丢(has_contact 才启用): blocked 无替换
assert results[2]["action"] == "blocked"
assert results[2]["reason"] == "reask"
assert results[2]["replacement"] is None
# source=app: 零号码墙替换句用 app 变体
r = _post(super_client, "demo", {"sentences": [EMAIL], "source": "app"})
assert r.json()["results"][0]["replacement"] == boundary_filter.NO_CONTACT_APP
# has_contact=False(默认): 再索要句放行(墙未启用)
r = _post(super_client, "demo", {"sentences": [REASK]})
assert r.json()["results"][0]["action"] == "pass"
def test_touchstone_wall_override(super_client):
"""body 带 wall 覆盖 = reset/编辑器的预览:同句换墙判定翻转,不动库。"""
_create(super_client, slug="demo")
version_before = _config_version(super_client, "demo")
# 覆盖为 auto 墙(无 € 组): 价格句放行
r = _post(super_client, "demo",
{"sentences": [PRICE], "wall": AUTO_WALL})
assert r.status_code == 200
body = r.json()
assert body["wall_source"] == "override"
assert body["results"][0]["action"] == "pass"
# 覆盖墙坏 regex → 400 带字段路径与 regex 字样
bad = dict(JC_WALL)
bad["price_patterns"] = ["([unclosed"]
r = _post(super_client, "demo", {"sentences": [PRICE], "wall": bad})
assert r.status_code == 400
assert "price_patterns[0]" in r.json()["detail"]
assert "regex" in r.json()["detail"]
# 覆盖实验全程不 bump 版本
assert _config_version(super_client, "demo") == version_before
def test_touchstone_input_validation(super_client):
_create(super_client, slug="demo")
# sentences 必须是非空字符串列表
assert _post(super_client, "demo", {"sentences": []}).status_code == 400
assert _post(super_client, "demo",
{"sentences": "not a list"}).status_code == 400
assert _post(super_client, "demo",
{"sentences": ["ok", 42]}).status_code == 400
assert _post(super_client, "demo", {}).status_code == 400
# source 只认 web/app;has_contact 必须 bool;未知键拒绝
assert _post(super_client, "demo",
{"sentences": ["ok."], "source": "sms"}).status_code == 400
assert _post(super_client, "demo",
{"sentences": ["ok."],
"has_contact": "yes"}).status_code == 400
assert _post(super_client, "demo",
{"sentences": ["ok."], "surprise": 1}).status_code == 400
# 未知租户 404
assert _post(super_client, "ghost",
{"sentences": ["ok."]}).status_code == 404