Migrate all repos into monorepo context folders
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.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# 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)]
|
||||
Reference in New Issue
Block a user