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,369 @@
# Feature: team-profile-matching, Property 4: Kompetenz-Batch ist konsistent zur Einzel-Variante
"""Property-based test for the batch/single consistency of the team
competence queries on ``TrinoClient``.
Validates Property 4 from the team-profile-matching design:
*Für jede* Liste von OUIDs ``ouids`` und jede Stub-DB-Antwort gilt:
``batch_get_team_competences(ouids)[ouid]`` ist für jedes ``ouid`` in
``ouids`` gleich ``get_team_competences(ouid)``. Außerdem gilt für jeden
Eintrag der Ergebnislisten:
- ``top_competency`` ist ``False``, wenn die Quellzeile ``NULL`` enthielt
(Anforderung 3.5),
- Einträge ohne auflösbaren Kompetenz-Namen sind nicht enthalten
(Anforderung 3.6),
- die Reihenfolge ist ``(top_competency desc, name asc)`` und damit
deterministisch (Anforderung 3.7),
- für ``ouid``-Werte ohne Kompetenzen ist die Liste leer
(Anforderung 3.4).
The test installs a stub cursor that simulates the SQL ``ORDER BY`` and
``COALESCE`` semantics for both the single (``WHERE tc.ouid = ?``) and
the batch (``WHERE tc.ouid IN (?, ?, ...)``) form. The simulated cursor
holds an in-memory list of competence rows
``(ouid, name, top_competency)`` where ``name`` may be ``None`` or empty
(to exercise the Python-side filter, Requirement 3.6) and
``top_competency`` may be ``None`` (to exercise the COALESCE
normalisation, Requirement 3.5).
**Validates: Requirements 3.1, 3.3, 3.4, 3.5, 3.6, 3.7**
"""
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
# ---------------------------------------------------------------------------
# Stub-DB row container
# ---------------------------------------------------------------------------
class _StubDB:
"""Holds the in-memory competence rows for the fake cursor.
Each row is a ``(ouid, name, top_competency)`` tuple. ``name`` may
be ``None`` (unresolvable competence_id) or ``""`` (empty resolved
name); both must be filtered out by the Trino client. ``top``
may be ``None`` (NULL in the source view) and must be normalised
to ``False``.
"""
def __init__(self, rows: list[tuple[str, Any, Any]]) -> None:
self.rows = rows
def _top_norm(value: Any) -> bool:
"""Mirror ``COALESCE(tc.top_competency, FALSE)`` on the SQL side."""
if value is None:
return False
return bool(value)
def _name_sort_key(name: Any) -> tuple[int, str]:
"""Order keys with ``None`` last, then by string ascending.
Trino orders ``NULL`` last for ``ASC``; we mirror that here so the
simulated cursor produces a deterministic order even for the rows
that the Python-side filter will subsequently drop.
"""
if name is None:
return (1, "")
return (0, str(name))
# ---------------------------------------------------------------------------
# Fake cursor / connection plumbing
# ---------------------------------------------------------------------------
class _FakeCursor:
"""Stub cursor that dispatches by SQL substring match.
For the team-competence SQL the cursor inspects whether the
statement contains ``WHERE tc.ouid = ?`` (single variant) or
``WHERE tc.ouid IN`` (batch variant) and returns rows in the
column shape the production code expects.
Single variant column shape: ``(name, top_competency)``.
Batch variant column shape: ``(ouid, name, top_competency)``.
"""
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
if params is None:
params_tuple: tuple[str, ...] = ()
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_teammeter_team_competences_latest" not in sql
):
self._next_rows = []
return
if "WHERE tc.ouid IN" in sql:
self._next_rows = self._simulate_batch(params_tuple)
return
if "WHERE tc.ouid = ?" in sql:
self._next_rows = self._simulate_single(params_tuple)
return
# Defensive default: the production code only emits the two
# shapes above, so any other shape is unexpected.
self._next_rows = []
def _simulate_single(
self, params: tuple[str, ...]
) -> list[tuple]:
"""Simulate the single-OUID query.
Mirrors the SQL ``ORDER BY CASE WHEN COALESCE(top, FALSE)
THEN 0 ELSE 1 END ASC, c.name ASC`` and emits the two-column
result shape used by ``get_team_competences``.
"""
target = params[0] if params else ""
rows = [r for r in self.stub.rows if r[0] == target]
rows.sort(
key=lambda r: (
0 if _top_norm(r[2]) else 1,
_name_sort_key(r[1]),
)
)
return [(r[1], _top_norm(r[2])) for r in rows]
def _simulate_batch(
self, params: tuple[str, ...]
) -> list[tuple]:
"""Simulate the batch-OUID query.
Mirrors the SQL ``ORDER BY tc.ouid ASC, CASE WHEN COALESCE(top,
FALSE) THEN 0 ELSE 1 END ASC, c.name ASC`` and emits the
three-column result shape used by ``batch_get_team_competences``.
"""
wanted = set(params)
rows = [r for r in self.stub.rows if r[0] in wanted]
rows.sort(
key=lambda r: (
str(r[0]),
0 if _top_norm(r[2]) else 1,
_name_sort_key(r[1]),
)
)
return [(r[0], r[1], _top_norm(r[2])) for r in 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 a single :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
# ---------------------------------------------------------------------------
# A small pool of distinct OUID strings keeps the search space tight and
# makes it likely that several rows share the same ouid (so grouping
# behaviour and ordering inside a group are exercised).
_KNOWN_OUIDS = ("OU1", "OU2", "OU3", "OU4")
_UNKNOWN_OUIDS = ("UNKNOWN1", "UNKNOWN2")
_BLANK_OUIDS = ("", " ")
# ``name`` may be ``None``/``""`` (must be filtered) or any non-empty
# string. We use a small alphabet so duplicates inside an ouid group
# are likely, which exercises the secondary ``name ASC`` sort key.
_name_value = st.one_of(
st.none(),
st.just(""),
st.text(
alphabet=st.characters(whitelist_categories=("Lu", "Ll", "Nd")),
min_size=1,
max_size=4,
),
)
# ``top_competency`` can be ``None`` (NULL → must normalise to False),
# ``True`` or ``False``. Integers (truthy/falsy) are not used because
# the production SQL exposes a Boolean column after ``COALESCE``.
_top_value = st.one_of(st.none(), st.booleans())
@st.composite
def _stub_and_inputs(draw: st.DrawFn) -> tuple[_StubDB, list[str]]:
"""Generate stub competence rows and a list of input OUIDs.
The strategy intentionally mixes:
- rows for OUIDs in :data:`_KNOWN_OUIDS` (so batch/single both find
data),
- input OUIDs that resolve to known/unknown/blank values (so
Requirements 3.4 and the blank-input short-circuit both fire),
- ``None``/``""`` names and ``None`` top values (Requirements 3.5,
3.6).
Duplicate input OUIDs are allowed: the production batch method
deduplicates via the output dict's keys, so the test asserts the
consistency property only over the set of distinct input OUIDs.
"""
rows = draw(
st.lists(
st.tuples(
st.sampled_from(_KNOWN_OUIDS),
_name_value,
_top_value,
),
min_size=0,
max_size=15,
)
)
candidates = list(_KNOWN_OUIDS) + list(_UNKNOWN_OUIDS) + list(_BLANK_OUIDS)
input_ouids = draw(
st.lists(
st.sampled_from(candidates),
min_size=0,
max_size=6,
)
)
return _StubDB(rows=rows), input_ouids
# ---------------------------------------------------------------------------
# Property
# ---------------------------------------------------------------------------
_SETTINGS = settings(
max_examples=100,
deadline=None,
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
@_SETTINGS
@given(payload=_stub_and_inputs())
def test_batch_team_competences_matches_single_variant(
payload: tuple[_StubDB, list[str]],
) -> None:
"""Property 4: Kompetenz-Batch ist konsistent zur Einzel-Variante.
Asserts the four sub-properties listed in the module docstring
against the stub-DB-driven fake cursor.
"""
stub, input_ouids = payload
client = _make_client(stub)
batch = client.batch_get_team_competences(input_ouids)
# ---------------------------------------------------------------
# (1) Batch result has exactly the requested OUIDs as keys
# (Requirement 3.4: empty list for OUIDs without competences).
# ---------------------------------------------------------------
expected_keys = {str(o) for o in input_ouids}
assert set(batch.keys()) == expected_keys, (
"batch_get_team_competences must contain every input ouid; "
f"expected {expected_keys!r}, got {set(batch.keys())!r}"
)
for ouid in input_ouids:
ouid_key = str(ouid)
single = client.get_team_competences(ouid)
batch_entry = batch[ouid_key]
# -----------------------------------------------------------
# (2) Batch-vs-single consistency (Requirements 3.1, 3.3).
# -----------------------------------------------------------
assert batch_entry == single, (
f"batch[{ouid_key!r}] differs from get_team_competences"
f"({ouid!r}): batch={batch_entry!r}, single={single!r}"
)
# -----------------------------------------------------------
# (3) Per-entry invariants (Requirements 3.5, 3.6, 3.7).
# -----------------------------------------------------------
for entry in single:
assert isinstance(entry, dict)
assert set(entry.keys()) == {"name", "top_competency"}
# 3.6: filtered out NULL/empty names.
assert entry["name"], (
"entries with NULL or empty name must be filtered out, "
f"got {entry!r}"
)
# 3.5: NULL top_competency normalised to False.
assert isinstance(entry["top_competency"], bool), (
"top_competency must be a bool after COALESCE, got "
f"{entry['top_competency']!r}"
)
# 3.7: order is (top_competency desc, name asc).
ordering = [
(0 if e["top_competency"] else 1, e["name"]) for e in single
]
assert ordering == sorted(ordering), (
"competence list is not ordered by (top desc, name asc) "
f"for ouid={ouid!r}: {single!r}"
)
# 3.4: blank/unknown OUIDs map to an empty list.
if not str(ouid).strip() or str(ouid) in _UNKNOWN_OUIDS:
assert batch_entry == [], (
"OUID without competences must map to an empty list; "
f"ouid={ouid!r}, got {batch_entry!r}"
)