# #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 == {}