Files
wehub-resource-sync 6c9c7fe7f3
CI / integration tests (3.13) (push) Failing after 1s
Commit lint / pull request title (push) Has been skipped
Docs / links (push) Failing after 1s
CI / unit tests (3.13) (push) Failing after 1s
CI / lint (push) Failing after 1s
CI / integration tests (push) Failing after 1s
CI / package build (push) Failing after 1s
Commit lint / commit messages (push) Failing after 1s
CI / unit tests (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:24:24 +08:00

443 lines
17 KiB
Python

"""Unit tests for knowledge search service.
White-box surfaces mocked:
``acategory_retrieve`` (the everalgo facade)
``knowledge_document_repo`` (get_documents_by_ids)
``_get_embedding`` (embedding provider)
``_get_reranker`` (rerank provider)
``load_settings`` (knowledge search settings)
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from everalgo.types import Candidate
from everos.component.utils.datetime import get_utc_now
from everos.core.errors import ConfigurationError
from everos.infra.persistence.sqlite.tables.knowledge import (
KnowledgeDocumentRow,
)
from everos.service.knowledge import (
DocumentContext,
SearchKnowledgeResult,
compile_knowledge_where,
search_knowledge,
)
_MOD = "everos.service.knowledge"
_CONFIG_MOD = "everos.config"
# ── Helpers ──────────────────────────────────────────────────────────────────
def _candidate(
node_id: str = "n_001",
score: float = 0.85,
source: str = "keyword",
doc_id: str = "d_testdoc00001",
category_id: str = "Technology",
topic_name: str = "Neural Networks",
topic_path: str = "Technology / Neural Networks",
depth: int = 1,
summary: str = "Overview of neural networks.",
content: str = "",
) -> Candidate:
return Candidate(
id=node_id,
score=score,
source=source,
metadata={
"doc_id": doc_id,
"category_id": category_id,
"topic_name": topic_name,
"topic_path": topic_path,
"depth": depth,
"summary": summary,
"content": content,
},
)
def _doc_row(
doc_id: str = "d_testdoc00001",
title: str = "AI Handbook",
summary: str = "A handbook on AI.",
) -> KnowledgeDocumentRow:
now = get_utc_now()
return KnowledgeDocumentRow(
doc_id=doc_id,
app_id="default",
project_id="default",
category_id="Technology",
title=title,
summary=summary,
source_name="ai.pdf",
source_type="file",
md_path="/tmp/knowledge/Technology/d_testdoc00001",
created_at=now,
updated_at=now,
)
def _mock_settings() -> MagicMock:
"""Build a mock Settings with knowledge.search defaults."""
s = MagicMock()
s.knowledge.search.recall_n = 200
s.knowledge.search.rerank_n = 50
s.knowledge.search.mass_top_m = 50
s.knowledge.search.lam = 0.1
s.knowledge.search.top_k_cap = 100
s.embedding.model = ""
s.embedding.api_key = None
return s
def _patch_stack(
facade_return: list[Candidate] | None = None,
doc_rows: list[KnowledgeDocumentRow] | None = None,
embed_vector: list[float] | None = None,
):
"""Return a dict of patches for common mocks.
``acategory_retrieve`` is mocked at the module level — its internal
behavior (recall -> rollup -> rerank -> boost) is tested in everalgo.
"""
acategory = AsyncMock(return_value=facade_return or [])
doc_repo = AsyncMock()
doc_repo.get_documents_by_ids = AsyncMock(return_value=doc_rows or [])
embedder = AsyncMock()
embedder.embed = AsyncMock(return_value=embed_vector or [0.1] * 1024)
reranker = AsyncMock()
recaller = AsyncMock()
settings = _mock_settings()
return {
"acategory": acategory,
"doc_repo": doc_repo,
"embedder": embedder,
"reranker": reranker,
"recaller": recaller,
"settings": settings,
}
# ── compile_knowledge_where ──────────────────────────────────────────────────
class TestCompileKnowledgeWhere:
def test_basic_clause(self) -> None:
result = compile_knowledge_where("myapp", "myproj")
assert result == "app_id = 'myapp' AND project_id = 'myproj'"
def test_defaults(self) -> None:
result = compile_knowledge_where("default", "default")
assert "app_id = 'default'" in result
assert "project_id = 'default'" in result
def test_rejects_invalid_app_id_with_sql_injection(self) -> None:
with pytest.raises(ValueError, match="app_id"):
compile_knowledge_where("app'; DROP TABLE --", "proj")
def test_rejects_invalid_project_id_with_sql_injection(self) -> None:
with pytest.raises(ValueError, match="project_id"):
compile_knowledge_where("app", "proj'); DELETE FROM--")
def test_rejects_empty_app_id(self) -> None:
with pytest.raises(ValueError, match="app_id"):
compile_knowledge_where("", "proj")
def test_rejects_empty_project_id(self) -> None:
with pytest.raises(ValueError, match="project_id"):
compile_knowledge_where("app", "")
def test_accepts_valid_ids_with_special_chars(self) -> None:
result = compile_knowledge_where("my_app.v2", "project-1")
assert "my_app.v2" in result
assert "project-1" in result
def test_accepts_valid_ids_with_at_plus(self) -> None:
result = compile_knowledge_where("app@org+v1", "proj_1")
assert "app@org+v1" in result
assert "proj_1" in result
# ── search_knowledge ─────────────────────────────────────────────────────────
class TestSearchKnowledgeFacadeWiring:
"""Verify search_knowledge delegates to acategory_retrieve correctly."""
async def test_calls_facade_with_config_params(self) -> None:
c = _candidate(node_id="n_001", score=0.9, content="Neural net content.")
mocks = _patch_stack(facade_return=[c], doc_rows=[_doc_row()])
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch(
"everalgo.rank.acategory_retrieve", mocks["acategory"]
) as mock_facade,
):
await search_knowledge(query="neural nets", method="keyword")
mock_facade.assert_awaited_once()
call_kwargs = mock_facade.call_args
assert call_kwargs[0][0] == "neural nets"
assert call_kwargs[1]["recall_n"] == 200
assert call_kwargs[1]["rerank_n"] == 50
assert call_kwargs[1]["mass_top_m"] == 50
assert call_kwargs[1]["lam"] == pytest.approx(0.1)
assert call_kwargs[1]["top_n"] == 10
async def test_top_n_capped_by_top_k_cap(self) -> None:
mocks = _patch_stack(doc_rows=[_doc_row()])
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch(
"everalgo.rank.acategory_retrieve", mocks["acategory"]
) as mock_facade,
):
# top_k=200 but top_k_cap=100 → effective_k=100
await search_knowledge(query="test", method="keyword", top_k=200)
assert mock_facade.call_args[1]["top_n"] == 100
class TestSearchKnowledgeResults:
"""Verify result assembly from facade output."""
async def test_returns_hits_with_scores(self) -> None:
c1 = _candidate(node_id="n_001", score=0.9, content="Content A.")
c2 = _candidate(
node_id="n_002", score=0.7, topic_name="CNNs", content="Content B."
)
mocks = _patch_stack(facade_return=[c1, c2], doc_rows=[_doc_row()])
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch("everalgo.rank.acategory_retrieve", mocks["acategory"]),
):
result = await search_knowledge(query="neural networks", method="keyword")
assert isinstance(result, SearchKnowledgeResult)
assert len(result.hits) == 2
assert result.total == 2
async def test_empty_results(self) -> None:
mocks = _patch_stack()
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch("everalgo.rank.acategory_retrieve", mocks["acategory"]),
):
result = await search_knowledge(query="nothing", method="keyword")
assert result.hits == []
assert result.total == 0
class TestSearchKnowledgeIncludeContent:
async def test_content_populated_when_true(self) -> None:
content_text = "Full content of neural networks topic."
c = _candidate(node_id="n_001", score=0.9, content=content_text)
mocks = _patch_stack(facade_return=[c], doc_rows=[_doc_row()])
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch("everalgo.rank.acategory_retrieve", mocks["acategory"]),
):
result = await search_knowledge(
query="neural nets", method="keyword", include_content=True
)
assert result.hits[0].content == content_text
async def test_content_none_when_false(self) -> None:
c = _candidate(node_id="n_001", score=0.9, content="Some content.")
mocks = _patch_stack(facade_return=[c], doc_rows=[_doc_row()])
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch("everalgo.rank.acategory_retrieve", mocks["acategory"]),
):
result = await search_knowledge(
query="neural nets", method="keyword", include_content=False
)
assert result.hits[0].content is None
class TestSearchKnowledgeScoreThreshold:
async def test_filters_low_score_candidates(self) -> None:
high = _candidate(node_id="n_001", score=0.9)
low = _candidate(node_id="n_002", score=0.1, topic_name="Low")
mocks = _patch_stack(facade_return=[high, low], doc_rows=[_doc_row()])
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch("everalgo.rank.acategory_retrieve", mocks["acategory"]),
):
result = await search_knowledge(
query="neural nets", method="keyword", score_threshold=0.5
)
assert len(result.hits) == 1
assert result.hits[0].topic_id == "n_001"
async def test_no_filtering_when_threshold_none(self) -> None:
c1 = _candidate(node_id="n_001", score=0.9)
c2 = _candidate(node_id="n_002", score=0.1, topic_name="Low")
mocks = _patch_stack(facade_return=[c1, c2], doc_rows=[_doc_row()])
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch("everalgo.rank.acategory_retrieve", mocks["acategory"]),
):
result = await search_knowledge(query="test", method="keyword")
assert len(result.hits) == 2
class TestSearchKnowledgeDocumentContext:
async def test_hits_carry_document_context(self) -> None:
c = _candidate(node_id="n_001", score=0.9)
doc = _doc_row(title="AI Handbook", summary="A handbook on AI.")
mocks = _patch_stack(facade_return=[c], doc_rows=[doc])
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch("everalgo.rank.acategory_retrieve", mocks["acategory"]),
):
result = await search_knowledge(query="AI", method="keyword")
hit = result.hits[0]
assert isinstance(hit.document, DocumentContext)
assert hit.document.doc_id == "d_testdoc00001"
assert hit.document.title == "AI Handbook"
assert hit.document.summary == "A handbook on AI."
class TestSearchKnowledgeTookMs:
async def test_took_ms_positive(self) -> None:
mocks = _patch_stack()
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch("everalgo.rank.acategory_retrieve", mocks["acategory"]),
):
result = await search_knowledge(query="test", method="keyword")
assert result.took_ms >= 0
class TestSearchKnowledgeReturnFields:
"""Verify all SearchHit fields are correctly populated."""
async def test_all_hit_fields_populated(self) -> None:
c = _candidate(
node_id="n_001",
score=0.85,
source="keyword",
doc_id="d_testdoc00001",
category_id="Technology",
topic_name="Neural Networks",
topic_path="Technology / Neural Networks",
depth=1,
summary="Overview of neural networks.",
)
mocks = _patch_stack(facade_return=[c], doc_rows=[_doc_row()])
with (
patch(f"{_MOD}._build_recaller", return_value=mocks["recaller"]),
patch(f"{_MOD}.knowledge_document_repo", mocks["doc_repo"]),
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
patch("everalgo.rank.acategory_retrieve", mocks["acategory"]),
):
result = await search_knowledge(query="neural", method="keyword")
hit = result.hits[0]
assert hit.topic_id == "n_001"
assert hit.category_id == "Technology"
assert hit.topic_name == "Neural Networks"
assert hit.topic_path == "Technology / Neural Networks"
assert hit.depth == 1
assert hit.summary == "Overview of neural networks."
assert hit.retrieval_method == "keyword"
assert hit.source == "keyword"
class TestSearchKnowledgeProviderRequired:
"""Missing providers raise ConfigurationError (HTTP 500 CONFIGURATION_ERROR)."""
async def test_raises_without_embedding(self) -> None:
mocks = _patch_stack()
with (
patch(f"{_MOD}._get_embedding", return_value=None),
patch(f"{_MOD}._get_reranker", return_value=mocks["reranker"]),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
pytest.raises(ConfigurationError, match="Embedding provider"),
):
await search_knowledge(query="test", method="keyword")
async def test_raises_without_reranker(self) -> None:
mocks = _patch_stack()
with (
patch(f"{_MOD}._get_embedding", return_value=mocks["embedder"]),
patch(f"{_MOD}._get_reranker", return_value=None),
patch(f"{_CONFIG_MOD}.load_settings", return_value=mocks["settings"]),
pytest.raises(ConfigurationError, match="Rerank provider"),
):
await search_knowledge(query="test", method="keyword")