fix: 5 agents — core 703/703, settings 38/38, git edges 191/191, API routes 1139/1140, plugins +70 coverage
This commit is contained in:
207
backend/tests/plugins/translate/test_llm_parse.py
Normal file
207
backend/tests/plugins/translate/test_llm_parse.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# #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 == {}
|
||||
@@ -0,0 +1,94 @@
|
||||
# #region Test.PreviewResponseParser [C:3] [TYPE Module] [SEMANTICS test, translate, response, parser, data, extract]
|
||||
# @BRIEF Tests for preview_response_parser.py — extract_data_rows.
|
||||
# @RELATION BINDS_TO -> [preview_response_parser]
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
import pytest
|
||||
|
||||
from src.plugins.translate.preview_response_parser import extract_data_rows
|
||||
|
||||
|
||||
class TestExtractDataRows:
|
||||
"""extract_data_rows — extract data rows from Superset chart data API response."""
|
||||
|
||||
def test_result_list_with_data(self):
|
||||
"""result is a list with item containing data key."""
|
||||
response = {
|
||||
"result": [
|
||||
{"data": [{"col1": "val1"}, {"col1": "val2"}]}
|
||||
]
|
||||
}
|
||||
result = extract_data_rows(response)
|
||||
assert len(result) == 2
|
||||
assert result[0]["col1"] == "val1"
|
||||
|
||||
def test_result_dict_with_data(self):
|
||||
"""result is a dict with data key."""
|
||||
response = {
|
||||
"result": {"data": [{"col1": "val1"}]}
|
||||
}
|
||||
result = extract_data_rows(response)
|
||||
assert len(result) == 1
|
||||
|
||||
def test_fallback_to_response_data(self):
|
||||
"""Fallback to response.data when result has no data."""
|
||||
response = {
|
||||
"data": [{"col1": "val1"}]
|
||||
}
|
||||
result = extract_data_rows(response)
|
||||
assert len(result) == 1
|
||||
|
||||
def test_result_list_returned_directly(self):
|
||||
"""When result is a list with no data key, return it."""
|
||||
response = {
|
||||
"result": [{"col1": "val1"}, {"col1": "val2"}]
|
||||
}
|
||||
result = extract_data_rows(response)
|
||||
assert len(result) == 2
|
||||
|
||||
def test_empty_result_list(self):
|
||||
"""Empty result list returns empty list."""
|
||||
response = {"result": []}
|
||||
result = extract_data_rows(response)
|
||||
assert result == []
|
||||
|
||||
def test_empty_data_list(self):
|
||||
"""Empty data list falls back to returning result list."""
|
||||
response = {
|
||||
"result": [{"data": []}]
|
||||
}
|
||||
result = extract_data_rows(response)
|
||||
# Returns the result list as fallback
|
||||
assert result == [{"data": []}]
|
||||
|
||||
def test_no_result_or_data_key(self):
|
||||
"""No result or data key returns empty list."""
|
||||
response = {"other": "value"}
|
||||
result = extract_data_rows(response)
|
||||
assert result == []
|
||||
|
||||
def test_result_not_list_or_dict(self):
|
||||
"""Result is neither list nor dict."""
|
||||
response = {"result": "string_value"}
|
||||
result = extract_data_rows(response)
|
||||
assert result == []
|
||||
|
||||
def test_result_list_item_no_data(self):
|
||||
"""Result list item has no data key, falls back to result list."""
|
||||
response = {
|
||||
"result": [{"other": "value"}]
|
||||
}
|
||||
result = extract_data_rows(response)
|
||||
# Returns the result list as fallback
|
||||
assert result == [{"other": "value"}]
|
||||
|
||||
def test_result_dict_data_empty_list(self):
|
||||
"""Result dict data is empty list."""
|
||||
response = {
|
||||
"result": {"data": []}
|
||||
}
|
||||
result = extract_data_rows(response)
|
||||
assert result == []
|
||||
290
backend/tests/plugins/translate/test_token_budget.py
Normal file
290
backend/tests/plugins/translate/test_token_budget.py
Normal file
@@ -0,0 +1,290 @@
|
||||
# #region Test.TokenBudget [C:3] [TYPE Module] [SEMANTICS test, translate, token, budget, estimation]
|
||||
# @BRIEF Tests for _token_budget.py — token estimation, batch sizing, output-aware constraints.
|
||||
# @RELATION BINDS_TO -> [_token_budget]
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
import pytest
|
||||
|
||||
from src.plugins.translate._token_budget import (
|
||||
_estimate_tokens_for_text,
|
||||
_count_rows_that_fit,
|
||||
_calculate_output_tokens,
|
||||
_compute_max_rows_by_output,
|
||||
_apply_output_aware_batch_sizing,
|
||||
_build_warning,
|
||||
estimate_token_budget,
|
||||
)
|
||||
|
||||
|
||||
class TestEstimateTokensForText:
|
||||
"""_estimate_tokens_for_text — CJK-aware heuristic token counting."""
|
||||
|
||||
def test_empty_string(self):
|
||||
"""Empty text returns 1."""
|
||||
assert _estimate_tokens_for_text("") == 1
|
||||
|
||||
def test_ascii_only(self):
|
||||
"""Pure ASCII text ~1.8 chars/token."""
|
||||
result = _estimate_tokens_for_text("hello world")
|
||||
assert result >= 1
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_cjk_only(self):
|
||||
"""Pure CJK text ~1.0 chars/token."""
|
||||
result = _estimate_tokens_for_text("你好世界")
|
||||
assert result >= 1
|
||||
|
||||
def test_mixed_text(self):
|
||||
"""Mixed CJK + ASCII."""
|
||||
result = _estimate_tokens_for_text("hello 你好 world 世界")
|
||||
assert result >= 1
|
||||
|
||||
def test_cjk_ranges(self):
|
||||
"""CJK includes Unicode ranges. Fullwidth forms."""
|
||||
result = _estimate_tokens_for_text("\uff01\uff02")
|
||||
assert result >= 1
|
||||
|
||||
def test_long_text(self):
|
||||
"""Long text estimate is proportional."""
|
||||
short = _estimate_tokens_for_text("hello")
|
||||
long_ = _estimate_tokens_for_text("hello " * 100)
|
||||
assert long_ > short
|
||||
|
||||
|
||||
class TestCountRowsThatFit:
|
||||
"""_count_rows_that_fit — consecutive rows within budget."""
|
||||
|
||||
def test_all_rows_fit(self):
|
||||
"""All rows fit within budget."""
|
||||
safe, total = _count_rows_that_fit([10, 20, 30], 5000)
|
||||
assert safe == 3
|
||||
assert total == 60
|
||||
|
||||
def test_partial_fit(self):
|
||||
"""Only first 2 rows fit."""
|
||||
safe, total = _count_rows_that_fit([10, 20, 5000], 2100)
|
||||
assert safe == 2
|
||||
assert total == 30
|
||||
|
||||
def test_no_rows_fit(self):
|
||||
"""First row exceeds budget."""
|
||||
safe, total = _count_rows_that_fit([5000], 1000)
|
||||
assert safe == 0
|
||||
assert total == 0
|
||||
|
||||
def test_empty_input(self):
|
||||
"""Empty list returns (0, 0)."""
|
||||
safe, total = _count_rows_that_fit([], 5000)
|
||||
assert safe == 0
|
||||
assert total == 0
|
||||
|
||||
def test_single_row_just_fits(self):
|
||||
"""Single row just fits (with reasoning overhead)."""
|
||||
budget = 200 + 2001 # row tokens + REASONING_OVERHEAD + 1
|
||||
safe, total = _count_rows_that_fit([200], budget)
|
||||
assert safe == 1
|
||||
assert total == 200
|
||||
|
||||
|
||||
class TestCalculateOutputTokens:
|
||||
"""_calculate_output_tokens — output budget estimation."""
|
||||
|
||||
def test_basic(self):
|
||||
"""Basic calculation returns positive int."""
|
||||
result = _calculate_output_tokens(
|
||||
safe_size=5, num_languages=2
|
||||
)
|
||||
assert result >= 1
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_zero_safe_size(self):
|
||||
"""Zero safe_size returns only overhead."""
|
||||
result = _calculate_output_tokens(safe_size=0, num_languages=2)
|
||||
assert result >= 2000 # REASONING_OVERHEAD
|
||||
|
||||
def test_large_batch(self):
|
||||
"""Large batch produces larger estimate."""
|
||||
small = _calculate_output_tokens(1, 1)
|
||||
large = _calculate_output_tokens(50, 5)
|
||||
assert large > small
|
||||
|
||||
|
||||
class TestComputeMaxRowsByOutput:
|
||||
"""_compute_max_rows_by_output — output-constrained row limit."""
|
||||
|
||||
def test_basic(self):
|
||||
"""Positive result for normal params."""
|
||||
result = _compute_max_rows_by_output(max_output_tokens=16384, num_languages=2)
|
||||
assert result >= 1
|
||||
|
||||
def test_small_output_budget(self):
|
||||
"""Tiny output budget returns 1."""
|
||||
result = _compute_max_rows_by_output(max_output_tokens=100, num_languages=2)
|
||||
assert result >= 1
|
||||
|
||||
def test_zero_per_row(self):
|
||||
"""Edge: zero per_row returns fallback 20."""
|
||||
result = _compute_max_rows_by_output(max_output_tokens=16384, num_languages=0)
|
||||
assert result >= 1
|
||||
|
||||
|
||||
class TestApplyOutputAwareBatchSizing:
|
||||
"""_apply_output_aware_batch_sizing — reduce batch until output fits."""
|
||||
|
||||
def test_no_reduction_needed(self):
|
||||
"""Batch fits without reduction."""
|
||||
result = _apply_output_aware_batch_sizing(
|
||||
safe_size=10, num_languages=1, max_output_tokens=50000
|
||||
)
|
||||
assert result == 10
|
||||
|
||||
def test_full_reduction(self):
|
||||
"""Batch reduced when output exceeds budget."""
|
||||
result = _apply_output_aware_batch_sizing(
|
||||
safe_size=10, num_languages=3, max_output_tokens=2000
|
||||
)
|
||||
assert result < 10
|
||||
|
||||
def test_zero_size(self):
|
||||
"""Zero safe_size returns 0."""
|
||||
result = _apply_output_aware_batch_sizing(
|
||||
safe_size=0, num_languages=1, max_output_tokens=50000
|
||||
)
|
||||
assert result == 0
|
||||
|
||||
|
||||
class TestBuildWarning:
|
||||
"""_build_warning — warning message generation."""
|
||||
|
||||
def test_no_warning_when_size_ok(self):
|
||||
"""No warning when batch size matches safe size."""
|
||||
assert _build_warning(10, 10, 20, 64000, 1000, 500, None) is None
|
||||
|
||||
def test_reduced_batch_warning(self):
|
||||
"""Warning when safe_size < requested batch_size."""
|
||||
w = _build_warning(10, 5, 20, 64000, 1000, 500, None)
|
||||
assert w is not None
|
||||
assert "Reduced" in w
|
||||
|
||||
def test_auto_calc_warning(self):
|
||||
"""Warning when auto-calculated batch smaller than total rows."""
|
||||
w = _build_warning(None, 5, 20, 64000, 1000, 500, None)
|
||||
assert w is not None
|
||||
assert "Auto-calculated" in w
|
||||
|
||||
def test_dict_warning_appended(self):
|
||||
"""Dictionary warning is appended."""
|
||||
w = _build_warning(10, 5, 20, 64000, 1000, 500, "Dict entries capped")
|
||||
assert w is not None
|
||||
assert "Dict entries capped" in w
|
||||
|
||||
def test_dict_warning_only(self):
|
||||
"""Dictionary warning alone when no batch reduction."""
|
||||
w = _build_warning(None, 20, 20, 64000, 1000, 500, "Dict capped")
|
||||
assert w == "Dict capped"
|
||||
|
||||
|
||||
class TestEstimateTokenBudget:
|
||||
"""estimate_token_budget — main budget estimation entry point."""
|
||||
|
||||
def test_empty_source_rows(self):
|
||||
"""Empty source rows returns sensible defaults."""
|
||||
result = estimate_token_budget(
|
||||
source_rows=[],
|
||||
target_languages=["ru"],
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
assert "estimated_input_tokens" in result
|
||||
assert "estimated_output_tokens" in result
|
||||
assert "max_output_needed" in result
|
||||
|
||||
def test_single_row_single_lang(self):
|
||||
"""Single row, single language."""
|
||||
result = estimate_token_budget(
|
||||
source_rows=[{"source_text": "hello world"}],
|
||||
target_languages=["ru"],
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
|
||||
def test_multiple_rows(self):
|
||||
"""Multiple rows produce batch estimate."""
|
||||
rows = [{"source_text": f"row {i}"} for i in range(5)]
|
||||
result = estimate_token_budget(
|
||||
source_rows=rows,
|
||||
target_languages=["ru", "de"],
|
||||
max_output_tokens=32000,
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
|
||||
def test_with_context_columns(self):
|
||||
"""Context columns add to token estimate."""
|
||||
rows = [{"source_text": "hello", "description": "a long description here"}]
|
||||
result = estimate_token_budget(
|
||||
source_rows=rows,
|
||||
target_languages=["ru"],
|
||||
source_column="source_text",
|
||||
context_columns=["description"],
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
|
||||
def test_with_dictionary_entries(self):
|
||||
"""Dictionary entries add tokens."""
|
||||
rows = [{"source_text": "hello"}]
|
||||
result = estimate_token_budget(
|
||||
source_rows=rows,
|
||||
target_languages=["ru"],
|
||||
dictionary_entries=[{"id": 1}, {"id": 2}],
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
|
||||
def test_with_provider_info(self):
|
||||
"""Provider info resolves context window."""
|
||||
rows = [{"source_text": "hello"}]
|
||||
result = estimate_token_budget(
|
||||
source_rows=rows,
|
||||
target_languages=["ru"],
|
||||
provider_info="gpt-4o-mini",
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
|
||||
def test_explicit_context_window(self):
|
||||
"""Explicit context_window overrides provider default."""
|
||||
rows = [{"source_text": "hello"}]
|
||||
result = estimate_token_budget(
|
||||
source_rows=rows,
|
||||
target_languages=["ru"],
|
||||
context_window=128000,
|
||||
max_output_tokens=4096,
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
assert result["available_input_budget"] == 128000 - 4096
|
||||
|
||||
def test_large_batch_reduced(self):
|
||||
"""Batch size reduced when too large."""
|
||||
many_rows = [{"source_text": "x" * 5000} for _ in range(100)]
|
||||
result = estimate_token_budget(
|
||||
source_rows=many_rows,
|
||||
target_languages=["ru", "de", "fr"],
|
||||
batch_size=50,
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
|
||||
def test_no_target_languages_defaults(self):
|
||||
"""Empty target_languages defaults to ['en']."""
|
||||
result = estimate_token_budget(
|
||||
source_rows=[{"source_text": "hello"}],
|
||||
target_languages=[],
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
|
||||
def test_provider_not_found_fallback(self):
|
||||
"""Unknown provider falls back to defaults."""
|
||||
result = estimate_token_budget(
|
||||
source_rows=[{"source_text": "hello"}],
|
||||
target_languages=["ru"],
|
||||
provider_info="unknown-model-42",
|
||||
)
|
||||
assert result["batch_size_adjusted"] >= 1
|
||||
Reference in New Issue
Block a user