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.
194 lines
6.5 KiB
Python
194 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
from teamlandkarte_mcp.cache.search_cache import SearchCache
|
|
|
|
|
|
def test_filter_id_increments_under_same_search_id() -> None:
|
|
cache = SearchCache(ttl_minutes=60, max_size=10)
|
|
search_id = cache.store_search(
|
|
task_id=None, requirements={}, results={"by_category": {}}
|
|
)
|
|
|
|
filter_id_1 = cache.add_filter(
|
|
search_id, filtered_results={"by_category": {}}, filter_meta={}
|
|
)
|
|
filter_id_2 = cache.add_filter(
|
|
search_id, filtered_results={"by_category": {}}, filter_meta={}
|
|
)
|
|
|
|
assert filter_id_1 == "filter-1"
|
|
assert filter_id_2 == "filter-2"
|
|
|
|
entry = cache.get(search_id)
|
|
assert entry is not None
|
|
assert filter_id_1 in entry.filters
|
|
assert filter_id_2 in entry.filters
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Team-search coverage (Profile_Type="team")
|
|
# ---------------------------------------------------------------------------
|
|
#
|
|
# The SearchCache itself is search-type-agnostic - it stores whatever
|
|
# ``results`` payload it receives. The tests below exercise the same
|
|
# cache primitives (``store_search``, ``add_filter``, ``get``) for
|
|
# ``team_search`` payloads in both ``matching_method`` flavours
|
|
# (``score`` and ``llm_fulltext``) so the entire cache surface is
|
|
# covered for both ``Profile_Type`` values.
|
|
#
|
|
# _Requirements: 8.1, 8.2, 8.3, 12.7_
|
|
|
|
|
|
def _team_payload(matching_method: str) -> dict:
|
|
"""Return a minimal team_search payload mimicking the shapes that
|
|
``find_matching_teams`` persists in either ``matching_method`` mode.
|
|
"""
|
|
base_team = {
|
|
"team_id": "t1",
|
|
"ouid": "ou-1",
|
|
"team_name": "Team Alpha",
|
|
"focus_name": "Backend Developer",
|
|
"about_us": "",
|
|
"offerings": "",
|
|
"interests": "",
|
|
"competences": [{"name": "python", "top_competency": True}],
|
|
"references": [],
|
|
"category": "Top",
|
|
}
|
|
if matching_method == "score":
|
|
scored_team = {
|
|
**base_team,
|
|
"competence_score": 1.0,
|
|
"role_score": 1.0,
|
|
"overall_score": 1.0,
|
|
}
|
|
return {
|
|
"search_type": "team_search",
|
|
"matching_method": "score",
|
|
"reference": {
|
|
"availability_date_start": None,
|
|
"availability_date_end": None,
|
|
},
|
|
"summary": {"Top": 1, "Good": 0, "Partial": 0, "Low": 0,
|
|
"Irrelevant": 0},
|
|
"by_category": {
|
|
"Top": [scored_team],
|
|
"Good": [],
|
|
"Partial": [],
|
|
"Low": [],
|
|
"Irrelevant": [],
|
|
},
|
|
}
|
|
# llm_fulltext
|
|
llm_team = {**base_team, "rationale": "Backend match."}
|
|
return {
|
|
"search_type": "team_search",
|
|
"matching_method": "llm_fulltext",
|
|
"reference": {
|
|
"availability_date_start": None,
|
|
"availability_date_end": None,
|
|
},
|
|
"summary": {"Top": 1, "Good": 0, "Partial": 0, "Low": 0,
|
|
"Irrelevant": 0},
|
|
"by_category": {
|
|
"Top": [llm_team],
|
|
"Good": [],
|
|
"Partial": [],
|
|
"Low": [],
|
|
"Irrelevant": [],
|
|
},
|
|
"errors": [],
|
|
}
|
|
|
|
|
|
def test_team_search_cache_roundtrip_score_mode() -> None:
|
|
"""``team_search`` payloads round-trip through the cache and keep
|
|
their ``search_type``/``matching_method`` markers (Anforderungen
|
|
8.1, 8.2, 8.3)."""
|
|
cache = SearchCache(ttl_minutes=60, max_size=10)
|
|
payload = _team_payload("score")
|
|
|
|
search_id = cache.store_search(
|
|
task_id=None,
|
|
requirements={"role_name": "Backend Developer",
|
|
"competences": ["python"]},
|
|
results=payload,
|
|
)
|
|
|
|
entry = cache.get(search_id)
|
|
assert entry is not None
|
|
assert entry.results["search_type"] == "team_search"
|
|
assert entry.results["matching_method"] == "score"
|
|
# The cached items keep the team-specific fields used by the
|
|
# team-search column layout (Anforderung 8.4).
|
|
top = entry.results["by_category"]["Top"]
|
|
assert top and top[0]["team_id"] == "t1"
|
|
assert "competence_score" in top[0]
|
|
assert "role_score" in top[0]
|
|
assert "overall_score" in top[0]
|
|
|
|
|
|
def test_team_search_cache_roundtrip_llm_fulltext_mode() -> None:
|
|
"""``team_search`` LLM-mode payloads survive a cache round trip
|
|
including the ``rationale`` field used by ``Begründung``."""
|
|
cache = SearchCache(ttl_minutes=60, max_size=10)
|
|
payload = _team_payload("llm_fulltext")
|
|
|
|
search_id = cache.store_search(
|
|
task_id=None,
|
|
requirements={"role_name": "Backend Developer",
|
|
"competences": ["python"]},
|
|
results=payload,
|
|
)
|
|
|
|
entry = cache.get(search_id)
|
|
assert entry is not None
|
|
assert entry.results["search_type"] == "team_search"
|
|
assert entry.results["matching_method"] == "llm_fulltext"
|
|
# ``errors`` list is preserved for the LLM-fulltext path
|
|
# (Anforderung 7.6).
|
|
assert entry.results.get("errors") == []
|
|
top = entry.results["by_category"]["Top"]
|
|
assert top and top[0]["team_id"] == "t1"
|
|
assert top[0]["category"] == "Top"
|
|
assert top[0]["rationale"] == "Backend match."
|
|
|
|
|
|
def test_team_search_filter_ids_increment_independently() -> None:
|
|
"""``add_filter`` produces incrementing ids for ``team_search``
|
|
entries just like for capacity searches.
|
|
"""
|
|
cache = SearchCache(ttl_minutes=60, max_size=10)
|
|
search_id = cache.store_search(
|
|
task_id=None,
|
|
requirements={"role_name": "Backend Developer",
|
|
"competences": ["python"]},
|
|
results=_team_payload("score"),
|
|
)
|
|
|
|
fid1 = cache.add_filter(
|
|
search_id,
|
|
filtered_results={"by_category": {}, "search_type": "team_search",
|
|
"matching_method": "score"},
|
|
filter_meta={"role_filter": "Backend"},
|
|
)
|
|
fid2 = cache.add_filter(
|
|
search_id,
|
|
filtered_results={"by_category": {}, "search_type": "team_search",
|
|
"matching_method": "score"},
|
|
filter_meta={"role_filter": "Frontend"},
|
|
)
|
|
|
|
assert fid1 == "filter-1"
|
|
assert fid2 == "filter-2"
|
|
|
|
entry = cache.get(search_id)
|
|
assert entry is not None
|
|
# The filter payloads keep the team_search marker independent of
|
|
# the parent entry, so downstream consumers can distinguish them.
|
|
for fid in (fid1, fid2):
|
|
fdata = entry.filters[fid]
|
|
assert fdata["results"]["search_type"] == "team_search"
|
|
assert fdata["results"]["matching_method"] == "score"
|
|
|