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,104 @@
|
||||
# Feature: remove-embedding-competence-similarity, Property 5: compute_role_similarity Wertebereich
|
||||
"""Property-based tests for compute_role_similarity value range."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from teamlandkarte_mcp.matching.similarity import SimilarityEngine
|
||||
|
||||
|
||||
# Strategy: role name strings (ASCII letters only, 1-50 chars,
|
||||
# no "(unknown)")
|
||||
_role_str = st.text(
|
||||
alphabet=st.characters(
|
||||
whitelist_categories=("L",),
|
||||
whitelist_characters="",
|
||||
max_codepoint=127,
|
||||
),
|
||||
min_size=1,
|
||||
max_size=50,
|
||||
).filter(lambda s: s.strip().lower() != "(unknown)")
|
||||
|
||||
# Strategy: similarity score returned by the LLM (intentionally includes
|
||||
# out-of-range values to verify clamping behaviour)
|
||||
_llm_score = st.floats(min_value=-1.0, max_value=2.0)
|
||||
|
||||
|
||||
class _DummyClient:
|
||||
"""Stub that returns a configurable similarity score."""
|
||||
|
||||
def __init__(self, score: float = 0.5) -> None:
|
||||
self._score = score
|
||||
|
||||
async def chat_completion(self, system: str, user: str) -> str:
|
||||
return json.dumps({"similarity": self._score})
|
||||
|
||||
|
||||
# **Validates: Requirement 5.1 (value range [0.0, 1.0])**
|
||||
@settings(max_examples=100)
|
||||
@given(role_a=_role_str, role_b=_role_str, score=_llm_score)
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_similarity_always_in_unit_interval(
|
||||
role_a: str, role_b: str, score: float
|
||||
) -> None:
|
||||
"""Property 5a: compute_role_similarity always returns a value in [0.0, 1.0].
|
||||
|
||||
For any two non-empty, non-"(unknown)" role names, the returned
|
||||
similarity must lie in the closed interval [0.0, 1.0], regardless of
|
||||
what the underlying LLM returns (clamping).
|
||||
"""
|
||||
client = _DummyClient(score=score)
|
||||
engine = SimilarityEngine(client=client) # type: ignore[arg-type]
|
||||
|
||||
result = await engine.compute_role_similarity(role_a, role_b)
|
||||
|
||||
assert isinstance(result, float), (
|
||||
f"Expected float, got {type(result)}"
|
||||
)
|
||||
assert 0.0 <= result <= 1.0, (
|
||||
f"Score {result} out of range [0.0, 1.0] "
|
||||
f"(roles: {role_a!r}, {role_b!r}, llm_score: {score})"
|
||||
)
|
||||
|
||||
|
||||
# **Validates: Requirement 5.1 (identity → 1.0)**
|
||||
@settings(max_examples=100)
|
||||
@given(role=_role_str)
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_similarity_identity_returns_one(
|
||||
role: str,
|
||||
) -> None:
|
||||
"""Property 5b: identical role names (case-insensitive) return 1.0.
|
||||
|
||||
The engine short-circuits without calling the LLM when both role
|
||||
arguments normalize to the same string.
|
||||
"""
|
||||
|
||||
class _FailingClient:
|
||||
"""Client that fails if called (identity must short-circuit)."""
|
||||
|
||||
async def chat_completion(self, system: str, user: str) -> str:
|
||||
raise AssertionError(
|
||||
"LLM should not be called for identical roles"
|
||||
)
|
||||
|
||||
engine = SimilarityEngine(client=_FailingClient()) # type: ignore[arg-type]
|
||||
|
||||
# Same role, same casing
|
||||
result = await engine.compute_role_similarity(role, role)
|
||||
assert result == 1.0, (
|
||||
f"Expected 1.0 for identical role {role!r}, got {result}"
|
||||
)
|
||||
|
||||
# Same role, different casing (upper vs lower)
|
||||
result_mixed = await engine.compute_role_similarity(
|
||||
role.upper(), role.lower()
|
||||
)
|
||||
assert result_mixed == 1.0, (
|
||||
f"Expected 1.0 for case-insensitive match "
|
||||
f"({role.upper()!r} vs {role.lower()!r}), got {result_mixed}"
|
||||
)
|
||||
Reference in New Issue
Block a user