v4: 7643 tests passing, 83% raw / 96.9% real coverage (excl __tests__).

12 known failures — all from Agent 3 new unverified tests (mock setup issues):
- 3 dataset_review_routes_extended (DTO field mismatches)
- 1 settings_consolidated (dict key access)
- 1 llm_analysis_service_coverage (rate_limit mock)
- 1 migration_plugin (SessionLocal side_effect exhaustion)
- 1 preview (DB query vs dict key)
- 5 scheduler (datetime timezone + async mock mismatches)

NEW TEST FILES THIS SESSION:
- test_batch_insert_coverage.py — 3 tests
- test_storage_plugin.py — +3 tests
- test_search.py — +2 tests
- test_mapper.py — already 100%
- test_llm_analysis_migration_v1_to_v2.py — 14 tests
- test_llm_async_http.py — +1 test
- test_prompt_builder.py — +1 test
- test_service_datasource.py — +1 test
- test_lang_detect.py — +1 test
- test_scheduler.py — +6 tests
- test_llm_analysis_service_coverage.py — +15 tests
- test_dataset_review_routes_extended.py — +14 tests
- test_settings_consolidated.py — +13 tests

Modules pushed to 100%: _batch_insert, dictionary_entries, service_datasource,
_llm_async_http, prompt_builder, dictionary_crud, _batch_sizer, storage/plugin,
mapper.py

Session: 7194→7643 tests (+449), 80%→83% raw (+3pp), 93.4%→96.9% real (+3.5pp).
Remaining: 12 failures to fix + ~300 statements to reach 98% real coverage.
This commit is contained in:
2026-06-16 09:34:10 +03:00
parent 005ef0f5c7
commit 8a4310169b
28 changed files with 8511 additions and 1 deletions

View File

