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.
251 lines
9.3 KiB
Python
251 lines
9.3 KiB
Python
# Feature: team-profile-matching, Property 14: Schema-Verifikation deckt fehlende Spalten in den vier neuen Views auf
|
|
"""Property-based test for schema verification of the four new team views.
|
|
|
|
Validates Property 14 from the team-profile-matching design:
|
|
|
|
*Für jede* Mock-DB-Antwort, die in einer der vier Views
|
|
(``teamlandkarte_v_teams_latest``,
|
|
``teamlandkarte_v_teammeter_organizational_units_latest``,
|
|
``teamlandkarte_v_teammeter_team_competences_latest``,
|
|
``teamlandkarte_v_team_references_latest``) eine erwartete Spalte
|
|
weglässt, liefert ``verify_required_columns(...)`` für die in
|
|
``mcp_server.build_server`` konfigurierte ``schema_expected``-Erweiterung
|
|
einen ``SchemaIssue`` mit dem betroffenen Tabellennamen und enthält die
|
|
fehlende Spalte in ``message``. Wenn alle erwarteten Spalten vorhanden
|
|
sind, ist das Issue-Set für diese vier Views leer.
|
|
|
|
The Hypothesis strategy generates, for a randomly chosen view, a
|
|
non-empty subset of its expected columns to drop. The stub DB then
|
|
returns ``full_set - dropped`` for that view (and the full sets for the
|
|
other three views). The test asserts that ``verify_required_columns``:
|
|
|
|
1. Returns at least one ``SchemaIssue`` whose ``table`` equals the
|
|
chosen view.
|
|
2. The issue's ``message`` mentions every dropped column (lowercased,
|
|
per the verifier's normalization).
|
|
3. No issues are reported for the other three views (which received
|
|
their full column sets).
|
|
|
|
The positive case is covered by an additional non-property test that
|
|
feeds the full column sets and expects an empty issue list.
|
|
|
|
**Validates: Requirements 12.1, 12.2, 12.3, 12.4**
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Iterable
|
|
|
|
from hypothesis import given, settings
|
|
from hypothesis import strategies as st
|
|
|
|
from teamlandkarte_mcp.database.schema_verifier import (
|
|
SchemaIssue,
|
|
verify_required_columns,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Source of truth: the four new views and their expected columns.
|
|
# Mirrors the ``schema_expected`` extension configured in
|
|
# ``mcp_server.build_server`` (see design.md, "Schema-Verifikation beim
|
|
# Start"). Keeping this map local keeps the test independent of
|
|
# ``build_server`` runtime wiring (which requires a full config + DB).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_EXPECTED_COLUMNS: dict[str, frozenset[str]] = {
|
|
"teamlandkarte_v_teams_latest": frozenset(
|
|
{"team_id", "ouid", "about_us", "offerings", "interests", "focus_name"}
|
|
),
|
|
"teamlandkarte_v_teammeter_organizational_units_latest": frozenset(
|
|
{"id", "name"}
|
|
),
|
|
"teamlandkarte_v_teammeter_team_competences_latest": frozenset(
|
|
{"ouid", "competence_id", "top_competency"}
|
|
),
|
|
"teamlandkarte_v_team_references_latest": frozenset(
|
|
{"ouid", "partner_id", "projects"}
|
|
),
|
|
}
|
|
|
|
_VIEW_NAMES: tuple[str, ...] = tuple(_EXPECTED_COLUMNS.keys())
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test double
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _StubDB:
|
|
"""Stub DB exposing ``get_table_columns`` for a fixed mapping.
|
|
|
|
The verifier protocol only requires this single method (see
|
|
``schema_verifier.SchemaIntrospector``), so this stub is sufficient
|
|
to exercise the full code path without touching a real database.
|
|
"""
|
|
|
|
def __init__(self, mapping: dict[str, list[str]]):
|
|
self._mapping = mapping
|
|
|
|
def get_table_columns(self, table: str) -> list[str]:
|
|
# ``verify_required_columns`` treats unknown tables as "no columns".
|
|
return list(self._mapping.get(table, []))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _full_columns_for_all_views() -> dict[str, list[str]]:
|
|
"""Return the full column lists for every configured view."""
|
|
|
|
return {view: sorted(cols) for view, cols in _EXPECTED_COLUMNS.items()}
|
|
|
|
|
|
def _expected_map() -> dict[str, set[str]]:
|
|
"""Return a fresh ``expected`` map matching the verifier's signature.
|
|
|
|
The verifier expects ``dict[str, set[str]]``; the canonical map uses
|
|
``frozenset`` for immutability, so we materialize plain ``set``s here.
|
|
"""
|
|
|
|
return {view: set(cols) for view, cols in _EXPECTED_COLUMNS.items()}
|
|
|
|
|
|
def _issues_for_table(
|
|
issues: Iterable[SchemaIssue], table: str
|
|
) -> list[SchemaIssue]:
|
|
return [issue for issue in issues if issue.table == table]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Hypothesis strategies
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _drop_subset_strategy() -> st.SearchStrategy[tuple[str, frozenset[str]]]:
|
|
"""Strategy for ``(view, columns_to_drop)`` pairs.
|
|
|
|
Picks a view from the four configured views and a non-empty subset
|
|
of its expected columns to "drop" (i.e. omit from the stub DB's
|
|
response for that view). The subset is non-empty so the property's
|
|
precondition ("at least one expected column is omitted") always
|
|
holds.
|
|
"""
|
|
|
|
def _for_view(view: str) -> st.SearchStrategy[tuple[str, frozenset[str]]]:
|
|
cols = sorted(_EXPECTED_COLUMNS[view])
|
|
# Subset of size in [1, len(cols)]: covers the full powerset
|
|
# minus the empty set, which corresponds to the design's
|
|
# "Powerset ... minus a randomly chosen column".
|
|
return st.lists(
|
|
st.sampled_from(cols),
|
|
min_size=1,
|
|
max_size=len(cols),
|
|
unique=True,
|
|
).map(lambda picks: (view, frozenset(picks)))
|
|
|
|
return st.sampled_from(_VIEW_NAMES).flatmap(_for_view)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Property: dropping any subset of columns yields an issue per dropped column
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@settings(max_examples=100, deadline=None)
|
|
@given(case=_drop_subset_strategy())
|
|
def test_missing_columns_in_any_of_the_four_views_produces_schema_issue(
|
|
case: tuple[str, frozenset[str]],
|
|
) -> None:
|
|
"""Property 14 - negative direction.
|
|
|
|
For every (view, non-empty drop-subset), the verifier emits a
|
|
``SchemaIssue`` whose ``table`` is the view and whose ``message``
|
|
mentions every dropped column. Other views (with full column sets)
|
|
must not produce any issues.
|
|
"""
|
|
|
|
view, dropped = case
|
|
full_cols = _EXPECTED_COLUMNS[view]
|
|
remaining = sorted(full_cols - dropped)
|
|
|
|
db_mapping = _full_columns_for_all_views()
|
|
db_mapping[view] = remaining
|
|
db = _StubDB(db_mapping)
|
|
|
|
issues = verify_required_columns(db=db, expected=_expected_map())
|
|
|
|
# 1) The affected view is reported exactly once.
|
|
affected = _issues_for_table(issues, view)
|
|
assert len(affected) == 1, (
|
|
f"Expected exactly one SchemaIssue for {view!r}, got {affected!r}"
|
|
)
|
|
|
|
# 2) Each dropped column (normalized to lowercase, like the verifier
|
|
# does internally) appears in the issue message.
|
|
message = affected[0].message
|
|
assert message.startswith("missing columns: "), message
|
|
for col in dropped:
|
|
assert col.lower() in message, (
|
|
f"Dropped column {col!r} not mentioned in issue message {message!r}"
|
|
)
|
|
|
|
# 3) The other three views must not produce any issues, since they
|
|
# were given their full expected column sets.
|
|
for other in _VIEW_NAMES:
|
|
if other == view:
|
|
continue
|
|
assert _issues_for_table(issues, other) == [], (
|
|
f"Unexpected issue for unaffected view {other!r}: "
|
|
f"{_issues_for_table(issues, other)!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Positive case: full column sets -> no issues for the four views
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_full_column_sets_for_all_four_views_yield_no_issues() -> None:
|
|
"""Property 14 - positive direction.
|
|
|
|
When every view exposes its full expected column set, the verifier
|
|
must return zero issues for the four configured team views.
|
|
"""
|
|
|
|
db = _StubDB(_full_columns_for_all_views())
|
|
issues = verify_required_columns(db=db, expected=_expected_map())
|
|
issues_for_team_views = [
|
|
issue for issue in issues if issue.table in _EXPECTED_COLUMNS
|
|
]
|
|
assert issues_for_team_views == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sanity check: the configured map in ``build_server`` matches the
|
|
# canonical map used by this test. This is not part of Property 14 but
|
|
# guards against silent drift between the schema verifier wiring and
|
|
# the test's source of truth.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_canonical_map_matches_mcp_server_configuration() -> None:
|
|
"""The four-view subset of ``schema_expected`` matches this test."""
|
|
|
|
import inspect
|
|
|
|
from teamlandkarte_mcp import mcp_server
|
|
|
|
source = inspect.getsource(mcp_server.build_server)
|
|
for view, cols in _EXPECTED_COLUMNS.items():
|
|
assert view in source, (
|
|
f"View {view!r} missing from mcp_server.build_server"
|
|
)
|
|
for col in cols:
|
|
assert f'"{col}"' in source, (
|
|
f"Column {col!r} for view {view!r} missing "
|
|
f"from mcp_server.build_server"
|
|
)
|