Files
group_xinghuo_jinrong/tests/test_module_boundary.py
T
zhanghongyu_0626 4b8e11c9bd feat(threshold): Implement customer loss threshold configuration and notification system
- Added `ThresholdRepository` for managing customer loss threshold configurations and notifications.
- Introduced `threshold_service` to handle loss threshold alerts based on customer portfolio performance.
- Enhanced `customer_prompts` to include new intent for querying product net values.
- Updated `customer_service` to integrate new threshold alert functionality into existing workflows.
- Implemented `sanitize_postprocess` for improved compliance handling in customer interactions.
- Enhanced course documentation to reflect updates in advisor training modules and interactive elements.

This update significantly improves the customer experience by providing proactive loss threshold notifications and enhancing the overall service framework.
2026-09-10 10:57:40 +08:00

215 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""模块边界防呆测试(配合《风控Agent模块边界与合并接缝标注.md》§3)。
风控 Agent 按"独立封装模块"自治:模块私有实现自己维护,与宿主(main)的耦合
只走 4 个接缝。本文件把这层约定写成断言——**AL-09 合并 main 后必须全绿**,
任何一条红了都说明模块被宿主侵蚀或接缝被破坏,应先修边界再继续。
四类断言:
1. 模块私有文件未被误删;
2. 模块代码不得 import 宿主私有实现(防双套串味);
3. 模块 AuthContext 契约未被宿主类替换;
4. 模块私有 settings 字段未被合并丢掉。
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
from app.api.deps import AuthContext
from app.config.settings import settings
from app.utils.authz import AGENT_TYPES
APP_DIR = Path(__file__).resolve().parents[1] / "app"
# 模块私有关键文件(A 类):缺失即说明被误删或合并时被宿主覆盖
MODULE_PRIVATE_FILES = [
"app/api/deps.py",
"app/api/risk.py",
"app/api/simulate.py",
"app/service/auth_service.py",
"app/service/input_guard.py",
"app/service/suitability.py",
"app/service/tool_service.py",
"app/repository/core_ro.py",
"app/repository/risk_repository.py",
"app/repository/session_repository.py",
"app/gateway/trade_gateway.py",
"app/model/suitability.py",
"app/utils/db.py",
"app/utils/trace.py",
"app/api/audit_middleware.py",
"app/service/risk/engine.py",
"app/service/risk/alert_service.py",
]
# 禁止模块代码导入的宿主私有实现(D 类 / B 类宿主侧)
FORBIDDEN_IMPORT_PREFIXES = (
"from app.gateway.auth_deps",
"from app.gateway.jwt_service",
"from app.gateway.rbac",
"from app.gateway.ownership",
"from app.config.database",
"from app.middleware",
"from app.utils.input_guard", # 模块用 app.service.input_guard(含限流与留痕)
"from app.model.schemas", # 宿主 AuthContext,须经 app.api.auth_adapter 转换
)
# 模块私有 settings 字段(合并时一个都不能丢)
MODULE_SETTINGS_FIELDS = (
"mysql_core_database",
"redis_url",
"milvus_uri",
"ollama_base_url",
"embed_model",
"embed_dim",
"embed_timeout_seconds",
"deepseek_api_key",
"deepseek_base_url",
"risk_large_amount",
"risk_daily_total",
"risk_freq_count",
"risk_probe_window_minutes",
"risk_probe_count",
"risk_probe_amount",
"risk_small_amount",
"risk_small_count",
"risk_aml_default_threshold",
"guard_rate_limit_max",
"guard_rate_limit_window_seconds",
)
# 宿主 Wave 0 独占文件(D 类):允许 import gateway/middleware 等,不参与「模块禁 import」扫描
HOST_ONLY_SKIP_PREFIXES = (
"app/main.py",
"app/api/auth.py",
"app/gateway/",
"app/middleware/",
"app/repository/advisor_rel_repository.py",
"app/repository/agent_repository.py",
"app/repository/audit_repository.py",
"app/utils/input_guard.py",
"app/utils/response.py",
"app/utils/exceptions.py",
)
# 数据分析 Agent S3 接缝 — 走 deps + analyst_auth_adapter
ANALYST_AGENT_SEAM_SKIP = (
"app/api/analyst.py",
"app/api/analyst_auth_adapter.py",
"app/service/analyst_agent.py",
"app/service/analytics_repo.py",
"app/service/cache_service.py",
"app/service/dict_service.py",
"app/service/guardrail.py",
"app/service/llm.py",
"app/service/schema_meta.py",
"app/service/sql_guard.py",
)
# B7 Redis 网关惰性连接需 database._redis_kwargs(RESP2 口径)
INFRA_SKIP = (
"app/service/risk/redis_gateway.py",
)
# 客服 Agent S2 接缝(docs/项目框架设计/客服Agent-合并说明.md)— 允许 import 宿主 schemas
CUSTOMER_AGENT_SEAM_SKIP = (
"app/api/auth_adapter.py",
"app/api/visitor.py",
"app/repository/note_repository.py",
"app/repository/profile_repository.py",
"app/repository/threshold_repository.py",
"app/service/customer_service.py",
"app/service/customer_prompts.py",
"app/service/note_service.py",
"app/service/profile_service.py",
"app/service/threshold_service.py",
"app/service/visitor_prompts.py",
"app/service/visitor_service.py",
"app/tool/core_ro_tool.py",
"app/utils/compliance_guard.py",
"app/utils/data_masker.py",
"app/utils/sanitize_postprocess.py",
"app/config/profile_slots.py",
)
def _is_host_only_file(path: Path) -> bool:
rel = path.relative_to(APP_DIR.parent).as_posix()
if rel in CUSTOMER_AGENT_SEAM_SKIP or rel in ANALYST_AGENT_SEAM_SKIP or rel in INFRA_SKIP:
return True
return rel.startswith(HOST_ONLY_SKIP_PREFIXES)
def _iter_module_py_files():
for path in APP_DIR.rglob("*.py"):
if "__pycache__" in path.parts:
continue
if _is_host_only_file(path):
continue
yield path
_IMPORT_LINE = re.compile(r"^\s*(from|import)\s+")
@pytest.mark.parametrize("rel_path", MODULE_PRIVATE_FILES)
def test_module_private_file_exists(rel_path):
"""模块私有文件必须存在(防合并时被删除/覆盖)。"""
assert (APP_DIR.parent / rel_path).is_file(), f"模块私有文件缺失:{rel_path}"
def test_no_host_private_imports():
"""模块代码不得直接 import 宿主私有实现——跨层一律走接缝。"""
offenders: list[str] = []
for path in _iter_module_py_files():
for lineno, line in enumerate(
path.read_text(encoding="utf-8", errors="ignore").splitlines(), start=1
):
stripped = line.strip()
if not _IMPORT_LINE.match(stripped):
continue # 只看真正的 import 行,避免注释/文档字符串误报
if stripped.startswith(FORBIDDEN_IMPORT_PREFIXES):
rel = path.relative_to(APP_DIR.parent).as_posix()
offenders.append(f"{rel}:{lineno} -> {stripped}")
assert not offenders, "发现跨层导入宿主私有实现(应改走接缝):\n" + "\n".join(offenders)
def test_auth_context_contract_intact():
"""模块 AuthContext 契约必须完整——防被宿主的 schemas.AuthContext 替换。
宿主用 sub/trace_id/agent_type、has_role 单参、has_perm;
模块用 actor_id、has_role 多参、has_permission。字段名或方法名一变,
模块内全量引用会静默失效,故在此锁死。
"""
for field in (
"actor_id",
"roles",
"customer_id",
"token_type",
"permissions",
"tenant_id",
"jti",
):
assert field in AuthContext.model_fields, f"AuthContext 缺失字段:{field}"
ctx = AuthContext(actor_id="STAFF-90001", roles=["risk_officer", "risk_demo"])
assert ctx.has_role("risk_officer", "compliance") is True # 多参语义
assert ctx.has_role("advisor") is False
assert ctx.has_permission("risk:alert:write") is False
assert ctx.is_customer() is False
def test_module_settings_fields_present():
"""模块私有配置字段必须齐全(防合并 settings.py 时被丢)。"""
missing = [f for f in MODULE_SETTINGS_FIELDS if not hasattr(settings, f)]
assert not missing, f"settings 丢失模块私有字段:{missing}"
def test_agent_types_contract():
"""Agent 类型四值须与宿主一致(这是少数双方天然对齐的契约,不得漂移)。"""
assert AGENT_TYPES == ("customer", "advisor", "analyst", "risk")