Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
199 lines
6.8 KiB
Python
199 lines
6.8 KiB
Python
# Unit tests for VocabularyCache.infer_competences with mocked LLM client.
|
|
#
|
|
# **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5**
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
from teamlandkarte_mcp.matching.vocabulary import VocabularyCache
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fakes / Stubs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class FakeDB:
|
|
"""Stub DBClient returning a configurable competence list."""
|
|
|
|
def __init__(self, competences: list[str] | None = None) -> None:
|
|
self._competences = competences if competences is not None else []
|
|
|
|
def get_all_competence_names(self) -> list[str]:
|
|
return self._competences
|
|
|
|
def get_all_role_names(self) -> list[str]:
|
|
return ["Software Engineer"]
|
|
|
|
|
|
class FakeLLMClient:
|
|
"""Stub AzureOpenAIClient that returns a pre-configured response."""
|
|
|
|
def __init__(self, response: str | Exception) -> None:
|
|
self._response = response
|
|
|
|
async def chat_completion(self, system: str, user: str) -> str:
|
|
if isinstance(self._response, Exception):
|
|
raise self._response
|
|
return self._response
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_valid_json_response_returns_correct_tuples() -> None:
|
|
"""Valid JSON response from LLM → correct list of (name, confidence) tuples."""
|
|
competences = ["Python", "TypeScript", "React", "Docker"]
|
|
llm_response = json.dumps(
|
|
{
|
|
"competences": [
|
|
{"name": "Python", "confidence": 0.95},
|
|
{"name": "React", "confidence": 0.8},
|
|
]
|
|
}
|
|
)
|
|
db = FakeDB(competences=competences)
|
|
client = FakeLLMClient(response=llm_response)
|
|
cache = VocabularyCache(db=db, client=client) # type: ignore[arg-type]
|
|
|
|
result = await cache.infer_competences(task_text="Build a Python backend")
|
|
|
|
assert result == [("Python", 0.95), ("React", 0.8)]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_task_text_returns_empty_list() -> None:
|
|
"""Empty task text → []."""
|
|
competences = ["Python", "TypeScript"]
|
|
llm_response = json.dumps({"competences": [{"name": "Python", "confidence": 0.9}]})
|
|
db = FakeDB(competences=competences)
|
|
client = FakeLLMClient(response=llm_response)
|
|
cache = VocabularyCache(db=db, client=client) # type: ignore[arg-type]
|
|
|
|
assert await cache.infer_competences(task_text="") == []
|
|
assert await cache.infer_competences(task_text=" ") == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_competence_list_in_db_returns_empty_list() -> None:
|
|
"""Empty competence list in DB → []."""
|
|
llm_response = json.dumps({"competences": [{"name": "Python", "confidence": 0.9}]})
|
|
db = FakeDB(competences=[])
|
|
client = FakeLLMClient(response=llm_response)
|
|
cache = VocabularyCache(db=db, client=client) # type: ignore[arg-type]
|
|
|
|
result = await cache.infer_competences(task_text="Build a Python backend")
|
|
|
|
assert result == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_llm_error_returns_empty_list_and_logs_warning(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""LLM error (Exception) → [] + warning logged."""
|
|
competences = ["Python", "TypeScript"]
|
|
db = FakeDB(competences=competences)
|
|
client = FakeLLMClient(response=RuntimeError("API timeout"))
|
|
cache = VocabularyCache(db=db, client=client) # type: ignore[arg-type]
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
result = await cache.infer_competences(task_text="Build a Python backend")
|
|
|
|
assert result == []
|
|
assert any("LLM call failed" in record.message for record in caplog.records)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_json_response_returns_empty_list(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Invalid JSON response → [] (json.loads raises, caught by except)."""
|
|
competences = ["Python", "TypeScript"]
|
|
db = FakeDB(competences=competences)
|
|
client = FakeLLMClient(response="this is not valid json {{{")
|
|
cache = VocabularyCache(db=db, client=client) # type: ignore[arg-type]
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
result = await cache.infer_competences(task_text="Build a Python backend")
|
|
|
|
assert result == []
|
|
assert any("LLM call failed" in record.message for record in caplog.records)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unknown_competence_names_are_filtered_out() -> None:
|
|
"""Competence names not in DB → filtered out."""
|
|
competences = ["Python", "TypeScript", "React"]
|
|
llm_response = json.dumps(
|
|
{
|
|
"competences": [
|
|
{"name": "Python", "confidence": 0.9},
|
|
{"name": "Rust", "confidence": 0.85}, # not in DB
|
|
{"name": "Go", "confidence": 0.7}, # not in DB
|
|
{"name": "React", "confidence": 0.6},
|
|
]
|
|
}
|
|
)
|
|
db = FakeDB(competences=competences)
|
|
client = FakeLLMClient(response=llm_response)
|
|
cache = VocabularyCache(db=db, client=client) # type: ignore[arg-type]
|
|
|
|
result = await cache.infer_competences(task_text="Build a backend service")
|
|
|
|
assert result == [("Python", 0.9), ("React", 0.6)]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_competences_limited_to_10() -> None:
|
|
"""Result is limited to max_competences (default 10)."""
|
|
# Create 15 competences and have the LLM return all of them
|
|
competences = [f"Competence_{i}" for i in range(15)]
|
|
llm_response = json.dumps(
|
|
{
|
|
"competences": [
|
|
{"name": name, "confidence": round(0.5 + i * 0.03, 2)}
|
|
for i, name in enumerate(competences)
|
|
]
|
|
}
|
|
)
|
|
db = FakeDB(competences=competences)
|
|
client = FakeLLMClient(response=llm_response)
|
|
cache = VocabularyCache(db=db, client=client) # type: ignore[arg-type]
|
|
|
|
result = await cache.infer_competences(task_text="A complex task")
|
|
|
|
assert len(result) == 10
|
|
# All returned names must be in the DB list
|
|
for name, confidence in result:
|
|
assert name in competences
|
|
assert 0.0 <= confidence <= 1.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_confidence_clamped_to_valid_range() -> None:
|
|
"""Confidence values outside [0.0, 1.0] are clamped."""
|
|
competences = ["Python", "TypeScript"]
|
|
llm_response = json.dumps(
|
|
{
|
|
"competences": [
|
|
{"name": "Python", "confidence": 1.5}, # above 1.0
|
|
{"name": "TypeScript", "confidence": -0.3}, # below 0.0
|
|
]
|
|
}
|
|
)
|
|
db = FakeDB(competences=competences)
|
|
client = FakeLLMClient(response=llm_response)
|
|
cache = VocabularyCache(db=db, client=client) # type: ignore[arg-type]
|
|
|
|
result = await cache.infer_competences(task_text="Build something")
|
|
|
|
assert result == [("Python", 1.0), ("TypeScript", 0.0)]
|