Files
ss-tools/backend/tests/plugins/translate/test_llm_parse.py

208 lines
8.0 KiB
Python

# #region Test.LLMParse [C:3] [TYPE Module] [SEMANTICS test, translate, llm, parse, json, recovery]
# @BRIEF Tests for _llm_parse.py — parse_llm_response, markdown recovery, truncated row recovery.
# @RELATION BINDS_TO -> [_llm_parse]
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
import json
import pytest
from src.plugins.translate._llm_parse import (
parse_llm_response,
_recover_from_markdown,
_recover_truncated_rows,
)
class TestRecoverFromMarkdown:
"""_recover_from_markdown — extract JSON from markdown code blocks."""
def test_json_code_block(self):
"""Standard ```json code block."""
text = "```json\n{\"rows\": [{\"row_id\": 1, \"translation\": \"hola\"}]}\n```"
result = _recover_from_markdown(text)
assert result is not None
assert result["rows"][0]["row_id"] == 1
def test_no_marker_code_block(self):
"""Code block without json marker."""
text = "```\n{\"rows\": [{\"row_id\": 1}]}\n```"
result = _recover_from_markdown(text)
assert result is not None
assert len(result["rows"]) == 1
def test_no_code_block(self):
"""Plain text without code block."""
result = _recover_from_markdown("just plain text")
assert result is None
def test_invalid_json_in_block(self):
"""Code block with invalid JSON returns None."""
text = "```json\n{invalid json}\n```"
result = _recover_from_markdown(text)
assert result is None
def test_empty_block(self):
"""Empty code block returns None."""
text = "```json\n```"
result = _recover_from_markdown(text)
assert result is None
def test_inline_text_before_after(self):
"""Text before and after code block still extracts JSON."""
text = "some text\n```json\n{\"rows\": []}\n```\nmore text"
result = _recover_from_markdown(text)
assert result is not None
class TestRecoverTruncatedRows:
"""_recover_truncated_rows — extract complete rows from truncated JSON."""
def test_recover_single_row(self):
"""Single complete row extracted from truncated text."""
text = 'some text {"row_id": 1, "translation": "hola"} more text'
result = _recover_truncated_rows(text, 1, "length")
assert result is not None
assert len(result["rows"]) == 1
def test_recover_multiple_rows(self):
"""Multiple rows extracted."""
text = '{"row_id": 1, "translation": "hola"} {"row_id": 2, "translation": "adios"}'
result = _recover_truncated_rows(text, 2, "length")
assert result is not None
assert len(result["rows"]) == 2
def test_no_rows_found(self):
"""No row-like patterns found."""
result = _recover_truncated_rows("completely invalid text", 1, "length")
assert result is None
def test_invalid_row_patterns_skipped(self):
"""Invalid JSON within row-like patterns is skipped."""
text = '{"row_id": 1, invalid} {"row_id": 2, "translation": "ok"}'
result = _recover_truncated_rows(text, 1, "length")
assert result is not None
assert len(result["rows"]) == 1
def test_empty_text(self):
"""Empty text returns None."""
result = _recover_truncated_rows("", 1, "length")
assert result is None
def test_no_finish_reason(self):
"""Works without finish_reason."""
text = '{"row_id": 1, "translation": "hola"}'
result = _recover_truncated_rows(text, 1, None)
assert result is not None
class TestParseLLMResponse:
"""parse_llm_response — full LLM response parsing pipeline."""
def test_valid_json_direct(self):
"""Direct valid JSON response."""
response = json.dumps({
"rows": [
{"row_id": 1, "ru": "привет", "detected_source_language": "en"},
{"row_id": 2, "ru": "мир", "detected_source_language": "en"},
]
})
result = parse_llm_response(response, expected_count=2, target_languages=["ru"])
assert len(result) == 2
assert result["1"]["ru"] == "привет"
assert result["2"]["ru"] == "мир"
def test_missing_rows_key(self):
"""No 'rows' key returns empty dict (defaults to empty list)."""
result = parse_llm_response("{}", expected_count=1)
assert result == {}
def test_rows_not_a_list(self):
"""Rows key is not a list raises ValueError."""
with pytest.raises(ValueError, match="rows"):
parse_llm_response('{"rows": "not_a_list"}', expected_count=1)
def test_markdown_code_block(self):
"""Response wrapped in markdown code block."""
text = "```json\n{\"rows\": [{\"row_id\": 1, \"ru\": \"привет\"}]}\n```"
result = parse_llm_response(text, expected_count=1, target_languages=["ru"])
assert len(result) == 1
assert result["1"]["ru"] == "привет"
def test_truncated_recovery(self):
"""Truncated JSON recovers complete rows."""
text = '{"row_id": 1, "ru": "привет"} {"row_id": 2, "ru": "мир"'
result = parse_llm_response(text, expected_count=2, target_languages=["ru"])
assert len(result) >= 1
def test_invalid_json_fallback_to_markdown(self):
"""Invalid JSON falls through to markdown recovery."""
text = "some text\n```json\n{\"rows\": [{\"row_id\": 1, \"ru\": \"hola\"}]}\n```"
result = parse_llm_response(text, expected_count=1, target_languages=["ru"])
assert len(result) == 1
def test_row_without_id_skipped(self):
"""Row without row_id is skipped."""
response = json.dumps({
"rows": [
{"translation": "no_id"},
{"row_id": 2, "ru": "val", "detected_source_language": "en"},
]
})
result = parse_llm_response(response, expected_count=2, target_languages=["ru"])
assert len(result) == 1
assert "2" in result
def test_no_language_data_falls_back_to_translation(self):
"""Row without language key falls back to 'translation' key."""
response = json.dumps({
"rows": [
{"row_id": 1, "translation": "hola", "detected_source_language": "en"},
]
})
result = parse_llm_response(response, expected_count=1, target_languages=["ru"])
assert result["1"]["translation"] == "hola"
def test_no_language_or_translation_skipped(self):
"""Row with neither language nor translation is skipped."""
response = json.dumps({
"rows": [
{"row_id": 1, "detected_source_language": "en"},
]
})
result = parse_llm_response(response, expected_count=1, target_languages=["ru"])
assert len(result) == 0
def test_detected_language_defaults(self):
"""Missing detected_source_language defaults to 'und'."""
response = json.dumps({
"rows": [
{"row_id": 1, "ru": "привет"},
]
})
result = parse_llm_response(response, expected_count=1, target_languages=["ru"])
assert result["1"]["detected_source_language"] == "und"
def test_fully_failed_parse_raises(self):
"""Completely unparseable response raises ValueError."""
with pytest.raises(ValueError, match="not valid JSON"):
parse_llm_response("!@#$%^&*", expected_count=1)
def test_empty_target_languages(self):
"""Empty target_languages uses 'translation' fallback."""
response = json.dumps({
"rows": [
{"row_id": 1, "translation": "hola", "detected_source_language": "en"},
]
})
result = parse_llm_response(response, expected_count=1, target_languages=[])
assert result["1"]["translation"] == "hola"
def test_empty_rows(self):
"""Empty rows array returns empty dict."""
response = json.dumps({"rows": []})
result = parse_llm_response(response, expected_count=1)
assert result == {}