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.
120 lines
3.8 KiB
Python
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}"
|
|
)
|