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,732 @@
|
||||
"""End-to-end integration tests for LLM-fulltext matching (tasks 12.2 and
|
||||
12.3 of the ``llm-fulltext-matching`` spec).
|
||||
|
||||
These tests exercise ``find_matching_capacities`` and
|
||||
``find_matching_tasks`` through ``build_server`` using fully mocked
|
||||
``DBClient`` and ``AzureOpenAIClient`` collaborators. They cover:
|
||||
|
||||
* Task 12.2 — End-to-end runs in ``llm_fulltext`` mode for both
|
||||
directions:
|
||||
- The persisted ``SearchCache`` payload carries
|
||||
``matching_method``, the **ungekürzte** rationale and an
|
||||
``errors`` list.
|
||||
- The availability prefilter behaves identically in score and
|
||||
LLM-fulltext mode (out-of-window capacities are dropped).
|
||||
* Task 12.3 — Markdown table headers are stable across modes:
|
||||
- Score mode keeps the existing ``Role Score`` / ``Competence
|
||||
Score`` / ``Overall Score`` columns.
|
||||
- LLM mode ends with ``... | Category | Begründung |`` and
|
||||
drops all numeric score columns.
|
||||
|
||||
_Requirements: 5.1, 5.2, 6.1, 6.2, 7.1, 7.4, 8.1, 8.2, 8.6, 9.5_
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
import teamlandkarte_mcp.mcp_server as mcp_mod
|
||||
from teamlandkarte_mcp.azure.openai_client import AzureAPIError
|
||||
from teamlandkarte_mcp.cache.search_cache import SearchCache
|
||||
from teamlandkarte_mcp.mcp_server import build_server
|
||||
from teamlandkarte_mcp.models import Capacity
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test config and helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_CONFIG_TEMPLATE = """
|
||||
[database]
|
||||
host='x'
|
||||
port=1
|
||||
username='u'
|
||||
password='p'
|
||||
backend='trino'
|
||||
|
||||
[matching]
|
||||
competence_weight=0.8
|
||||
role_weight=0.2
|
||||
require_confirmation=false
|
||||
|
||||
[cache]
|
||||
db_ttl_hours=1
|
||||
search_ttl_minutes=60
|
||||
max_size=100
|
||||
|
||||
[azure_openai]
|
||||
endpoint='https://example.openai.azure.com'
|
||||
api_version='2024-02-15-preview'
|
||||
chat_deployment='gpt-4'
|
||||
""".strip()
|
||||
|
||||
|
||||
def _result_to_text(result: Any) -> str:
|
||||
if isinstance(result, tuple) and len(result) == 2:
|
||||
content, structured = result
|
||||
if isinstance(structured, dict) and isinstance(
|
||||
structured.get("result"), str
|
||||
):
|
||||
return structured["result"]
|
||||
if isinstance(content, list) and content:
|
||||
text = getattr(content[0], "text", None)
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
return str(result)
|
||||
|
||||
|
||||
async def _call_tool(srv: Any, name: str, args: dict[str, Any]) -> str:
|
||||
return _result_to_text(await srv.call_tool(name, args))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fake DB
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Task:
|
||||
"""In-test stand-in for the production ``Task`` dataclass."""
|
||||
|
||||
id: str
|
||||
title: str
|
||||
description: str
|
||||
skills: list[str] = field(default_factory=list)
|
||||
name: str | None = None
|
||||
created_date: datetime = datetime(2025, 1, 1)
|
||||
start_date: date | None = None
|
||||
end_date: date | None = None
|
||||
|
||||
|
||||
class _FakeDB:
|
||||
"""In-memory ``DBClient`` double used across the integration tests.
|
||||
|
||||
The fake supports both directions (capacity_search and task_search)
|
||||
and exposes both the single-row and the batch capacity-fulltext
|
||||
methods used by the LLM matcher.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
capacities: list[Capacity] | None = None,
|
||||
tasks: list[Any] | None = None,
|
||||
capacity_extras: dict[
|
||||
str,
|
||||
dict[str, Any],
|
||||
]
|
||||
| None = None,
|
||||
) -> None:
|
||||
self._capacities = capacities or []
|
||||
self._tasks = tasks or []
|
||||
# Map capacity_id (str) -> {description, certificates, references}.
|
||||
self._extras = capacity_extras or {}
|
||||
|
||||
# ---- generic stubs used by build_server boot-time --------------------
|
||||
|
||||
def test_connection(self) -> None:
|
||||
return
|
||||
|
||||
def get_table_columns(self, table: str) -> list[str]:
|
||||
if table == "teamlandkarte_v_capacity_roles_latest":
|
||||
return ["name", "active", "staffing_board_relevant"]
|
||||
if table == "teamlandkarte_v_capacities_latest":
|
||||
return ["creation_date"]
|
||||
if table == "teamlandkarte_v_teams_latest":
|
||||
return [
|
||||
"team_id",
|
||||
"ouid",
|
||||
"about_us",
|
||||
"offerings",
|
||||
"interests",
|
||||
"focus_name",
|
||||
]
|
||||
if table == "teamlandkarte_v_teammeter_organizational_units_latest":
|
||||
return ["id", "name"]
|
||||
if table == "teamlandkarte_v_teammeter_team_competences_latest":
|
||||
return ["ouid", "competence_id", "top_competency"]
|
||||
if table == "teamlandkarte_v_team_references_latest":
|
||||
return ["ouid", "partner_id", "projects"]
|
||||
return []
|
||||
|
||||
def get_all_role_names(self) -> list[str]:
|
||||
return list({c.role_name for c in self._capacities if c.role_name})
|
||||
|
||||
def get_all_competence_names(self) -> list[str]:
|
||||
names: set[str] = set()
|
||||
for c in self._capacities:
|
||||
names.update(c.competences)
|
||||
for t in self._tasks:
|
||||
names.update(getattr(t, "skills", []) or [])
|
||||
return sorted(names)
|
||||
|
||||
# ---- capacity / task readers ----------------------------------------
|
||||
|
||||
def get_all_capacities_with_competences(self) -> list[Capacity]:
|
||||
return list(self._capacities)
|
||||
|
||||
def get_recent_free_capacities(self, limit: int = 20) -> list[Capacity]:
|
||||
return []
|
||||
|
||||
def get_capacity_by_id(self, capacity_id: int | str) -> Capacity | None:
|
||||
target = str(capacity_id)
|
||||
for c in self._capacities:
|
||||
if str(c.id) == target:
|
||||
return c
|
||||
return None
|
||||
|
||||
def get_open_tasks(self, limit: int = 20) -> list[Any]:
|
||||
return list(self._tasks) if limit == 0 else list(self._tasks)[:limit]
|
||||
|
||||
def get_task_by_id(self, task_id: str): # pragma: no cover
|
||||
return None
|
||||
|
||||
def get_task_by_name(self, name: str): # pragma: no cover
|
||||
return None
|
||||
|
||||
# ---- capacity fulltext extras ---------------------------------------
|
||||
|
||||
def get_capacity_description(
|
||||
self, capacity_id: int | str
|
||||
) -> str | None:
|
||||
return self._extras.get(str(capacity_id), {}).get("description")
|
||||
|
||||
def get_capacity_certificates(
|
||||
self, capacity_id: int | str
|
||||
) -> list[str]:
|
||||
return list(
|
||||
self._extras.get(str(capacity_id), {}).get("certificates") or []
|
||||
)
|
||||
|
||||
def get_capacity_references(
|
||||
self, capacity_id: int | str
|
||||
) -> list[dict]:
|
||||
return list(
|
||||
self._extras.get(str(capacity_id), {}).get("references") or []
|
||||
)
|
||||
|
||||
def batch_get_capacity_descriptions(
|
||||
self, capacity_ids: list[Any]
|
||||
) -> dict[str, str | None]:
|
||||
return {
|
||||
str(i): self._extras.get(str(i), {}).get("description")
|
||||
for i in capacity_ids
|
||||
}
|
||||
|
||||
def batch_get_capacity_certificates(
|
||||
self, capacity_ids: list[Any]
|
||||
) -> dict[str, list[str]]:
|
||||
return {
|
||||
str(i): list(
|
||||
self._extras.get(str(i), {}).get("certificates") or []
|
||||
)
|
||||
for i in capacity_ids
|
||||
}
|
||||
|
||||
def batch_get_capacity_references(
|
||||
self, capacity_ids: list[Any]
|
||||
) -> dict[str, list[dict]]:
|
||||
return {
|
||||
str(i): list(
|
||||
self._extras.get(str(i), {}).get("references") or []
|
||||
)
|
||||
for i in capacity_ids
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server builder
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _build_server_with(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path,
|
||||
*,
|
||||
fake_db: _FakeDB,
|
||||
chat_response: str | None = None,
|
||||
chat_side_effect: Any = None,
|
||||
) -> Any:
|
||||
"""Build an MCP server backed by ``fake_db`` and a stubbed LLM.
|
||||
|
||||
Either ``chat_response`` (constant payload) or ``chat_side_effect``
|
||||
(callable taking ``(system, user)`` and returning a string or
|
||||
raising) must be provided.
|
||||
"""
|
||||
cfg = tmp_path / "config.toml"
|
||||
cfg.write_text(_CONFIG_TEMPLATE, encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(
|
||||
mcp_mod, "create_db_client", lambda *_a, **_k: fake_db
|
||||
)
|
||||
|
||||
if chat_side_effect is not None:
|
||||
async def _fake_chat(self, system, user): # noqa: ARG001
|
||||
outcome = chat_side_effect(system, user)
|
||||
if isinstance(outcome, BaseException):
|
||||
raise outcome
|
||||
return outcome
|
||||
else:
|
||||
async def _fake_chat(self, system, user): # noqa: ARG001
|
||||
return chat_response or "{}"
|
||||
|
||||
monkeypatch.setattr(
|
||||
"teamlandkarte_mcp.azure.openai_client."
|
||||
"AzureOpenAIClient.chat_completion",
|
||||
_fake_chat,
|
||||
)
|
||||
|
||||
monkeypatch.setenv("DATA_LAKE_USERNAME", "u")
|
||||
monkeypatch.setenv("DATA_LAKE_PASSWORD", "p")
|
||||
monkeypatch.setenv("AZURE_OPENAI_LLM_API_KEY", "k")
|
||||
|
||||
return build_server(str(cfg))
|
||||
|
||||
|
||||
def _patch_capturing_store(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Patch ``SearchCache.store_search`` to capture its ``results``.
|
||||
|
||||
Returns a list that is appended to on every call (most recent
|
||||
payload last). The original method is still invoked so ``search_id``
|
||||
bookkeeping continues to work.
|
||||
"""
|
||||
captured: list[dict[str, Any]] = []
|
||||
original = SearchCache.store_search
|
||||
|
||||
def _capturing(self, *, task_id, requirements, results):
|
||||
captured.append(results)
|
||||
return original(
|
||||
self,
|
||||
task_id=task_id,
|
||||
requirements=requirements,
|
||||
results=results,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(SearchCache, "store_search", _capturing)
|
||||
return captured
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Common fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _two_capacities_one_in_window() -> list[Capacity]:
|
||||
"""Capacity 1 overlaps the test window 2025-03-01..2025-06-30, 2 does not."""
|
||||
return [
|
||||
Capacity(
|
||||
id=1,
|
||||
owner_name="Alice",
|
||||
role_name="X",
|
||||
role_level=None,
|
||||
begin_date=date(2025, 2, 1),
|
||||
end_date=date(2025, 7, 31),
|
||||
competences=["A"],
|
||||
),
|
||||
Capacity(
|
||||
id=2,
|
||||
owner_name="Bob",
|
||||
role_name="X",
|
||||
role_level=None,
|
||||
begin_date=date(2026, 1, 1),
|
||||
end_date=date(2026, 12, 31),
|
||||
competences=["A"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Task 12.2 — End-to-end integration tests
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_find_matching_capacities_llm_mode_caches_unshortened_rationale(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path
|
||||
) -> None:
|
||||
"""LLM mode persists ``matching_method``, full rationale and errors list.
|
||||
|
||||
Validates: requirements 5.1, 7.4, 8.6, 9.5.
|
||||
"""
|
||||
long_rationale = "a" * 350
|
||||
fake = _FakeDB(capacities=_two_capacities_one_in_window())
|
||||
chat_payload = json.dumps(
|
||||
{"category": "Top", "rationale": long_rationale},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
captured = _patch_capturing_store(monkeypatch)
|
||||
srv = _build_server_with(
|
||||
monkeypatch, tmp_path, fake_db=fake, chat_response=chat_payload
|
||||
)
|
||||
|
||||
res = await _call_tool(
|
||||
srv,
|
||||
"find_matching_capacities",
|
||||
{
|
||||
"role_name": "X",
|
||||
"competences": ["A"],
|
||||
"date_start": "2025-03-01",
|
||||
"date_end": "2025-06-30",
|
||||
"matching_method": "llm_fulltext",
|
||||
},
|
||||
)
|
||||
assert "SEARCH_ID=" in res, res
|
||||
assert captured, "store_search was not called"
|
||||
|
||||
payload = captured[-1]
|
||||
assert payload["matching_method"] == "llm_fulltext"
|
||||
assert isinstance(payload.get("errors"), list)
|
||||
|
||||
top_items = payload["by_category"].get("Top", [])
|
||||
assert len(top_items) == 1, (
|
||||
f"only the in-window capacity should be categorised, got "
|
||||
f"{[i.get('id') for i in top_items]}"
|
||||
)
|
||||
persisted_rationale = top_items[0]["rationale"]
|
||||
assert len(persisted_rationale) >= 350, (
|
||||
f"persisted rationale was truncated: len={len(persisted_rationale)}"
|
||||
)
|
||||
assert persisted_rationale == long_rationale
|
||||
|
||||
# Capacity 2 is outside the window and must not appear in any
|
||||
# category (availability prefilter).
|
||||
all_items = [
|
||||
item
|
||||
for cat_items in payload["by_category"].values()
|
||||
for item in cat_items
|
||||
]
|
||||
ids = {str(it.get("id")) for it in all_items}
|
||||
assert ids == {"1"}, f"expected only capacity 1, got {ids!r}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_find_matching_capacities_availability_filter_same_in_both_modes(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path
|
||||
) -> None:
|
||||
"""Score and LLM mode apply identical availability prefilters.
|
||||
|
||||
Validates: requirements 5.2, 9.5.
|
||||
"""
|
||||
fake = _FakeDB(capacities=_two_capacities_one_in_window())
|
||||
# Single payload satisfies both consumers (similarity for score path,
|
||||
# category/rationale for LLM path).
|
||||
chat_payload = json.dumps(
|
||||
{
|
||||
"similarity": 1.0,
|
||||
"category": "Top",
|
||||
"rationale": "ok",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
captured = _patch_capturing_store(monkeypatch)
|
||||
srv = _build_server_with(
|
||||
monkeypatch, tmp_path, fake_db=fake, chat_response=chat_payload
|
||||
)
|
||||
|
||||
args_common = {
|
||||
"role_name": "X",
|
||||
"competences": ["A"],
|
||||
"date_start": "2025-03-01",
|
||||
"date_end": "2025-06-30",
|
||||
}
|
||||
|
||||
# Score-mode run.
|
||||
score_res = await _call_tool(
|
||||
srv,
|
||||
"find_matching_capacities",
|
||||
{**args_common, "matching_method": "score"},
|
||||
)
|
||||
assert "SEARCH_ID=" in score_res
|
||||
score_payload = captured[-1]
|
||||
assert score_payload["matching_method"] == "score"
|
||||
|
||||
score_ids = {
|
||||
str(item.get("id"))
|
||||
for cat in score_payload["by_category"].values()
|
||||
for item in cat
|
||||
}
|
||||
|
||||
# LLM-mode run.
|
||||
llm_res = await _call_tool(
|
||||
srv,
|
||||
"find_matching_capacities",
|
||||
{**args_common, "matching_method": "llm_fulltext"},
|
||||
)
|
||||
assert "SEARCH_ID=" in llm_res
|
||||
llm_payload = captured[-1]
|
||||
assert llm_payload["matching_method"] == "llm_fulltext"
|
||||
|
||||
llm_ids = {
|
||||
str(item.get("id"))
|
||||
for cat in llm_payload["by_category"].values()
|
||||
for item in cat
|
||||
}
|
||||
|
||||
assert score_ids == {"1"}, f"score-mode ids: {score_ids!r}"
|
||||
assert llm_ids == {"1"}, f"llm-mode ids: {llm_ids!r}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_find_matching_tasks_llm_mode_persists_errors_and_rationale(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path
|
||||
) -> None:
|
||||
"""``find_matching_tasks`` persists errors list and full rationale.
|
||||
|
||||
The mock alternates between a successful payload (long rationale,
|
||||
Good) and ``AzureAPIError`` so exactly one task is categorised and
|
||||
the other ends up in ``errors``.
|
||||
|
||||
Validates: requirements 6.1, 6.2, 7.4, 8.6, 9.5.
|
||||
"""
|
||||
long_rationale = "b" * 360
|
||||
capacity = Capacity(
|
||||
id=1,
|
||||
owner_name="Alice",
|
||||
role_name="Frontend Developer",
|
||||
role_level=None,
|
||||
begin_date=date(2025, 4, 1),
|
||||
end_date=date(2025, 8, 31),
|
||||
competences=["React", "TypeScript"],
|
||||
)
|
||||
tasks = [
|
||||
_Task(
|
||||
id="T1",
|
||||
title="Frontend Developer",
|
||||
description="React and TypeScript",
|
||||
skills=["React"],
|
||||
),
|
||||
_Task(
|
||||
id="T2",
|
||||
title="Backend Engineer",
|
||||
description="Python",
|
||||
skills=["Python"],
|
||||
),
|
||||
]
|
||||
fake = _FakeDB(capacities=[capacity], tasks=tasks)
|
||||
|
||||
state = {"call": 0}
|
||||
|
||||
def _side_effect(_system: str, _user: str) -> Any:
|
||||
n = state["call"]
|
||||
state["call"] += 1
|
||||
if n % 2 == 0:
|
||||
return json.dumps(
|
||||
{"category": "Good", "rationale": long_rationale},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
return AzureAPIError("LLM call failed")
|
||||
|
||||
captured = _patch_capturing_store(monkeypatch)
|
||||
srv = _build_server_with(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
fake_db=fake,
|
||||
chat_side_effect=_side_effect,
|
||||
)
|
||||
|
||||
res = await _call_tool(
|
||||
srv,
|
||||
"find_matching_tasks",
|
||||
{"capacity_id": 1, "matching_method": "llm_fulltext"},
|
||||
)
|
||||
assert "SEARCH_ID=" in res
|
||||
assert captured, "store_search was not called"
|
||||
|
||||
payload = captured[-1]
|
||||
assert payload["matching_method"] == "llm_fulltext"
|
||||
|
||||
good = payload["by_category"].get("Good", [])
|
||||
assert len(good) == 1, (
|
||||
f"expected exactly one Good item, got {len(good)}: {good!r}"
|
||||
)
|
||||
assert good[0]["rationale"] == long_rationale
|
||||
assert len(good[0]["rationale"]) >= 360
|
||||
|
||||
errors = payload["errors"]
|
||||
assert isinstance(errors, list)
|
||||
assert len(errors) == 1, f"expected one error, got {errors!r}"
|
||||
error_ids = {e["item_id"] for e in errors}
|
||||
good_ids = {str(item.get("task_id") or item.get("id")) for item in good}
|
||||
assert error_ids.isdisjoint(good_ids), (
|
||||
"an item must appear in either by_category or errors, never both"
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Task 12.3 — Markdown header snapshot tests
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snapshot_capacity_search_score_mode_header(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path
|
||||
) -> None:
|
||||
"""Score mode keeps the existing 9-column capacity table header.
|
||||
|
||||
Validates: requirement 7.1.
|
||||
"""
|
||||
fake = _FakeDB(capacities=_two_capacities_one_in_window())
|
||||
srv = _build_server_with(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
fake_db=fake,
|
||||
chat_response='{"similarity": 1.0}',
|
||||
)
|
||||
|
||||
res = await _call_tool(
|
||||
srv,
|
||||
"find_matching_capacities",
|
||||
{
|
||||
"role_name": "X",
|
||||
"competences": ["A"],
|
||||
"date_start": "2025-03-01",
|
||||
"date_end": "2025-06-30",
|
||||
"matching_method": "score",
|
||||
},
|
||||
)
|
||||
|
||||
expected_header = (
|
||||
"| ID | Owner | Role | Competences | Availability "
|
||||
"| Role Score | Competence Score | Overall Score | Category |"
|
||||
)
|
||||
assert expected_header in res, res
|
||||
assert "Begründung" not in res
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snapshot_capacity_search_llm_mode_header(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path
|
||||
) -> None:
|
||||
"""LLM mode header ends with ``... | Category | Begründung |``.
|
||||
|
||||
Validates: requirements 8.1, 8.2.
|
||||
"""
|
||||
fake = _FakeDB(capacities=_two_capacities_one_in_window())
|
||||
srv = _build_server_with(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
fake_db=fake,
|
||||
chat_response='{"category":"Top","rationale":"ok"}',
|
||||
)
|
||||
|
||||
res = await _call_tool(
|
||||
srv,
|
||||
"find_matching_capacities",
|
||||
{
|
||||
"role_name": "X",
|
||||
"competences": ["A"],
|
||||
"date_start": "2025-03-01",
|
||||
"date_end": "2025-06-30",
|
||||
"matching_method": "llm_fulltext",
|
||||
},
|
||||
)
|
||||
|
||||
assert "| Category | Begründung |" in res, res
|
||||
assert "| Role Score |" not in res
|
||||
assert "| Competence Score |" not in res
|
||||
assert "| Overall Score |" not in res
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snapshot_task_search_score_mode_header(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path
|
||||
) -> None:
|
||||
"""Score mode for ``find_matching_tasks`` keeps the score columns.
|
||||
|
||||
Validates: requirement 7.1.
|
||||
"""
|
||||
capacity = Capacity(
|
||||
id=1,
|
||||
owner_name="Alice",
|
||||
role_name="Frontend Developer",
|
||||
role_level=None,
|
||||
begin_date=date(2025, 4, 1),
|
||||
end_date=date(2025, 8, 31),
|
||||
competences=["React"],
|
||||
)
|
||||
tasks = [
|
||||
_Task(
|
||||
id="T1",
|
||||
title="Frontend Developer",
|
||||
description="React",
|
||||
skills=["React"],
|
||||
start_date=date(2025, 5, 1),
|
||||
end_date=date(2025, 7, 31),
|
||||
)
|
||||
]
|
||||
fake = _FakeDB(capacities=[capacity], tasks=tasks)
|
||||
srv = _build_server_with(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
fake_db=fake,
|
||||
chat_response='{"similarity": 1.0}',
|
||||
)
|
||||
|
||||
res = await _call_tool(
|
||||
srv,
|
||||
"find_matching_tasks",
|
||||
{"capacity_id": 1, "matching_method": "score"},
|
||||
)
|
||||
|
||||
assert (
|
||||
"| Role Score | Competence Score | Overall Score | Category |"
|
||||
in res
|
||||
), res
|
||||
assert "Begründung" not in res
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snapshot_task_search_llm_mode_header(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path
|
||||
) -> None:
|
||||
"""LLM mode for ``find_matching_tasks`` ends with Begründung header.
|
||||
|
||||
Validates: requirements 8.1, 8.2.
|
||||
"""
|
||||
capacity = Capacity(
|
||||
id=1,
|
||||
owner_name="Alice",
|
||||
role_name="Frontend Developer",
|
||||
role_level=None,
|
||||
begin_date=date(2025, 4, 1),
|
||||
end_date=date(2025, 8, 31),
|
||||
competences=["React"],
|
||||
)
|
||||
tasks = [
|
||||
_Task(
|
||||
id="T1",
|
||||
title="Frontend Developer",
|
||||
description="React",
|
||||
skills=["React"],
|
||||
start_date=date(2025, 5, 1),
|
||||
end_date=date(2025, 7, 31),
|
||||
)
|
||||
]
|
||||
fake = _FakeDB(capacities=[capacity], tasks=tasks)
|
||||
srv = _build_server_with(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
fake_db=fake,
|
||||
chat_response='{"category":"Good","rationale":"ok"}',
|
||||
)
|
||||
|
||||
res = await _call_tool(
|
||||
srv,
|
||||
"find_matching_tasks",
|
||||
{"capacity_id": 1, "matching_method": "llm_fulltext"},
|
||||
)
|
||||
|
||||
assert "| Category | Begründung |" in res, res
|
||||
assert "| Role Score |" not in res
|
||||
assert "| Competence Score |" not in res
|
||||
assert "| Overall Score |" not in res
|
||||
Reference in New Issue
Block a user