# Feature: llm-batch-matching, Property 1: Vollständigkeit der Ergebnisse """Property-based test for result completeness (partition) in batch matching. For any list of capacities and any mix of LLM successes/failures, the total number of items across all ``by_category`` buckets plus the number of entries in ``errors`` must equal the number of input candidates. No candidate may be lost or duplicated. The fake LLM is driven by a Hypothesis-generated boolean mask: position ``i`` decides whether the call for capacity ``i+1`` raises ``AzureAPIError`` (``True``) or returns a valid JSON payload (``False``). **Validates: Requirements 1.1, 1.4, 3.2, 4.2, 4.3** """ from __future__ import annotations import asyncio import json from typing import Any from hypothesis import given, settings from hypothesis import strategies as st from teamlandkarte_mcp.azure.openai_client import AzureAPIError from teamlandkarte_mcp.matching.llm_fulltext_matcher import LlmFulltextMatcher from teamlandkarte_mcp.matching.profiles import TaskProfile from teamlandkarte_mcp.models import Capacity class _MaskLlm: """Returns valid JSON for mask[i] is False; raises AzureAPIError for True.""" def __init__(self, mask: list[bool]) -> None: self._mask = mask self._call_index = 0 async def chat_completion(self, system: str, user: str) -> str: idx = self._call_index self._call_index += 1 if self._mask[idx]: raise AzureAPIError("simulated") return json.dumps({"category": "Top", "rationale": "ok"}) class _StubDb: """Minimal DBClient double returning empty extras for every id.""" 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_capacities(n: int) -> list[Capacity]: return [ Capacity( id=i + 1, owner_name=f"o{i + 1}", role_name="R", role_level=None, begin_date=None, end_date=None, competences=[], ) for i in range(n) ] @settings(max_examples=100, deadline=None) @given(mask=st.lists(st.booleans(), min_size=1, max_size=20)) def test_batch_completeness_partition(mask: list[bool]) -> None: """Property 1: all input candidates appear exactly once in output. The sum of items across all by_category buckets plus the number of errors must equal the number of input capacities. Additionally, all item_ids across by_category and errors must be unique (no duplicates). """ matcher = LlmFulltextMatcher(db=_StubDb(), client=_MaskLlm(mask)) capacities = _make_capacities(len(mask)) task_profile = TaskProfile(id="t", title="T", description="D", skills=[]) result = asyncio.run( matcher.match_capacities( task_profile=task_profile, capacities=capacities, ) ) # Collect all item_ids from by_category category_ids: list[str] = [] for items in result.by_category.values(): for it in items: category_ids.append(it.item_id) # Collect all item_ids from errors error_ids: list[str] = [e.item_id for e in result.errors] # Property: completeness – no candidate lost total = len(category_ids) + len(error_ids) assert total == len(capacities), ( f"Expected {len(capacities)} total items, got {total} " f"({len(category_ids)} categorized + {len(error_ids)} errors)" ) # Property: no duplicates – all item_ids are unique all_ids = category_ids + error_ids assert len(all_ids) == len(set(all_ids)), ( f"Duplicate item_ids detected: {len(all_ids)} total vs " f"{len(set(all_ids))} unique" )