Files
Orchestrator/bahn/teamlandkarte-mcp/tests/test_batch_mapping_pbt.py
T
ankn a5f8fb49ab 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.
2026-06-30 20:39:52 +02:00

102 lines
3.5 KiB
Python

# Feature: llm-batch-matching, Property 5: Korrekte Zuordnung (Response-Mapping)
"""Property-based test for correct response-to-candidate mapping in batch matching.
For any list of capacities with item_id-specific category assignments, each
item must land in exactly the category that the mock LLM returned for it.
This validates that concurrent execution does not mix up responses between
candidates.
**Validates: Requirements 3.4**
"""
from __future__ import annotations
import asyncio
import json
import re
from typing import Any
from hypothesis import given, settings
from hypothesis import strategies as st
from teamlandkarte_mcp.matching.llm_fulltext_matcher import LlmFulltextMatcher
from teamlandkarte_mcp.matching.profiles import TaskProfile
from teamlandkarte_mcp.models import Capacity
_ID_RE = re.compile(r"ID:\s*(\S+)")
_CATEGORIES: tuple[str, ...] = ("Top", "Good", "Partial", "Low", "Irrelevant")
class _MapLlm:
def __init__(self, id_to_category: dict[str, str]) -> None:
self._map = id_to_category
async def chat_completion(self, system: str, user: str) -> str:
match = _ID_RE.search(user)
cap_id = match.group(1) if match else "unknown"
category = self._map.get(cap_id, "Irrelevant")
await asyncio.sleep(0.005) # simulate I/O
return json.dumps({"category": category, "rationale": f"r{cap_id}"})
class _StubDb:
def batch_get_capacity_descriptions(self, capacity_ids: list[Any]) -> dict[str, str | None]:
return {str(i): None for i in capacity_ids}
def batch_get_capacity_certificates(self, capacity_ids: list[Any]) -> dict[str, list[str]]:
return {str(i): [] for i in capacity_ids}
def batch_get_capacity_references(self, capacity_ids: list[Any]) -> dict[str, list[dict]]:
return {str(i): [] for i in capacity_ids}
def _make_capacity(cap_id: int) -> Capacity:
return Capacity(
id=cap_id,
owner_name=f"o{cap_id}",
role_name="R",
role_level=None,
begin_date=None,
end_date=None,
competences=[],
)
@settings(max_examples=100, deadline=None)
@given(
ids=st.lists(st.integers(min_value=1, max_value=99), min_size=2, max_size=15, unique=True),
max_concurrency=st.integers(min_value=1, max_value=5),
data=st.data(),
)
def test_batch_correct_mapping(
ids: list[int], max_concurrency: int, data: st.DataObject
) -> None:
"""Property 5: each item lands in exactly the category the mock assigned."""
id_to_category = {str(i): data.draw(st.sampled_from(_CATEGORIES)) for i in ids}
capacities = [_make_capacity(i) for i in ids]
task_profile = TaskProfile(id="t", title="T", description="D", skills=[])
matcher = LlmFulltextMatcher(
db=_StubDb(), client=_MapLlm(id_to_category), max_concurrency=max_concurrency,
)
result = asyncio.run(
matcher.match_capacities(task_profile=task_profile, capacities=capacities)
)
# Build a mapping from item_id to actual category in the result
actual_mapping: dict[str, str] = {}
for cat, items in result.by_category.items():
for it in items:
actual_mapping[it.item_id] = cat
# Assert: every capacity ended up in the correct category
for cap_id_str, expected_cat in id_to_category.items():
assert cap_id_str in actual_mapping, (
f"Capacity {cap_id_str} not found in any category"
)
assert actual_mapping[cap_id_str] == expected_cat, (
f"Capacity {cap_id_str}: expected {expected_cat}, "
f"got {actual_mapping[cap_id_str]}"
)