commit 2394eff1c0d1439bc8ff6b25d55adc991022e45b Author: Mavis Date: Sat Jun 13 15:32:54 2026 +0300 Initial commit: OmniSVG-Lite MVP before live-streaming work - LM Studio client (httpx-based, OpenAI-compatible) - SVG validator (lxml, whitelist tags, no +""" + ok, reason, _ = validate_svg(bad, mode="icon") + assert ok is False + assert reason == "disallowed_tag" + + +def test_foreign_object_rejected(): + bad = """ + +
Hi
+
+
""" + ok, reason, _ = validate_svg(bad, mode="icon") + assert ok is False + assert reason == "disallowed_tag" + + +def test_http_ref_rejected(): + bad = """ + +""" + ok, reason, _ = validate_svg(bad, mode="icon") + assert ok is False + assert reason == "external_ref" + + +def test_onclick_attribute_rejected(): + bad = """ + +""" + ok, reason, _ = validate_svg(bad, mode="icon") + assert ok is False + assert reason == "event_handler" + + +def test_url_with_https_rejected(): + """https:// в url(...) тоже блокируется.""" + bad = """ + + + + + +""" + ok, reason, _ = validate_svg(bad, mode="icon") + assert ok is False + # Сейчас external_ref ловится на `url(` независимо от http(s). + assert reason in ("external_ref", "disallowed_tag", "unknown_tag") + + +def test_too_large_rejected(): + """SVG > MAX_BYTES[mode] → too_large.""" + # Соберём раздутый SVG с большим комментарием. + pad = "" + bad = ( + '' + + pad + + "" + ) + ok, reason, _ = validate_svg(bad, mode="icon") + assert ok is False + assert reason == "too_large" + + +def test_empty_input_rejected(): + ok, reason, _ = validate_svg("", mode="icon") + assert ok is False + assert reason == "empty_input" + + +def test_no_svg_block_rejected(): + ok, reason, _ = validate_svg("Sorry, I cannot help with that.", mode="icon") + assert ok is False + assert reason == "not_svg" + + +# --------------------------------------------------------------------------- +# Прямые проверки API (validate() бросает, extract_svg() утилитарный) +# --------------------------------------------------------------------------- + + +def test_validate_strict_raises(): + with pytest.raises(ValidatorError) as excinfo: + validate("plain text", mode="icon") + assert excinfo.value.code == "not_svg" + + +def test_extract_svg_finds_block(): + text = "noise noise" + found = extract_svg(text) + assert found.startswith("") + + +def test_validate_svg_returns_tuple_for_unexpected_exception(): + """Даже если что-то странное, обёртка возвращает кортеж (ok=False, ...).""" + ok, reason, cleaned = validate_svg(None, mode="icon") # type: ignore[arg-type] + assert ok is False + assert reason in ("empty_input", "internal:TypeError") + assert cleaned == "" diff --git a/validator.py b/validator.py new file mode 100644 index 0000000..2ba16e4 --- /dev/null +++ b/validator.py @@ -0,0 +1,228 @@ +"""Валидация SVG-ответов модели. + +Делает две вещи: +1) `extract_svg()` — вытаскивает ... из произвольного текста + (модель иногда отвечает с пояснениями или code-fence). +2) `validate(svg_text, mode=...)` — строгая проверка по правилам дизайна. +3) `validate_svg(svg_text) -> (ok, reason, cleaned_svg)` — обёртка для UI, + описанная в задаче: всегда возвращает кортеж, не бросает исключений. + +Правила (соответствуют §5 design.md и §6 spec.md): +- well-formed XML (lxml); +- ровно один корневой ; +- наличие `viewBox` (4 числа); +- теги — только из ALLOWED_TAGS; +-