"""Unit tests for the Novice-mode glossary wrap. Pure-function; no DB / HTTP.""" from __future__ import annotations import pytest from app.services.glossary import wrap_glossary def test_no_op_when_tone_is_not_novice(): """Wrap is gated by tone — INTERMEDIATE and unset both pass through.""" text = "VIX spiked to 22." assert wrap_glossary(text, tone="INTERMEDIATE") == text assert wrap_glossary(text, tone=None) == text assert wrap_glossary(text, tone="") == text def test_no_op_when_html_is_empty(): assert wrap_glossary("", tone="NOVICE") == "" assert wrap_glossary(None, tone="NOVICE") == "" def test_wraps_first_occurrence_only(): """A term that appears twice gets wrapped only on the first hit — repeating tooltips on every word is noisy.""" out = wrap_glossary("VIX is high; VIX matters.", tone="NOVICE") assert out.count('class="glossary"') == 1 assert '>VIX' in out # Second occurrence stays plain. assert "; VIX matters" in out def test_wraps_multiple_distinct_terms(): out = wrap_glossary("VIX rose; the yield curve flattened.", tone="NOVICE") assert 'data-term="VIX"' in out assert 'data-term="yield curve"' in out def test_acronyms_are_case_sensitive(): """VIX matches; 'vix' alone shouldn't (avoid false positives).""" assert 'class="glossary"' in wrap_glossary("VIX up.", tone="NOVICE") assert 'class="glossary"' not in wrap_glossary("vix up.", tone="NOVICE") def test_phrase_terms_match_case_insensitively(): """'yield curve' should match regardless of capitalisation.""" out_lower = wrap_glossary("the yield curve flattened.", tone="NOVICE") out_title = wrap_glossary("The Yield Curve flattened.", tone="NOVICE") assert 'class="glossary"' in out_lower assert 'class="glossary"' in out_title def test_aliases_match(): """'high-yield OAS' aliases through to the canonical HY OAS entry.""" out = wrap_glossary("the credit spread widened today.", tone="NOVICE") assert 'class="glossary"' in out assert 'data-term="HY OAS"' in out def test_word_boundary_prevents_substring_match(): """ERP shouldn't match inside 'WERP', 'HERP', etc.""" out = wrap_glossary("WERPS isn't a term.", tone="NOVICE") assert 'class="glossary"' not in out def test_definition_is_escaped_in_data_attr(): """A definition with quotes/HTML must be HTML-escaped in attributes so it doesn't break the surrounding markup.""" out = wrap_glossary("VIX moved.", tone="NOVICE") # data-def="..." must use " not raw ", & not raw &. assert 'data-def="' in out # The S&P 500 reference in the VIX definition uses an ampersand; it # should be escaped. assert "&P 500" in out assert '"P 500' not in out # raw " inside attr would break def test_skips_content_inside_code_blocks(): """Wrapping inside would mangle source examples; we skip those.""" html = "Outside: VIX is up. Inside: VIX is up." out = wrap_glossary(html, tone="NOVICE") # The first VIX (outside) should be wrapped. assert ' stays plain. assert "Inside: VIX is up." in out def test_skips_content_inside_anchor_tags(): """Wrapping inside would double-up on tooltips and weird the link.""" html = 'VIX explainer and VIX here too.' out = wrap_glossary(html, tone="NOVICE") # Anchor content untouched. assert 'VIX explainer' in out # The non-anchor VIX got wrapped. assert 'Yield Curve" in out