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.
105 lines
3.3 KiB
Python
105 lines
3.3 KiB
Python
# 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}"
|
|
)
|