# Feature: team-profile-matching, Task 5.5: Unit tests for build_team_profile / serialize_team_profile """Example-based unit tests for the team-profile builder and serializer. These tests complement the property-based tests in ``tests/test_team_profile_serialization_pbt.py`` by pinning down concrete example outputs for ``build_team_profile`` and ``serialize_team_profile`` defined in ``teamlandkarte_mcp.matching.profiles``. Covered scenarios (see task 5.5 in the team-profile-matching spec): * Empty strings on every text field plus empty competence/reference lists still produce an output that contains all seven fixed German headings, with empty values rendered after the heading and lists rendered as ``Kompetenzen: (keine)`` / ``Referenzen: (keine)``. * Mixed top / non-top competences are rendered with the ``(Top)`` suffix only on the top entries. * References with a non-empty ``partner_name`` use the ``Partner: – Projekte: `` form (en-dash U+2013). * References with an empty ``partner_name`` use the bare ``Projekte: `` form (no ``Partner:`` token, no placeholder). * ``build_team_profile`` performs a 1:1 mapping from ``Team`` to ``TeamProfile`` without re-sorting, including the ouid, team_id and the competence / reference lists. * Empty competence / reference lists pass through ``build_team_profile`` unchanged. **Validates: Requirements 5.1, 5.3, 5.4, 5.5, 5.6** """ from __future__ import annotations from teamlandkarte_mcp.matching.profiles import ( TeamCompetenceEntry, TeamProfile, TeamReferenceEntry, build_team_profile, serialize_team_profile, ) from teamlandkarte_mcp.models import Team, TeamCompetence, TeamReference # --------------------------------------------------------------------------- # build_team_profile # --------------------------------------------------------------------------- def test_build_team_profile_maps_all_fields_one_to_one() -> None: """``build_team_profile`` copies every ``Team`` field onto the profile. Validates requirement 5.1: every team field (team_id, ouid, team_name, focus_name, about_us, offerings, interests, competences, references) appears on the resulting :class:`TeamProfile` with the exact same value, in the exact same list order. ``team_id`` is converted to ``str`` for the profile ``id`` field. """ team = Team( team_id="T-42", ouid="ou-9", team_name="Alpha Team", focus_name="Backend", about_us="Wir bauen APIs.", offerings="Beratung", interests="Cloud", competences=[ TeamCompetence(name="Python", top_competency=True), TeamCompetence(name="FastAPI", top_competency=False), ], references=[ TeamReference(partner_name="Acme", projects="Project X"), TeamReference(partner_name="", projects="Project Y"), ], ) profile = build_team_profile(team) assert profile.id == "T-42" assert profile.ouid == "ou-9" assert profile.team_name == "Alpha Team" assert profile.focus_name == "Backend" assert profile.about_us == "Wir bauen APIs." assert profile.offerings == "Beratung" assert profile.interests == "Cloud" # Competences are preserved 1:1 and in the original order. assert profile.competences == [ TeamCompetenceEntry(name="Python", top_competency=True), TeamCompetenceEntry(name="FastAPI", top_competency=False), ] # References are preserved 1:1 and in the original order. assert profile.references == [ TeamReferenceEntry(partner_name="Acme", projects="Project X"), TeamReferenceEntry(partner_name="", projects="Project Y"), ] def test_build_team_profile_passes_empty_lists_through() -> None: """Empty ``competences`` and ``references`` lists pass through unchanged. Validates requirement 5.1: an empty list on the input team yields an empty list on the resulting profile, without dropping the profile or inserting placeholder entries. """ team = Team( team_id="T-empty", ouid="ou-empty", team_name="", focus_name="", about_us="", offerings="", interests="", competences=[], references=[], ) profile = build_team_profile(team) assert profile.competences == [] assert profile.references == [] # The string fields stay empty without any placeholder. assert profile.team_name == "" assert profile.focus_name == "" assert profile.about_us == "" assert profile.offerings == "" assert profile.interests == "" def test_build_team_profile_does_not_reorder_competences() -> None: """``build_team_profile`` preserves the input order of competences. The DB layer is the single source of truth for the deterministic competence order ``(top_competency desc, name asc)``; the builder must not re-sort. Validates requirement 5.1 / 13.4. """ team = Team( team_id="T-1", ouid="ou-1", team_name="Team", focus_name="", about_us="", offerings="", interests="", competences=[ # Intentionally not in (top desc, name asc) order so a # re-sort would be visible. TeamCompetence(name="Zeta", top_competency=False), TeamCompetence(name="Alpha", top_competency=True), TeamCompetence(name="Beta", top_competency=False), ], references=[], ) profile = build_team_profile(team) assert [c.name for c in profile.competences] == ["Zeta", "Alpha", "Beta"] assert [c.top_competency for c in profile.competences] == [False, True, False] # --------------------------------------------------------------------------- # serialize_team_profile # --------------------------------------------------------------------------- def test_serialize_team_profile_empty_profile_emits_all_headings() -> None: """An empty profile still emits the seven fixed German headings. Validates requirements 5.4 (fixed heading order) and the ``(keine)`` fallback for empty competence and reference lists. """ profile = TeamProfile( id="", ouid="", team_name="", focus_name="", about_us="", offerings="", interests="", competences=[], references=[], ) output = serialize_team_profile(profile) expected = "\n".join( [ "Teamname: ", "Schwerpunkt: ", "Über uns: ", "Leistungen: ", "Interessen: ", "Kompetenzen: (keine)", "Referenzen: (keine)", ] ) assert output == expected def test_serialize_team_profile_mixed_top_and_non_top_competences() -> None: """Top competences carry the ``(Top)`` suffix; non-top entries do not. Validates requirement 5.5: the marker is rendered as ``" (Top)"`` directly after the competence name, exactly for entries with ``top_competency=True``. """ profile = TeamProfile( id="T-1", ouid="ou-1", team_name="Team Alpha", focus_name="Backend", about_us="", offerings="", interests="", competences=[ TeamCompetenceEntry(name="Python", top_competency=True), TeamCompetenceEntry(name="FastAPI", top_competency=False), TeamCompetenceEntry(name="SQL", top_competency=True), ], references=[], ) output = serialize_team_profile(profile) assert "Kompetenzen:\n- Python (Top)\n- FastAPI\n- SQL (Top)" in output # Non-top entries must not accidentally pick up the marker. assert "FastAPI (Top)" not in output def test_serialize_team_profile_reference_with_partner_name() -> None: """References with a partner render as ``Partner: – Projekte: ``. Validates requirement 5.6: both partner name and projects text are visible, joined with the en-dash U+2013 (``\u2013``). """ profile = TeamProfile( id="T-1", ouid="ou-1", team_name="Team", focus_name="", about_us="", offerings="", interests="", competences=[], references=[ TeamReferenceEntry(partner_name="Acme", projects="Project X"), ], ) output = serialize_team_profile(profile) assert "Referenzen:\n- Partner: Acme \u2013 Projekte: Project X" in output def test_serialize_team_profile_reference_without_partner_name() -> None: """References without a partner render as bare ``Projekte: ``. Validates requirement 5.3: an empty ``partner_name`` suppresses the ``Partner: `` token entirely, and no placeholder is inserted. """ profile = TeamProfile( id="T-1", ouid="ou-1", team_name="Team", focus_name="", about_us="", offerings="", interests="", competences=[], references=[ TeamReferenceEntry(partner_name="", projects="Project Y"), ], ) output = serialize_team_profile(profile) # Locate the references block to make the absence-assertion robust # against unrelated text in other fields. refs_start = output.index("Referenzen:") refs_block = output[refs_start:] assert "- Projekte: Project Y" in refs_block assert "Partner: " not in refs_block # No en-dash filler when there is no partner. assert "\u2013" not in refs_block def test_serialize_team_profile_mixed_references_with_and_without_partner() -> None: """Mixed references render each entry with the appropriate shape. Validates requirements 5.3 and 5.6 jointly: in a single profile, a reference with a partner uses the ``Partner: ... – Projekte: ...`` form, and a reference without a partner uses the bare ``Projekte: ...`` form. Order is preserved 1:1. """ profile = TeamProfile( id="T-1", ouid="ou-1", team_name="Team", focus_name="", about_us="", offerings="", interests="", competences=[], references=[ TeamReferenceEntry(partner_name="Acme", projects="Project X"), TeamReferenceEntry(partner_name="", projects="Project Y"), ], ) output = serialize_team_profile(profile) expected_block = ( "Referenzen:\n" "- Partner: Acme \u2013 Projekte: Project X\n" "- Projekte: Project Y" ) assert expected_block in output def test_serialize_team_profile_full_example_round_trip() -> None: """End-to-end example: build then serialize a populated team. Validates requirements 5.1, 5.3, 5.4, 5.5, 5.6 together: the full serialized output for a representative team matches the expected fixed-format string exactly, including heading order, ``(Top)`` markers, and reference shapes. """ team = Team( team_id="T-42", ouid="ou-9", team_name="Alpha Team", focus_name="Backend", about_us="Wir bauen APIs.", offerings="Beratung", interests="Cloud", competences=[ TeamCompetence(name="Python", top_competency=True), TeamCompetence(name="FastAPI", top_competency=False), ], references=[ TeamReference(partner_name="Acme", projects="Project X"), TeamReference(partner_name="", projects="Project Y"), ], ) output = serialize_team_profile(build_team_profile(team)) expected = "\n".join( [ "Teamname: Alpha Team", "Schwerpunkt: Backend", "Über uns: Wir bauen APIs.", "Leistungen: Beratung", "Interessen: Cloud", "Kompetenzen:", "- Python (Top)", "- FastAPI", "Referenzen:", "- Partner: Acme \u2013 Projekte: Project X", "- Projekte: Project Y", ] ) assert output == expected