Files
Orchestrator/bahn/teamlandkarte-mcp/tests/test_pbt_role_similarity_cache.py
T
ankn a5f8fb49ab 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.
2026-06-30 20:39:52 +02:00

120 lines
3.8 KiB
Python

# Feature: remove-embedding-competence-similarity, Property 6: Rollen-Similarity-Cache ist symmetrisch und idempotent
"""Property-based tests for role similarity cache symmetry and idempotency."""
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",),
max_codepoint=127,
),
min_size=1,
max_size=50,
).filter(lambda s: s.strip().lower() != "(unknown)")
class _DummyClient:
"""Stub that returns a fixed similarity score and tracks call count."""
def __init__(self, score: float = 0.72) -> None:
self._score = score
self.call_count = 0
async def chat_completion(self, system: str, user: str) -> str:
self.call_count += 1
return json.dumps({"similarity": self._score})
# **Validates: Requirements 5.8**
@settings(max_examples=100)
@given(role_a=_role_str, role_b=_role_str)
@pytest.mark.asyncio
async def test_role_similarity_cache_symmetry(
role_a: str, role_b: str,
) -> None:
"""Property 6a: Cache is symmetric.
For any role pair (A, B) where A != B (case-insensitive),
compute_role_similarity(A, B) == compute_role_similarity(B, A),
and the second call shall not invoke the LLM (cache hit).
"""
# Skip identical roles (they short-circuit to 1.0 without LLM)
if role_a.strip().lower() == role_b.strip().lower():
return
client = _DummyClient(score=0.72)
engine = SimilarityEngine(client=client) # type: ignore[arg-type]
# First call: A, B
score_ab = await engine.compute_role_similarity(role_a, role_b)
# Second call: B, A (should use cache)
score_ba = await engine.compute_role_similarity(role_b, role_a)
# Symmetry: both calls return the same score
assert score_ab == score_ba, (
f"Symmetry violated: "
f"sim({role_a!r}, {role_b!r})={score_ab} != "
f"sim({role_b!r}, {role_a!r})={score_ba}"
)
# Only one LLM call should have been made (second was a cache hit)
assert client.call_count == 1, (
f"Expected 1 LLM call but got {client.call_count} "
f"(roles: {role_a!r}, {role_b!r})"
)
# **Validates: Requirements 5.8**
@settings(max_examples=100)
@given(role_a=_role_str, role_b=_role_str)
@pytest.mark.asyncio
async def test_role_similarity_cache_idempotent(
role_a: str, role_b: str,
) -> None:
"""Property 6b: Cache is idempotent.
Repeated calls with the same role pair always return the same
cached value without additional LLM calls.
"""
# Skip identical roles (they short-circuit to 1.0 without LLM)
if role_a.strip().lower() == role_b.strip().lower():
return
client = _DummyClient(score=0.65)
engine = SimilarityEngine(client=client) # type: ignore[arg-type]
# First call populates the cache
score_1 = await engine.compute_role_similarity(role_a, role_b)
assert client.call_count == 1
# Second call with same order: cache hit
score_2 = await engine.compute_role_similarity(role_a, role_b)
assert score_2 == score_1, (
f"Idempotency violated: call 1={score_1}, call 2={score_2}"
)
assert client.call_count == 1, (
f"Expected 1 LLM call after 2 same-order calls, "
f"got {client.call_count}"
)
# Third call with reversed order: also cache hit
score_3 = await engine.compute_role_similarity(role_b, role_a)
assert score_3 == score_1, (
f"Idempotency violated on reverse: "
f"call 1={score_1}, call 3={score_3}"
)
assert client.call_count == 1, (
f"Expected 1 LLM call after 3 total calls, "
f"got {client.call_count}"
)