# Feature: llm-batch-matching, Property 2: Concurrency-Begrenzung """Property-based test for concurrency limiting in batch matching. For any number of candidates and any valid ``max_concurrency`` value, the number of simultaneously running LLM calls must never exceed the configured ``max_concurrency`` limit. A mock LLM client uses an asyncio counter (protected by a lock) to track concurrent invocations and records the maximum observed concurrency. After matching completes, we assert that the peak never exceeded the configured limit. **Validates: Requirements 1.2** """ 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.matching.llm_fulltext_matcher import LlmFulltextMatcher from teamlandkarte_mcp.matching.profiles import TaskProfile from teamlandkarte_mcp.models import Capacity class _ConcurrencyTracker: """Mock LLM client that tracks peak concurrent invocations.""" def __init__(self) -> None: self._current = 0 self._max_seen = 0 self._lock = asyncio.Lock() async def chat_completion(self, system: str, user: str) -> str: async with self._lock: self._current += 1 if self._current > self._max_seen: self._max_seen = self._current await asyncio.sleep(0.01) # simulate I/O async with self._lock: self._current -= 1 return json.dumps({"category": "Top", "rationale": "ok"}) @property def max_concurrent(self) -> int: return self._max_seen 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=50, deadline=None) @given( max_concurrency=st.integers(min_value=1, max_value=10), num_candidates=st.integers(min_value=1, max_value=30), ) def test_batch_concurrency_limit( max_concurrency: int, num_candidates: int ) -> None: """Property 2: peak concurrent LLM calls never exceed max_concurrency. We generate a random max_concurrency (1-10) and a random number of candidates (1-30), then verify that the concurrency tracker never observed more simultaneous calls than the configured limit. """ tracker = _ConcurrencyTracker() matcher = LlmFulltextMatcher( db=_StubDb(), client=tracker, max_concurrency=max_concurrency, ) capacities = _make_capacities(num_candidates) task_profile = TaskProfile(id="t", title="T", description="D", skills=[]) asyncio.run( matcher.match_capacities( task_profile=task_profile, capacities=capacities, ) ) assert tracker.max_concurrent <= max_concurrency, ( f"Observed {tracker.max_concurrent} concurrent calls, " f"but max_concurrency was set to {max_concurrency}" )