"""DeterministicEmbedder 与工厂:纯数学,不连库不联网(T-P2-03 接口位).""" from __future__ import annotations import math import pytest from packages.retrieval.embedding import ( DETERMINISTIC_MODEL_NAME, DeterministicEmbedder, build_embedding_provider, ) from packages.shared.config import get_settings from packages.shared.errors import AppError, ErrorCode def _dot(a: list[float], b: list[float]) -> float: return sum(x * y for x, y in zip(a, b, strict=True)) async def test_same_text_embeds_to_same_vector() -> None: embedder = DeterministicEmbedder(dimensions=1024) first, second = await embedder.embed(["白衬衫怎么搭配", "白衬衫怎么搭配"]) assert first == second async def test_dimensions_match_declaration() -> None: vec = (await DeterministicEmbedder(dimensions=1024).embed(["hello"]))[0] assert len(vec) == 1024 small = (await DeterministicEmbedder(dimensions=8).embed(["hello"]))[0] assert len(small) == 8 async def test_vectors_are_l2_normalized() -> None: for text in ("hello", "中文知识片段", ""): vec = (await DeterministicEmbedder(dimensions=1024).embed([text]))[0] assert math.isclose(_dot(vec, vec), 1.0, rel_tol=1e-6, abs_tol=1e-9) async def test_different_texts_are_near_orthogonal() -> None: """不同文本近似正交(|dot| ≪ 1):向量召回管线在无真模型时仍可测.""" left, right = await DeterministicEmbedder(dimensions=1024).embed( ["interview outfit advice", "winter skincare routine"] ) assert left != right assert abs(_dot(left, right)) < 0.1 async def test_batch_preserves_order_and_length() -> None: texts = ["alpha", "beta", "gamma"] vectors = await DeterministicEmbedder(dimensions=16).embed(texts) assert len(vectors) == len(texts) singles = [(await DeterministicEmbedder(dimensions=16).embed([t]))[0] for t in texts] assert vectors == singles def test_metadata_is_stable() -> None: embedder = DeterministicEmbedder(dimensions=8) assert embedder.model_name == DETERMINISTIC_MODEL_NAME assert embedder.version assert embedder.dimensions == 8 def test_invalid_dimensions_rejected() -> None: with pytest.raises(ValueError, match="dimensions"): DeterministicEmbedder(dimensions=0) def test_factory_builds_deterministic_embedder(monkeypatch: pytest.MonkeyPatch) -> None: # 带 alias 的 Settings 字段按字段名构造会被静默忽略 —— 统一走 # setenv + cache_clear(上批经验,见任务文档 §2)。 monkeypatch.setenv("KB_EMBEDDING_MODEL", "deterministic-v1") monkeypatch.setenv("KB_VECTOR_DIMENSIONS", "1024") get_settings.cache_clear() try: settings = get_settings() provider = build_embedding_provider(settings) assert isinstance(provider, DeterministicEmbedder) assert provider.model_name == "deterministic-v1" assert provider.dimensions == 1024 finally: monkeypatch.undo() get_settings.cache_clear() def test_factory_rejects_unknown_model(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("KB_EMBEDDING_MODEL", "bge-m3-onnx") get_settings.cache_clear() try: settings = get_settings() assert settings.kb_embedding_model == "bge-m3-onnx" with pytest.raises(AppError) as excinfo: build_embedding_provider(settings) assert excinfo.value.code == ErrorCode.CONFIG_ERROR finally: monkeypatch.undo() get_settings.cache_clear()