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