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