Files
group_fqcd_jr/tests/unit/infrastructure/test_milvus_profile_projection.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

202 lines
6.1 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.
"""`MilvusProfileProjection` 的定向测试。
前 4 个用例移植自同事 `ZSY_develop` 的
`tests/unit/infrastructure/test_milvus_profile_projection.py`;
后 4 个覆盖本仓对其做的**两处契约放宽**(`customer_id` 兼容字符串、
不可投影键跳过而非整批失败)与脱敏,这些是移植时必须钉住的差异点。
"""
from uuid import uuid4
import pytest
from app.core.errors import RecoverableAgentError
from app.infrastructure.milvus_profile_projection import MilvusProfileProjection
class FakeMilvus:
def __init__(self, existing: list[dict[str, object]] | None = None) -> None:
self.existing = existing or []
self.queries: list[dict[str, object]] = []
self.upserts: list[dict[str, object]] = []
async def query(self, **kwargs: object) -> list[dict[str, object]]:
self.queries.append(kwargs)
return self.existing
async def upsert(self, **kwargs: object) -> None:
self.upserts.append(kwargs)
def payload() -> dict[str, object]:
return {
"customer_id": 7,
"profile_version": 1,
"memory_sources": [{
"memory_uuid": str(uuid4()),
"memory_key": "preference:risk_level",
"content": "稳健型",
"memory_type": "preference",
"confidence": 0.9,
"version": 2,
"valid_until": None,
}],
}
def _source(data: dict[str, object]) -> dict[str, object]:
sources = data["memory_sources"]
assert isinstance(sources, list)
source = sources[0]
assert isinstance(source, dict)
return source
@pytest.mark.asyncio
async def test_upsert_writes_schema_fields_and_vector() -> None:
client = FakeMilvus()
projection = MilvusProfileProjection(client, _embed)
await projection.upsert(payload())
assert len(client.upserts) == 1
row = client.upserts[0]["data"][0]
assert row["customer_id"] == 7
assert row["status"] == "active"
assert len(row["embedding"]) == 1024
@pytest.mark.asyncio
async def test_lower_memory_version_is_not_overwritten() -> None:
data = payload()
memory_uuid = _source(data)["memory_uuid"]
client = FakeMilvus(existing=[{
"memory_uuid": memory_uuid, "customer_id": 7, "version": 3,
}])
await MilvusProfileProjection(client, _embed).upsert(data)
assert client.upserts == []
@pytest.mark.asyncio
async def test_embedding_dimension_is_enforced() -> None:
with pytest.raises(RecoverableAgentError, match="维度"):
await MilvusProfileProjection(client=FakeMilvus(), embed=_embed_short).upsert(
payload()
)
@pytest.mark.asyncio
async def test_non_uuid_memory_id_is_rejected() -> None:
data = payload()
_source(data)["memory_uuid"] = "unsafe\" or true"
with pytest.raises(ValueError, match="memory_uuid"):
await MilvusProfileProjection(FakeMilvus(), _embed).upsert(data)
# --- 本仓放宽的契约(移植差异点) -------------------------------------------
@pytest.mark.asyncio
async def test_string_customer_id_is_accepted() -> None:
"""本仓三处生产者写的都是 `str(customer_id)`;不接受字符串则事件必然全部失败。"""
data = payload()
data["customer_id"] = "9102"
client = FakeMilvus()
await MilvusProfileProjection(client, _embed).upsert(data)
assert client.upserts[0]["data"][0]["customer_id"] == 9102
@pytest.mark.asyncio
async def test_non_numeric_customer_id_is_rejected() -> None:
"""放宽不等于不校验:uuid 之类的非数字串必须拒绝,不能当成客户号写进向量库。"""
data = payload()
data["customer_id"] = "957c0552-7fa2-4f2a-924d-d2d2e133b245"
with pytest.raises(ValueError, match="customer_id"):
await MilvusProfileProjection(FakeMilvus(), _embed).upsert(data)
@pytest.mark.asyncio
async def test_version_key_fallback_is_supported() -> None:
"""本仓生产端 payload 用 `version`;适配器契约用 `profile_version`。两个都要认。"""
data = payload()
del data["profile_version"]
data["version"] = 5
client = FakeMilvus()
await MilvusProfileProjection(client, _embed).upsert(data)
assert len(client.upserts) == 1
@pytest.mark.asyncio
async def test_non_projectable_memory_key_is_skipped_not_fatal() -> None:
"""`constraint:*` / `profile:*` 不在可投影前缀内。
关键:一条不可投影的键**不得**毒死同一客户其余可投影记忆。
"""
data = payload()
good = _source(data)
data["memory_sources"] = [
{
"memory_uuid": str(uuid4()),
"memory_key": "constraint:liquidity",
"content": "半年内需要流动性",
"memory_type": "constraint",
"confidence": 0.8,
"version": 1,
"valid_until": None,
},
good,
]
client = FakeMilvus()
await MilvusProfileProjection(client, _embed).upsert(data)
assert len(client.upserts) == 1
rows = client.upserts[0]["data"]
assert [row["memory_key"] for row in rows] == ["preference:risk_level"]
@pytest.mark.asyncio
async def test_all_keys_non_projectable_writes_nothing() -> None:
"""全部不可投影时不写 Milvus,但也不报错(不是失败,是无需投影)。"""
data = payload()
_source(data)["memory_key"] = "profile:occupation"
client = FakeMilvus()
await MilvusProfileProjection(client, _embed).upsert(data)
assert client.upserts == []
@pytest.mark.asyncio
async def test_sensitive_credentials_are_sanitized_before_write() -> None:
"""落外部存储前必须脱敏:手机号不得原样写进向量库。"""
data = payload()
_source(data)["content"] = "我的手机号是 15936583816,稳健型"
client = FakeMilvus()
await MilvusProfileProjection(client, _embed).upsert(data)
content = client.upserts[0]["data"][0]["content"]
assert "15936583816" not in content
assert "[手机号已隐藏]" in content
def _vector(size: int = 1024) -> list[float]:
return [0.0] * size
async def _embed(_: str) -> list[float]:
return _vector()
async def _embed_short(_: str) -> list[float]:
return _vector(3)