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,120 @@
|
||||
# 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}"
|
||||
)
|
||||
Reference in New Issue
Block a user