# Feature: llm-fulltext-matching, Property 1: Profil-Serialisierung ist deterministisch und feldvollständig """Property-based tests for deterministic profile serialization. These tests cover the deterministic serialization helpers ``serialize_capacity_profile`` and ``serialize_task_profile`` defined in ``teamlandkarte_mcp.matching.profiles``. They verify three core properties that hold for every randomly generated profile: 1. **Determinism**: calling the serializer twice on the same profile yields exactly identical strings. 2. **Field-heading completeness**: the serialized output always contains every field heading required by the spec. 3. **Partner-name visibility**: when a ``CapacityReferenceEntry`` has a non-empty ``partner_name``, that name appears as a substring of the serialized capacity profile. **Validates: Requirements 3.3, 3.4, 3.6, 3.7, 4.3, 4.4** """ from __future__ import annotations from hypothesis import given, settings from hypothesis import strategies as st from teamlandkarte_mcp.matching.profiles import ( CapacityProfile, CapacityReferenceEntry, TaskProfile, serialize_capacity_profile, serialize_task_profile, ) # Small text strategy to keep generation fast and deterministic. _text = st.text(max_size=40) # `partner_name` may be empty (NULL `partner_id` / Join-Mismatch) or any # short non-empty text. _partner_name = st.one_of(st.just(""), st.text(min_size=1, max_size=20)) # The builder strips empty `projects`, but the serializer renders every # entry it receives. Use non-whitespace projects so containment assertions # are unambiguous. _projects = st.text(min_size=1, max_size=40).filter(lambda s: s.strip() != "") _reference = st.builds( CapacityReferenceEntry, partner_name=_partner_name, projects=_projects, ) _capacity_profile = st.builds( CapacityProfile, id=_text, owner_name=_text, role_name=_text, competences=st.lists(_text, max_size=5), description=_text, references=st.lists(_reference, max_size=5), certificates=st.lists( _text.filter(lambda s: s.strip() != ""), max_size=5 ), ) _task_profile = st.builds( TaskProfile, id=_text, title=_text, description=_text, skills=st.lists(_text, max_size=5), ) _CAPACITY_HEADINGS = ( "Rolle:", "Kompetenzen:", "Beschreibung:", "Referenzen:", "Zertifikate:", ) _TASK_HEADINGS = ( "Titel:", "Beschreibung:", "Gesuchte Kompetenzen:", ) @settings(max_examples=100, deadline=None) @given(profile=_capacity_profile) def test_capacity_profile_serialization_is_deterministic_and_complete( profile: CapacityProfile, ) -> None: """Property 1 (Capacity): determinism, heading completeness, partner name. For any :class:`CapacityProfile` (including references with empty or non-empty ``partner_name``) the serializer must: * produce identical strings on repeated calls (determinism), * always include every field heading required by the spec, and * include every non-empty ``partner_name`` from the references list as a substring of the serialized output. """ first = serialize_capacity_profile(profile) second = serialize_capacity_profile(profile) # Determinism: two consecutive calls return the exact same string. assert first == second, "serialize_capacity_profile is not deterministic" # Every field heading must always be present. for heading in _CAPACITY_HEADINGS: assert heading in first, ( f"Heading {heading!r} missing from capacity serialization: {first!r}" ) # Each reference with a non-empty partner_name must appear verbatim. for ref in profile.references: if ref.partner_name: assert ref.partner_name in first, ( f"Partner name {ref.partner_name!r} missing from " f"serialization: {first!r}" ) @settings(max_examples=100, deadline=None) @given(profile=_task_profile) def test_task_profile_serialization_is_deterministic_and_complete( profile: TaskProfile, ) -> None: """Property 1 (Task): determinism and heading completeness. For any :class:`TaskProfile` the serializer must produce identical strings on repeated calls and always include every field heading required by the spec. """ first = serialize_task_profile(profile) second = serialize_task_profile(profile) # Determinism: two consecutive calls return the exact same string. assert first == second, "serialize_task_profile is not deterministic" # Every field heading must always be present. for heading in _TASK_HEADINGS: assert heading in first, ( f"Heading {heading!r} missing from task serialization: {first!r}" )