@@ -0,0 +1,262 @@
# #region Test.AdaptiveBatchSizer [C:3] [TYPE Module] [SEMANTICS test,batch,sizer,token,budget]
# @BRIEF Tests for _batch_sizer.py — AdaptiveBatchSizer.
# @RELATION BINDS_TO -> [AdaptiveBatchSizer]
# @TEST_EDGE: empty_rows -> empty list
# @TEST_EDGE: budget_zero -> fallback to fixed size
# @TEST_EDGE: per_batch_budget_collapsed -> fallback
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
import pytest
from unittest.mock import MagicMock, patch
from src.models.translate import TranslationJob
from src.plugins.translate._batch_sizer import AdaptiveBatchSizer
class TestResolveProviderConfig:
"""AdaptiveBatchSizer.resolve_provider_config."""
def test_no_provider_id(self):
"""No provider_id returns empty config."""
sizer = AdaptiveBatchSizer(MagicMock(), MagicMock())
job = MagicMock(spec=TranslationJob)
job.provider_id = None
config = sizer.resolve_provider_config(job)
assert config["model"] is None
assert config["context_window"] is None
assert config["max_output_tokens"] is None
def test_with_provider_id(self):
"""Provider ID returns token config."""
sizer = AdaptiveBatchSizer(MagicMock(), MagicMock())
job = MagicMock(spec=TranslationJob)
job.provider_id = "prov-1"
with patch("src.plugins.translate._batch_sizer.LLMProviderService") as mock_svc:
mock_instance = MagicMock()
mock_svc.return_value = mock_instance
mock_instance.get_provider_token_config.return_value = {
"model": "gpt-4o", "context_window": 128000, "max_output_tokens": 16384,
}
config = sizer.resolve_provider_config(job)
assert config["model"] == "gpt-4o"
assert config["context_window"] == 128000
def test_exception_returns_empty(self):
"""Exception in provider returns empty config."""
sizer = AdaptiveBatchSizer(MagicMock(), MagicMock())
job = MagicMock(spec=TranslationJob)
job.provider_id = "prov-err"
with patch("src.plugins.translate._batch_sizer.LLMProviderService") as mock_svc:
mock_instance = MagicMock()
mock_svc.return_value = mock_instance
mock_instance.get_provider_token_config.side_effect = Exception("DB error")
config = sizer.resolve_provider_config(job)
assert config["model"] is None
class TestAutoSizeBatches:
"""AdaptiveBatchSizer.auto_size_batches."""
def _make_job(self):
job = MagicMock(spec=TranslationJob)
job.provider_id = "prov-1"
job.translation_column = "source_text"
job.context_columns = None
job.batch_size = None
return job
def _make_sizer(self):
return AdaptiveBatchSizer(MagicMock(), MagicMock())
def test_empty_rows(self):
"""Empty source_rows returns empty list."""
sizer = self._make_sizer()
job = self._make_job()
batches = sizer.auto_size_batches(job, [], ["fr"])
assert batches == []
def test_single_batch(self):
"""Few rows fit in one batch."""
sizer = self._make_sizer()
job = self._make_job()
rows = [
{"source_text": f"text {i}", "source_data": {}}
for i in range(3)
]
with patch.object(sizer, "resolve_provider_config",
return_value={"model": "gpt-4o", "context_window": 128000,
"max_output_tokens": 16384}):
with patch("src.plugins.translate._batch_sizer.estimate_token_budget") as mock_budget:
mock_budget.return_value = {
"batch_size_adjusted": 10,
"available_input_budget": 100000,
"estimated_input_tokens": 50000,
"max_rows_by_output": 20,
"warning": None,
}
with patch("src.plugins.translate._batch_sizer.estimate_row_tokens",
return_value=100):
batches = sizer.auto_size_batches(job, rows, ["fr"])
assert len(batches) == 1
assert len(batches[0]) == 3
def test_fallback_to_fixed_size(self):
"""When budget returns zero, falls back to fixed batch_size."""
sizer = self._make_sizer()
job = self._make_job()
job.batch_size = 2
rows = [
{"source_text": f"text {i}", "source_data": {}}
for i in range(5)
]
with patch.object(sizer, "resolve_provider_config",
return_value={"model": "gpt-4o", "context_window": 128000,
"max_output_tokens": 16384}):
with patch("src.plugins.translate._batch_sizer.estimate_token_budget") as mock_budget:
mock_budget.return_value = {
"batch_size_adjusted": 0,
"available_input_budget": 0,
"estimated_input_tokens": 0,
"max_rows_by_output": 0,
}
with patch("src.plugins.translate._batch_sizer.estimate_row_tokens",
return_value=100):
batches = sizer.auto_size_batches(job, rows, ["fr"])
# 5 rows / 2 per batch = 3 batches
assert len(batches) == 3
def test_fallback_when_budget_collapsed(self):
"""When per_batch_budget <= 0, falls back."""
sizer = self._make_sizer()
job = self._make_job()
rows = [
{"source_text": "text", "source_data": {}}
for _ in range(3)
]
with patch.object(sizer, "resolve_provider_config",
return_value={"model": "gpt-4o", "context_window": 128000,
"max_output_tokens": 16384}):
with patch("src.plugins.translate._batch_sizer.estimate_token_budget") as mock_budget:
mock_budget.return_value = {
"batch_size_adjusted": 5,
"available_input_budget": 100,
"estimated_input_tokens": 50,
"max_rows_by_output": 1,
}
with patch("src.plugins.translate._batch_sizer.estimate_row_tokens",
return_value=10000):
batches = sizer.auto_size_batches(job, rows, ["fr"])
assert len(batches) >= 1
def test_single_row_exceeds_budget(self):
"""A row exceeding per-batch budget gets its own batch."""
sizer = self._make_sizer()
job = self._make_job()
rows = [
{"source_text": "short text", "source_data": {}},
{"source_text": "x" * 10000, "source_data": {}},
]
with patch.object(sizer, "resolve_provider_config",
return_value={"model": "gpt-4o", "context_window": 128000,
"max_output_tokens": 16384}):
with patch("src.plugins.translate._batch_sizer.estimate_token_budget") as mock_budget:
mock_budget.return_value = {
"batch_size_adjusted": 10,
"available_input_budget": 100000,
"estimated_input_tokens": 50000,
"max_rows_by_output": 20,
}
# Second row tokens must exceed per_batch_budget (~74550)
with patch("src.plugins.translate._batch_sizer.estimate_row_tokens",
side_effect=[50, 80000]):
batches = sizer.auto_size_batches(job, rows, ["fr"])
# Large row gets its own batch (row_tokens > per_batch_budget)
assert len(batches) >= 2
def test_respects_job_batch_size(self):
"""job.batch_size limits row count per batch."""
sizer = self._make_sizer()
job = self._make_job()
job.batch_size = 2
rows = [
{"source_text": f"text {i}", "source_data": {}}
for i in range(10)
]
with patch.object(sizer, "resolve_provider_config",
return_value={"model": "gpt-4o", "context_window": 128000,
"max_output_tokens": 16384}):
with patch("src.plugins.translate._batch_sizer.estimate_token_budget") as mock_budget:
mock_budget.return_value = {
"batch_size_adjusted": 10,
"available_input_budget": 100000,
"estimated_input_tokens": 50000,
"max_rows_by_output": 50,
}
with patch("src.plugins.translate._batch_sizer.estimate_row_tokens",
return_value=500):
batches = sizer.auto_size_batches(job, rows, ["fr"])
# Max 2 per batch due to job.batch_size
assert all(len(b) <= 2 for b in batches)
def test_uses_provider_info_fallback(self):
"""When provider_info is given and model is None, uses it."""
sizer = self._make_sizer()
job = self._make_job()
job.provider_id = None
rows = [{"source_text": "text", "source_data": {}}]
with patch.object(sizer, "resolve_provider_config",
return_value={"model": None, "context_window": None,
"max_output_tokens": None}):
with patch("src.plugins.translate._batch_sizer.estimate_token_budget") as mock_budget:
mock_budget.return_value = {
"batch_size_adjusted": 5,
"available_input_budget": 100000,
"estimated_input_tokens": 50000,
"max_rows_by_output": 20,
}
with patch("src.plugins.translate._batch_sizer.estimate_row_tokens",
return_value=100):
batches = sizer.auto_size_batches(job, rows, ["fr"],
provider_info="gpt-4o-mini")
assert len(batches) == 1
def test_available_input_budget_none(self):
"""When available_input_budget is None, uses estimated_input_tokens."""
sizer = self._make_sizer()
job = self._make_job()
rows = [{"source_text": "text", "source_data": {}} for _ in range(3)]
with patch.object(sizer, "resolve_provider_config",
return_value={"model": "gpt-4o", "context_window": 128000,
"max_output_tokens": 16384}):
with patch("src.plugins.translate._batch_sizer.estimate_token_budget") as mock_budget:
mock_budget.return_value = {
"batch_size_adjusted": 5,
"available_input_budget": None,
"estimated_input_tokens": 50000,
"max_rows_by_output": 20,
}
with patch("src.plugins.translate._batch_sizer.estimate_row_tokens",
return_value=100):
batches = sizer.auto_size_batches(job, rows, ["fr"])
assert len(batches) == 1
# #endregion Test.AdaptiveBatchSizer