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:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
@@ -0,0 +1,415 @@
# Feature: team-profile-matching, Property 3: Stammdaten-Konsistenz und INNER-JOIN-Filter
"""Property-based test for team master-data consistency and the
INNER JOIN filter implemented in
``TrinoClient._fetch_teams_base`` / ``get_all_teams`` /
``get_team_by_id``.
Validates Property 3 from the team-profile-matching design:
*Für jede* Stub-DB-Antwort (Listen von Teams- und OrganizationalUnits-
Zeilen mit beliebigen NULL/Empty-Verteilungen) gilt:
``get_all_teams()``
1. enthält ausschließlich Teams, deren ``team_id`` in der
OrganizationalUnits-Liste über ``id`` einen Treffer hat
(Anforderung 2.3),
2. liefert für jede der Spalten ``about_us``, ``offerings``,
``interests``, ``focus_name`` einen leeren String, wenn die Quelle
``NULL`` oder leer ist, und niemals ``None`` (Anforderung 2.4),
3. setzt ``Team.team_name`` auf den Namen aus der OU-Zeile, der dem
``team_id``-Match entspricht (Anforderung 2.2),
4. liefert für jede gefundene ``team_id`` ein konsistentes Ergebnis
mit ``get_team_by_id(team_id)`` (Anforderung 2.5).
The test does *not* exercise the real Trino driver. Instead it
installs a stub-DB-driven ``_FakeCursor`` that simulates the SQL
operations the production code expresses (INNER JOIN over the Teams
and OUs row lists, plus ``COALESCE`` for nullable text fields). The
cursor inspects the executed SQL string to dispatch between the
team-master-data query (with or without ``WHERE`` clause) and the
batch-competences / batch-references queries. The latter return
empty rows in this test scenario because Property 3 only constrains
the team master-data path (competences and references are covered
by Properties 4 and 5).
**Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5**
"""
from __future__ import annotations
from typing import Any
from hypothesis import HealthCheck, given, settings
from hypothesis import strategies as st
from teamlandkarte_mcp.database.trino_client import TrinoClient
from teamlandkarte_mcp.models import Team
# ---------------------------------------------------------------------------
# Stub-DB row containers
# ---------------------------------------------------------------------------
class _StubDB:
"""A pair of stub row lists feeding the fake cursor.
``team_rows`` are dicts with keys ``team_id``, ``ouid``,
``focus_name``, ``about_us``, ``offerings``, ``interests`` (the
text columns may be ``None`` or empty strings to exercise the
``COALESCE`` paths).
``ou_rows`` are dicts with keys ``id`` and ``name`` modelling
rows of ``teamlandkarte_v_teammeter_organizational_units_latest``.
"""
def __init__(
self,
team_rows: list[dict[str, Any]],
ou_rows: list[dict[str, Any]],
) -> None:
self.team_rows = team_rows
self.ou_rows = ou_rows
def _coalesce_empty(value: Any) -> str:
"""Simulate SQL ``COALESCE(<col>, '')`` for nullable text columns."""
return "" if value is None else str(value)
def _simulate_teams_join(
stub: _StubDB,
*,
where_params: tuple[str, ...] | None,
) -> list[tuple]:
"""Simulate the INNER JOIN of ``_fetch_teams_base``.
Mirrors the production SQL:
- INNER JOIN ``teams_latest`` × ``organizational_units_latest`` on
``CAST(team_id AS VARCHAR) = CAST(id AS VARCHAR)`` (teams
without a matching OU are dropped).
- ``COALESCE(<text>, '')`` for ``focus_name``, ``about_us``,
``offerings``, ``interests`` (empty string for ``NULL``).
- Optional ``WHERE CAST(t.team_id AS VARCHAR) = ? OR t.ouid = ?``
filter when
``where_params`` is non-``None``.
- ``ORDER BY ou.name ASC, t.team_id ASC``.
Returns rows in the shape ``_fetch_teams_base`` consumes:
``(team_id, ouid, team_name, focus_name, about_us, offerings,
interests)``.
"""
ou_by_id = {str(ou["id"]): ou for ou in stub.ou_rows}
rows: list[tuple] = []
for team in stub.team_rows:
ou = ou_by_id.get(str(team["team_id"]))
if ou is None:
# INNER JOIN excludes teams without a matching OU.
continue
if where_params is not None:
tid_param, ouid_param = where_params
if (
str(team["team_id"]) != tid_param
and team["ouid"] != ouid_param
):
continue
rows.append(
(
team["team_id"],
team["ouid"],
ou["name"],
_coalesce_empty(team.get("focus_name")),
_coalesce_empty(team.get("about_us")),
_coalesce_empty(team.get("offerings")),
_coalesce_empty(team.get("interests")),
)
)
rows.sort(key=lambda r: (r[2], r[0]))
return rows
# ---------------------------------------------------------------------------
# Fake cursor / connection plumbing
# ---------------------------------------------------------------------------
class _FakeCursor:
"""Stub cursor that dispatches by SQL substring match.
For the team-master-data query (matched via the
``teamlandkarte_v_teams_latest`` table reference) the cursor
simulates the INNER JOIN and ``COALESCE`` semantics via
:func:`_simulate_teams_join`. For the batch-competences and
batch-references queries it returns an empty result set: those
code paths are exercised by Properties 4 and 5 and only need to
not break here.
"""
def __init__(self, stub: _StubDB) -> None:
self.stub = stub
self._next_rows: list[tuple] = []
def execute(self, sql: str, params: Any = None) -> None: # noqa: ANN401
params_tuple: tuple[str, ...]
if params is None:
params_tuple = ()
elif isinstance(params, tuple):
params_tuple = tuple(str(p) for p in params)
else:
params_tuple = tuple(str(p) for p in params)
if "teamlandkarte_v_teams_latest" in sql:
where_params: tuple[str, ...] | None
if "CAST(t.team_id AS VARCHAR) = ? OR t.ouid = ?" in sql:
# ``get_team_by_id`` passes the same id as both params.
if len(params_tuple) >= 2:
where_params = (params_tuple[0], params_tuple[1])
else:
where_params = ("", "")
else:
where_params = None
self._next_rows = _simulate_teams_join(
self.stub, where_params=where_params
)
return
if (
"teamlandkarte_v_teammeter_team_competences_latest" in sql
or "teamlandkarte_v_team_references_latest" in sql
):
# No competences / references rows in this scenario.
self._next_rows = []
return
# Defensive default: no rows for unrecognised statements.
self._next_rows = []
def fetchone(self) -> Any: # noqa: ANN401
return self._next_rows[0] if self._next_rows else None
def fetchall(self) -> list[tuple]:
return list(self._next_rows)
class _FakeCursorContext:
"""Context-manager wrapper around :class:`_FakeCursor`."""
def __init__(self, cursor: _FakeCursor) -> None:
self._cursor = cursor
def __enter__(self) -> _FakeCursor:
return self._cursor
def __exit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
return None
class _FakeConfig:
"""Minimal ``DatabaseConfig`` stand-in for ``TrinoClient(...)``."""
host = "x"
port = 1
username = "u"
password = "p"
http_scheme = "http"
verify_ssl = False
catalog = "c"
schema = "s"
pool_size = 1
def _make_client(stub: _StubDB) -> TrinoClient:
"""Build a ``TrinoClient`` whose ``_cursor`` yields a fake cursor."""
cursor = _FakeCursor(stub)
client = TrinoClient(_FakeConfig()) # type: ignore[arg-type]
client._cursor = lambda: _FakeCursorContext(cursor) # type: ignore[method-assign]
return client
# ---------------------------------------------------------------------------
# Hypothesis strategies
# ---------------------------------------------------------------------------
# Text-field generator: ``None`` (NULL), ``""`` (empty), or a non-empty
# printable string. Mirrors realistic data-lake content for the four
# nullable text columns.
_text_or_null = st.one_of(
st.none(),
st.just(""),
st.text(min_size=1, max_size=20),
)
@st.composite
def _stub_db(draw: st.DrawFn) -> _StubDB:
"""Build a stub DB with intentionally varied INNER JOIN coverage.
The strategy generates a small pool of OU rows (with non-empty,
non-conflicting names so the SQL ``ORDER BY`` is well-defined)
and a list of team rows whose ``team_id`` either references an
existing OU id or a deliberately non-matching id (to exercise
the INNER JOIN filter, Anforderung 2.3).
Team ``team_id`` values are kept unique within a single stub DB
so ``get_team_by_id(team_id)`` is unambiguous (Anforderung 2.5).
Team ``ouid`` values are likewise unique. Text columns are drawn
from :data:`_text_or_null` to cover both ``NULL`` and ``""``
branches (Anforderung 2.4).
"""
# 1) Generate OU rows with unique ids and unique non-empty names.
n_ous = draw(st.integers(min_value=0, max_value=4))
ou_names = draw(
st.lists(
st.text(
alphabet=st.characters(
whitelist_categories=("Lu", "Ll", "Nd")
),
min_size=1,
max_size=8,
),
min_size=n_ous,
max_size=n_ous,
unique=True,
)
)
ou_rows = [
{"id": f"OU{i}", "name": name}
for i, name in enumerate(ou_names)
]
# 2) Generate team rows. Each team independently picks an existing
# OU id (matching team_id) or a deliberately missing id.
n_teams = draw(st.integers(min_value=0, max_value=6))
teams: list[dict[str, Any]] = []
used_team_ids: set[str] = set()
for i in range(n_teams):
prefer_match = draw(st.booleans()) and ou_rows
if prefer_match:
candidate = draw(
st.sampled_from([ou["id"] for ou in ou_rows])
)
else:
candidate = f"MISS{i}"
if candidate in used_team_ids:
# Skip duplicates so ``get_team_by_id`` is unambiguous.
continue
used_team_ids.add(candidate)
teams.append(
{
"team_id": candidate,
"ouid": f"TEAM_OU_{i}",
"focus_name": draw(_text_or_null),
"about_us": draw(_text_or_null),
"offerings": draw(_text_or_null),
"interests": draw(_text_or_null),
}
)
return _StubDB(team_rows=teams, ou_rows=ou_rows)
# ---------------------------------------------------------------------------
# Property
# ---------------------------------------------------------------------------
_SETTINGS = settings(
max_examples=100,
deadline=None,
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
@_SETTINGS
@given(stub=_stub_db())
def test_team_master_data_consistency_and_inner_join(
stub: _StubDB,
) -> None:
"""Property 3: Stammdaten-Konsistenz und INNER-JOIN-Filter.
Asserts the four sub-properties listed in the module docstring
against the stub-DB-driven fake cursor.
"""
client = _make_client(stub)
teams = client.get_all_teams()
# ---------------------------------------------------------------
# (1) INNER JOIN filter: only teams with a matching OU id
# (Anforderung 2.3).
# ---------------------------------------------------------------
ou_by_id = {ou["id"]: ou for ou in stub.ou_rows}
expected_team_ids = {
t["team_id"] for t in stub.team_rows if t["team_id"] in ou_by_id
}
actual_team_ids = {t.team_id for t in teams}
assert actual_team_ids == expected_team_ids, (
"INNER JOIN filter mismatch: expected "
f"{expected_team_ids!r}, got {actual_team_ids!r}"
)
# ---------------------------------------------------------------
# (2) NULL/empty → ""; never ``None`` (Anforderung 2.4).
# ---------------------------------------------------------------
for team in teams:
assert isinstance(team, Team)
for field_name in (
"focus_name",
"about_us",
"offerings",
"interests",
):
value = getattr(team, field_name)
assert isinstance(value, str), (
f"{field_name} must be str, got "
f"{type(value).__name__}: {value!r}"
)
# ---------------------------------------------------------------
# (3) team_name must come from the matched OU row
# (Anforderung 2.2).
# ---------------------------------------------------------------
for team in teams:
ou = ou_by_id[team.team_id]
assert team.team_name == ou["name"], (
f"team_name mismatch for {team.team_id!r}: "
f"expected {ou['name']!r}, got {team.team_name!r}"
)
# ---------------------------------------------------------------
# (4) ``get_team_by_id`` is consistent with ``get_all_teams``
# (Anforderung 2.5).
# ---------------------------------------------------------------
by_team_id = {t.team_id: t for t in teams}
for team_id, expected_team in by_team_id.items():
single = client.get_team_by_id(team_id)
assert single is not None, (
f"get_team_by_id({team_id!r}) returned None despite the "
f"team appearing in get_all_teams()"
)
assert single.team_id == expected_team.team_id
assert single.ouid == expected_team.ouid
assert single.team_name == expected_team.team_name
assert single.focus_name == expected_team.focus_name
assert single.about_us == expected_team.about_us
assert single.offerings == expected_team.offerings
assert single.interests == expected_team.interests
# And: a team_id that the INNER JOIN excluded must not be
# retrievable via ``get_team_by_id`` either.
excluded = [
t["team_id"]
for t in stub.team_rows
if t["team_id"] not in ou_by_id
]
for missing_id in excluded:
assert client.get_team_by_id(missing_id) is None, (
f"get_team_by_id({missing_id!r}) returned a team that "
f"was excluded by the INNER JOIN"
)