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,188 @@
|
||||
# Feature: llm-fulltext-matching, Property 2b:
|
||||
# Referenzen mit leerem Partner-Name behalten projects, ohne Partner-Token
|
||||
"""Property-based tests for reference rendering with empty partner names.
|
||||
|
||||
These tests cover the deterministic serializer
|
||||
``serialize_capacity_profile`` defined in
|
||||
``teamlandkarte_mcp.matching.profiles``. They focus specifically on the
|
||||
``Referenzen:`` block and verify three invariants for any list of
|
||||
:class:`CapacityReferenceEntry` values that mixes empty and non-empty
|
||||
``partner_name`` values:
|
||||
|
||||
(a) The number of rendered reference lines equals the number of input
|
||||
entries with non-whitespace ``projects``.
|
||||
(b) Each line that corresponds to an entry with empty ``partner_name``
|
||||
starts with ``Projekte:`` and does NOT contain a ``Partner:`` token.
|
||||
(c) Each line that corresponds to an entry with non-empty
|
||||
``partner_name`` contains both ``Partner: <name>`` and
|
||||
``Projekte: <projects>``.
|
||||
|
||||
Because the serializer classifies entries via ``if entry.partner_name:``
|
||||
(truthiness), this test mirrors that exact logic and uses
|
||||
``bool(entry.partner_name)`` to decide whether the ``Partner:`` token is
|
||||
expected.
|
||||
|
||||
The strategies deliberately exclude line-break-like characters
|
||||
(everything ``str.splitlines`` treats as a line boundary) so the
|
||||
line-based extraction of the rendered ``Referenzen:`` block remains
|
||||
unambiguous. The property under test is the *content* of the rendered
|
||||
references, not how the serializer would behave on text containing
|
||||
literal newlines.
|
||||
|
||||
**Validates: Requirements 2.8, 3.4, 3.6**
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from teamlandkarte_mcp.matching.profiles import (
|
||||
CapacityProfile,
|
||||
CapacityReferenceEntry,
|
||||
serialize_capacity_profile,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Strategies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Alphabet for partner_name/projects: any unicode codepoint that
|
||||
# ``str.splitlines`` does NOT treat as a line boundary, so the rendered
|
||||
# output remains a stable, single-line-per-entry block. We exclude:
|
||||
#
|
||||
# * ``Cs`` (surrogates) — invalid in well-formed strings.
|
||||
# * ``Cc`` (control characters) — includes ``\n``, ``\r``, ``\v``,
|
||||
# ``\f``, ``\x1c``-``\x1f``, ``\x85`` etc.
|
||||
# * ``Zl`` (line separator, ``\u2028``).
|
||||
# * ``Zp`` (paragraph separator, ``\u2029``).
|
||||
_safe_chars = st.characters(
|
||||
blacklist_categories=("Cs", "Cc", "Zl", "Zp"),
|
||||
)
|
||||
|
||||
# ``partner_name``: either empty (NULL ``partner_id`` or join mismatch,
|
||||
# see requirement 2.8) or a short non-empty string. The serializer uses
|
||||
# truthy-checking, so any non-empty string (including whitespace-only)
|
||||
# counts as "has partner" — we keep this strategy aligned with that
|
||||
# logic and classify entries by ``bool(entry.partner_name)`` in the
|
||||
# assertions below.
|
||||
_partner_name = st.one_of(
|
||||
st.just(""),
|
||||
st.text(alphabet=_safe_chars, min_size=1, max_size=20),
|
||||
)
|
||||
|
||||
# ``projects``: non-whitespace text so the serializer never drops the
|
||||
# entry defensively. This lets us assert the stronger invariant
|
||||
# "input entry count == rendered line count".
|
||||
_projects = st.text(
|
||||
alphabet=_safe_chars, min_size=1, max_size=30
|
||||
).filter(lambda s: s.strip() != "")
|
||||
|
||||
_reference = st.builds(
|
||||
CapacityReferenceEntry,
|
||||
partner_name=_partner_name,
|
||||
projects=_projects,
|
||||
)
|
||||
|
||||
_reference_list = st.lists(_reference, min_size=1, max_size=8)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _extract_reference_lines(serialized: str) -> list[str]:
|
||||
"""Extract the rendered reference lines from a serialized profile.
|
||||
|
||||
The serializer renders the ``Referenzen:`` block as a heading line
|
||||
followed by either a bullet list (one ``- <line>`` per entry) or
|
||||
`` (keine)`` on the same line as the heading. The next field
|
||||
heading is always ``Zertifikate:`` at the start of its own line,
|
||||
which we use as the terminator. The leading ``- `` prefix is
|
||||
stripped from each line so callers can assert directly on the
|
||||
rendered content.
|
||||
"""
|
||||
lines: list[str] = []
|
||||
in_references = False
|
||||
for raw_line in serialized.splitlines():
|
||||
if raw_line.startswith("Zertifikate:"):
|
||||
break
|
||||
if raw_line.startswith("Referenzen:"):
|
||||
in_references = True
|
||||
continue
|
||||
if in_references and raw_line.startswith("- "):
|
||||
lines.append(raw_line[2:])
|
||||
return lines
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@settings(max_examples=100, deadline=None)
|
||||
@given(refs=_reference_list)
|
||||
def test_references_with_empty_partner_name_keep_projects_without_partner_token(
|
||||
refs: list[CapacityReferenceEntry],
|
||||
) -> None:
|
||||
"""Property 2b: empty ``partner_name`` keeps ``projects`` without token.
|
||||
|
||||
For any list of :class:`CapacityReferenceEntry` values with mixed
|
||||
``partner_name``:
|
||||
|
||||
(a) The number of rendered reference lines equals the number of
|
||||
input entries (the strategy guarantees non-whitespace
|
||||
``projects``, so no entry is dropped).
|
||||
(b) Every line for an entry with empty ``partner_name`` starts with
|
||||
``Projekte:`` and contains no ``Partner:`` token.
|
||||
(c) Every line for an entry with non-empty ``partner_name`` contains
|
||||
both ``Partner: <name>`` and ``Projekte: <projects>``.
|
||||
"""
|
||||
profile = CapacityProfile(
|
||||
id="x",
|
||||
owner_name="o",
|
||||
role_name="r",
|
||||
competences=[],
|
||||
description="",
|
||||
references=list(refs),
|
||||
certificates=[],
|
||||
)
|
||||
|
||||
serialized = serialize_capacity_profile(profile)
|
||||
lines = _extract_reference_lines(serialized)
|
||||
|
||||
# (a) One rendered line per input entry, in input order.
|
||||
assert len(lines) == len(refs), (
|
||||
f"Expected {len(refs)} reference lines, got {len(lines)}: "
|
||||
f"{lines!r}"
|
||||
)
|
||||
|
||||
# (b) and (c): per-entry assertions, mirroring the serializer's
|
||||
# ``if entry.partner_name:`` truthiness check.
|
||||
for entry, line in zip(refs, lines):
|
||||
projects_clean = entry.projects.strip()
|
||||
if entry.partner_name:
|
||||
# (c) Non-empty partner name → both tokens present.
|
||||
assert f"Partner: {entry.partner_name}" in line, (
|
||||
f"Expected 'Partner: {entry.partner_name}' in line: "
|
||||
f"{line!r}"
|
||||
)
|
||||
assert f"Projekte: {projects_clean}" in line, (
|
||||
f"Expected 'Projekte: {projects_clean}' in line: "
|
||||
f"{line!r}"
|
||||
)
|
||||
else:
|
||||
# (b) Empty partner name → starts with ``Projekte:`` and no
|
||||
# ``Partner:`` token at all.
|
||||
assert line.startswith("Projekte:"), (
|
||||
f"Expected line to start with 'Projekte:', got: {line!r}"
|
||||
)
|
||||
assert "Partner:" not in line, (
|
||||
f"Did not expect 'Partner:' token in line: {line!r}"
|
||||
)
|
||||
assert line == f"Projekte: {projects_clean}", (
|
||||
f"Expected exact 'Projekte: {projects_clean}', got: "
|
||||
f"{line!r}"
|
||||
)
|
||||
Reference in New Issue
Block a user