"""Unit tests: SVG text extraction (phase 181, task 01). :func:`app.rag.svg_text.extract_svg_text` is a PURE function — the text that makes an SVG document retrievable WITHOUT a vision call (LOCKED A1). No DB, no filesystem, no LLM: every case is a literal SVG string, so the whole contract is pinned here before the importer branch (task 02) relies on it. The matrix: * the guards that run BEFORE any parse — empty / whitespace-only input, the :data:`~app.rag.svg_text.MAX_SVG_BYTES` cap, the ````/````/```` in DOCUMENT order, ```` children joined with a space, namespace-prefixed and namespaced tags treated alike, ``xlink:`` attributes ignored, whitespace normalised, repeats dropped case-insensitively keeping the first, ``&``/``—`` decoded by the parser; * the :data:`~app.rag.svg_text.MAX_SVG_TEXT_CHARS` cap — cut at the last newline/period before the cap, hard-cut only when the window holds no boundary at all; * the LOCKED A2 structural fallback — stem, shape census (counts, plural, top-3 selection with a document-order tie-break) and the ``viewBox`` clause, each omitted when the file does not state it. Importer-level tests (the vector branch, the no-vision-call seam) live in the phase's ingest module (tasks 02/05). """ from __future__ import annotations from app.rag.svg_text import ( MAX_SVG_BYTES, MAX_SVG_TEXT_CHARS, SvgText, extract_svg_text, local_name, ) SVG_NS = "http://www.w3.org/2000/svg" XLINK_NS = "http://www.w3.org/1999/xlink" # --- constants ------------------------------------------------------------- def test_module_constants_are_pinned() -> None: """The caps are part of the contract the importer's tests assume.""" assert MAX_SVG_BYTES == 2 * 1024 * 1024 assert MAX_SVG_TEXT_CHARS == 4000 # --- the local_name helper ------------------------------------------------- def test_local_name_strips_the_clark_notation_namespace() -> None: assert local_name(f"{{{SVG_NS}}}title") == "title" assert local_name("title") == "title" def test_local_name_of_a_non_string_tag_is_empty() -> None: """Comment / PI markers are callables — they carry no name.""" assert local_name(object()) == "" # --- guards (no parse attempted) ------------------------------------------- def test_empty_input_is_a_refusal_not_a_crash() -> None: assert extract_svg_text(b"") is None assert extract_svg_text(b"", stem="diagram") is None def test_whitespace_only_input_is_a_refusal() -> None: assert extract_svg_text(b" \n\t \r\n") is None def test_bytes_over_the_size_cap_are_refused() -> None: """The memory guard: an over-cap file never reaches the parser.""" padded = b"" + b"x" * (MAX_SVG_BYTES + 1) + b"" assert len(padded) > MAX_SVG_BYTES assert extract_svg_text(padded, stem="huge") is None def test_bytes_exactly_at_the_size_cap_are_parsed() -> None: """The cap is ``>``-based: exactly ``MAX_SVG_BYTES`` is in range.""" opener, closer = b"", b"" filler = b"x" * (MAX_SVG_BYTES - len(opener) - len(closer)) result = extract_svg_text(opener + filler + closer) assert result is not None assert result.structural is False def test_doctype_with_an_internal_entity_is_refused() -> None: """Stdlib ElementTree happily EXPANDS an internal entity — proving why the doctype-bearing input is refused outright (billion-laughs class): the reference must never be resolved.""" svg = b']>&x;' assert extract_svg_text(svg, stem="bomb") is None def test_bare_entity_declaration_is_refused() -> None: assert extract_svg_text(b'x') is None def test_entity_declaration_hidden_inside_content_is_refused() -> None: """The marker scan is over the raw bytes — a declaration anywhere (even in a position the parser would reject) refuses the file.""" assert extract_svg_text(b"\n\n") is None def test_guards_are_case_insensitive() -> None: assert extract_svg_text(b'') is None assert extract_svg_text(b'') is None def test_malformed_xml_is_a_refusal_not_an_exception() -> None: """The caller's fail-soft is a skip (``images_failed``), so a parse error must never escape.""" assert extract_svg_text(b"unclosed") is None assert extract_svg_text(b"") is None assert extract_svg_text(b"\x00\x01\x02") is None assert extract_svg_text(b"not xml at all") is None # --- extraction: title / desc / text --------------------------------------- def test_title_desc_and_text_are_collected_in_document_order() -> None: svg = ( b"" b"Top Title" b"What the diagram shows" b"first label" b"second label" b"" ) assert extract_svg_text(svg, stem="topo") == SvgText( text="Top Title\nWhat the diagram shows\nfirst label\nsecond label", structural=False, ) def test_a_title_after_its_shape_still_follows_document_order() -> None: svg = b"latergroup titled" assert extract_svg_text(svg) == SvgText( text="later\ngroup title\nd", structural=False ) def test_title_and_desc_concatenate_nested_markup() -> None: svg = b"Deploy <tspan>flow</tspan> map" full = b"" + svg + b"" assert extract_svg_text(full) == SvgText(text="Deploy flow map", structural=False) def test_text_children_are_joined_with_a_space() -> None: """``CPUusage`` reads "CPU usage" — without the space the labels weld into one unsearchable token.""" svg = b'CPUusage7d' assert extract_svg_text(svg) == SvgText(text="CPU usage 7d", structural=False) def test_whitespace_is_normalised_in_every_item() -> None: svg = b" rack\n\t layout map a\n b " assert extract_svg_text(svg) == SvgText( text="rack layout map\na b", structural=False ) def test_blank_title_is_dropped_and_never_stops_the_walk() -> None: svg = b" real label" assert extract_svg_text(svg) == SvgText(text="real label", structural=False) def test_only_blank_text_falls_back_to_the_structural_line() -> None: """A blank ````/``<text>`` contributes no words, so the file is still described by what it is (the empty ``<text>`` counts as a drawn element).""" svg = b'<svg viewBox="0 0 4 4"><title> ' assert extract_svg_text(svg, stem="blank") == SvgText( text='Vector diagram "blank" — 1 shape (text), viewBox 4×4', structural=True ) def test_repeats_are_dropped_case_insensitively_keeping_the_first() -> None: svg = ( b"" b"Power Draw" b"power draw" b"Other" b"" ) assert extract_svg_text(svg) == SvgText(text="Power Draw\nOther", structural=False) def test_parser_decodes_entity_references_in_the_text() -> None: """``&`` comes out decoded — the renderer re-escapes on display (phase 180), so storing the decoded value is the correct direction.""" svg = b"Red & Blue — map" assert extract_svg_text(svg) == SvgText(text="Red & Blue — map", structural=False) def test_attributes_are_never_text() -> None: svg = ( b'Visible' b'' ) assert extract_svg_text(svg, stem="diagram") == SvgText( text="Visible", structural=False ) def test_an_image_only_svg_yields_no_words_from_its_href() -> None: """ASSUMPTION (task 01): an embedded ```` is not followed — its ``href`` is not text, so the structural line describes the file.""" svg = ( b'' b'' b"" ) result = extract_svg_text(svg, stem="poster") assert result is not None assert result.structural is True assert result.text == 'Vector diagram "poster" — 1 shape (image), viewBox 10×10' # --- extraction: namespaces ------------------------------------------------ def test_the_default_svg_namespace_is_ignored() -> None: svg = ( b'Namespace titlens desc' b'ns label' ) assert extract_svg_text(svg) == SvgText( text="Namespace title\nns desc\nns label", structural=False ) def test_a_declared_prefix_form_counts_like_the_plain_tag() -> None: """``ElementTree`` expands ```` to Clark notation, so the local-name match covers it for free.""" svg = ( b'Prefixed' b"Prefixed desc" ) assert extract_svg_text(svg) == SvgText( text="Prefixed\nPrefixed desc", structural=False ) def test_a_foreign_namespace_element_is_not_worded_text() -> None: """A ```` in a NON-SVG vocabulary is still matched by local name (the walk is namespace-agnostic) while the foreign element counts as a shape when it carries no words.""" svg = ( b'<svg xmlns:h="http://www.w3.org/1999/xhtml">' b'<foreignObject><h:p>paragraph</h:p></foreignObject>' b"</svg>" ) result = extract_svg_text(svg, stem="mixed") assert result is not None assert result.structural is True assert result.text == 'Vector diagram "mixed" — 2 shapes (foreignObject, p)' def test_the_xml_declaration_encoding_is_honoured() -> None: svg = b"<?xml version='1.0' encoding='UTF-8'?><svg><title>Caf\xc3\xa9" assert extract_svg_text(svg) == SvgText(text="Café", structural=False) def test_comments_are_not_text_and_not_shapes() -> None: svg = b'' result = extract_svg_text(svg, stem="clean") assert result is not None assert result.text == 'Vector diagram "clean" — 1 shape (path), viewBox 2×2' # --- the MAX_SVG_TEXT_CHARS cap -------------------------------------------- def test_extraction_under_the_cap_is_untouched() -> None: svg = b"" + b"x" * (MAX_SVG_TEXT_CHARS - 20) + b"" result = extract_svg_text(svg) assert result is not None assert len(result.text) == MAX_SVG_TEXT_CHARS - 20 def test_over_cap_text_ends_on_a_sentence_boundary() -> None: svg = b"" + b"x. " * 2000 + b"" result = extract_svg_text(svg) assert result is not None assert result.structural is False assert len(result.text) <= MAX_SVG_TEXT_CHARS assert len(result.text) > MAX_SVG_TEXT_CHARS - 10 assert result.text.endswith(".") assert result.text.count("x.") < 2000 # the cut is mid-item def test_over_cap_text_prefers_the_last_newline_boundary() -> None: """Two items joined with ``\\n``: the newline is the LAST boundary inside the window, so the second item is dropped whole (never a partial word) and the trailing newline is stripped.""" first = b"a" * (MAX_SVG_TEXT_CHARS - 10) svg = b"" + first + b"" + b"b" * 200 + b"" result = extract_svg_text(svg) assert result is not None assert result.text == first.decode() def test_over_cap_text_without_any_boundary_is_hard_cut() -> None: svg = b"" + b"x" * (MAX_SVG_TEXT_CHARS + 1000) + b"" result = extract_svg_text(svg) assert result is not None assert result.text == "x" * MAX_SVG_TEXT_CHARS # --- the structural fallback (LOCKED A2) ----------------------------------- def test_single_shape_line_matches_the_pinned_wording() -> None: svg = b'' assert extract_svg_text(svg, stem="topo") == SvgText( text='Vector diagram "topo" — 1 shape (path)', structural=True ) def test_shape_noun_pluralises_and_counts_are_listed() -> None: svg = b"" assert extract_svg_text(svg, stem="topo") == SvgText( text='Vector diagram "topo" — 3 shapes (path×3)', structural=True ) def test_top_three_kinds_with_document_order_tie_break() -> None: svg = b"" result = extract_svg_text(svg, stem="census") assert result is not None assert result.text == 'Vector diagram "census" — 7 shapes (g×3, path×2, circle)' assert result.structural is True def test_structure_elements_are_never_counted_as_shapes() -> None: svg = b" " result = extract_svg_text(svg, stem="nested") assert result is not None assert result.text == 'Vector diagram "nested" — 2 shapes (g, rect)' def test_a_nested_svg_element_is_structure_too() -> None: svg = b"" result = extract_svg_text(svg, stem="inset") assert result is not None assert result.text == 'Vector diagram "inset" — 2 shapes (g, rect)' def test_viewbox_reports_width_and_height() -> None: svg = b'' result = extract_svg_text(svg, stem="wide") assert result is not None assert result.text == 'Vector diagram "wide" — 1 shape (path), viewBox 128×64' def test_viewbox_accepts_commas_and_integral_floats() -> None: svg = b'' result = extract_svg_text(svg, stem="comma") assert result is not None assert result.text == 'Vector diagram "comma" — 1 shape (path), viewBox 8×6' def test_viewbox_keeps_fractional_sizes_without_padding() -> None: svg = b'' result = extract_svg_text(svg) assert result is not None assert result.text == "Vector diagram — 2 shapes (g, circle), viewBox 8.5×6.25" def test_unparseable_viewbox_omits_the_clause() -> None: for raw in (b"", b"0 0 8", b"0 0 8 6 4", b"0 0 wide tall", b"0 0 NaN NaN"): svg = b'' result = extract_svg_text(svg, stem="odd") assert result is not None, raw assert result.text == 'Vector diagram "odd" — 1 shape (path)', raw def test_missing_viewbox_omits_the_clause() -> None: result = extract_svg_text(b"", stem="nobox") assert result is not None assert "viewBox" not in result.text def test_viewbox_without_shapes_is_the_only_fact() -> None: svg = b'' assert extract_svg_text(svg, stem="canvas") == SvgText( text='Vector diagram "canvas" — viewBox 16×9', structural=True ) def test_empty_stem_never_renders_empty_quotes() -> None: assert extract_svg_text(b"") == SvgText( text="Vector diagram", structural=True ) assert extract_svg_text(b'') == SvgText( text="Vector diagram — 1 shape (path), viewBox 1×1", structural=True ) def test_a_root_that_is_not_svg_still_yields_honest_text() -> None: """The walk is tag-based — a mis-named root is not rejected, and the root is never counted among the shapes.""" result = extract_svg_text(b"Legacy", stem="legacy") assert result == SvgText(text="Legacy", structural=False) structural = extract_svg_text(b"", stem="legacy") assert structural == SvgText( text='Vector diagram "legacy" — 1 shape (body)', structural=True )