Files
ss-tools/backend/tests/plugins/translate/test_batch_insert_coverage.py
busya 3133e50645 perf: fix translate deadlock, speed, trace_id, UI bugs — fullstack patch
## Backend (7 production files + 6 test files)

### P0-2: LLM output truncation cascade fix
- _token_budget.py: OUTPUT_PER_ROW_PER_LANG 120→200, OUTPUT_SAFETY_FACTOR 0.70→0.55
- Prevents finish_reason=length → split → retry cascade (3 calls → 1 call per batch)
- P2-8: added qwen-flash/qwen-plus/qwen-max/qwen-coder to PROVIDER_DEFAULTS

### P1-4/P1-5: EncryptionManager singleton
- encryption.py: get_encryption_manager() process-wide singleton
- llm_provider.py: use singleton instead of new EncryptionManager() per batch
- Eliminates ~90 redundant Fernet key validations per translation run

### P1-6: Cache-hit log aggregation
- _batch_proc.py: one log per batch (batch_rows + cache_hits) instead of per-row
- 1076 log lines → ~30 per run

### P1-7: Timezone-aware datetime fix
- scheduler.py: _ensure_aware() helper for naive DB datetime → UTC-aware
- Fixes TypeError in scheduled translation concurrency check

### P2-9: Connection test timeout
- connection_service.py: asyncio.wait_for(15s) on all dialect tests
- Prevents 2-minute UI hangs from DNS/TCP stalls

### Trace ID propagation
- middleware/trace.py: inject x-trace-id response header via ASGI send wrapper

### Test fixes & integration tests
- test_scheduler.py: AsyncMock for execute_run, mock get_async_job_runner
- test_sql_insert_service.py: AsyncMock for execute_sql
- test_token_budget.py: batch_size 50→45 for new OUTPUT_PER_ROW_PER_LANG=200
- test_encryption.py: +2 singleton tests
- test_scheduler_ensure_aware.py: +4 (naive→aware, passthrough, None, subtraction)
- test_batch_classify_persist.py: +2 cache-hit aggregation tests
- test_connection_service_edge.py: +2 timeout tests
- test_trace_middleware.py: +4 x-trace-id header tests
- test_token_budget.py: +4 qwen-flash/O200 tests

## Frontend (7 production files + 5 test files)

### Trace ID propagation
- api.ts: _captureTraceId() reads x-trace-id → setTraceId() in fetchApi/requestApi/postApi/deleteApi

### Duplicate datasource columns fetch
- ConfigTabForm.svelte: guard availableColumns.length === 0 before fetch

### Admin pages Svelte 5 runes fix
- admin/users/+page.svelte: plain let → () for all template-bound vars
- admin/roles/+page.svelte: same fix
- Both pages were stuck on «Загрузка...» due to mixed reactivity models

### Validation popover positioning
- +page.svelte: pass trigger HTMLElement instead of event
- DashboardHubModel.svelte.ts: toggleValidationPopover(HTMLElement), closeValidationPopover()
- Added X close button + click-outside overlay + i18n

### Test fixes & integration tests
- api.test.ts: mock setTraceId/getTraceId, +3 _captureTraceId tests
- provider_config.integration.test.ts: handleDelete→promptDeleteProvider
- DatasetPreview.test.ts: dashboards/ → ROUTES.dashboards
- test_config_tab_form.svelte.js: +2 columns fetch guard tests (NEW)
- admin-users.test.ts: +3 loading→table tests (NEW)
- admin-roles.test.ts: +2 loading→table tests (NEW)

## Semantic curation
- Removed @COMPLEXITY N from 6 route files + metrics.py (duplicate of [C:N])
- Added [C:N] to 2 orphan child contracts in metrics.py
- Added [C:N] + @BRIEF to 4 frontend anchors
- Fixed #region → # #region consistency in validation_tasks.py

## Verification
- Backend: 608 pytest passed (0 failures)
- Frontend: 2472 vitest passed (128 files, 0 failures)
- Frontend build: ✓ built in 18s
- Browser: dashboards, admin/users, admin/roles, validation popover — all green
2026-06-18 23:54:57 +03:00

221 lines
9.9 KiB
Python

# #region Test.BatchInsert.Coverage [C:3] [TYPE Module] [SEMANTICS test,translate,batch,insert,coverage]
# @BRIEF Coverage extension for _batch_insert.py — edge cases not covered in test_batch_insert.py.
# @RELATION BINDS_TO -> [BatchInsertService]
# @TEST_EDGE: missing_key_in_source_data -> base_row[k] = None
# @TEST_EDGE: empty_columns_fallback -> Uses effective_target as single column
# @TEST_EDGE: sql_chunk_is_none -> Skipped in chunked insert loop
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from src.models.translate import TranslationJob, TranslationRecord, TranslationLanguage
from src.plugins.translate._batch_insert import (
_build_insert_rows,
insert_batch_to_target,
)
from .conftest import JOB_ID
class TestBuildInsertRowsEdgeCases:
"""_build_insert_rows — edge cases for coverage."""
def _make_record(self, source_sql="hello", target_sql="bonjour",
source_data=None, languages=None):
rec = MagicMock(spec=TranslationRecord)
rec.source_sql = source_sql
rec.target_sql = target_sql
rec.source_data = source_data or {}
rec.languages = languages or []
rec.status = "SUCCESS"
return rec
def test_missing_key_in_source_data(self):
"""When target_key_cols key is missing from source_data -> base_row[k] = None (line 158)."""
lang = MagicMock(spec=TranslationLanguage)
lang.language_code = "fr"
lang.final_value = "bonjour"
lang.translated_value = "bonjour"
lang.source_language_detected = "en"
# source_data has 'id' but NOT 'created_at'
rec = self._make_record(
source_sql="hello", target_sql="bonjour",
source_data={"id": 1}, languages=[lang],
)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = ["id", "created_at"]
job.target_source_column = None
job.target_source_language_column = None
job.target_language_column = "lang"
job.context_columns = []
rows = _build_insert_rows([rec], job, "translated_text", "en", [])
assert len(rows) == 2
# 'id' present in source_data -> value should be 1
assert rows[0].get("id") == 1
# 'created_at' missing -> should be None
assert rows[0].get("created_at") is None
assert rows[1].get("created_at") is None
def test_primary_language_column_with_target_language(self):
"""When target_language_column is set and languages exist, translation row gets correct lang."""
lang = MagicMock(spec=TranslationLanguage)
lang.language_code = "de"
lang.final_value = "hallo"
lang.translated_value = "hallo"
lang.source_language_detected = "en"
rec = self._make_record(
source_sql="hello", target_sql="hallo",
source_data={"id": 1}, languages=[lang],
)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = ["id"]
job.target_source_column = "source"
job.target_source_language_column = "src_lang"
job.target_language_column = "lang"
job.context_columns = []
rows = _build_insert_rows([rec], job, "translated_text", "en", [])
assert len(rows) == 2
assert rows[0]["is_original"] == 1
assert rows[0]["lang"] == "en" # detected source lang
assert rows[1]["is_original"] == 0
assert rows[1]["lang"] == "de" # translation lang
def test_multiple_languages_with_skip(self):
"""Multiple languages: skip language matching detected source."""
en_lang = MagicMock(spec=TranslationLanguage)
en_lang.language_code = "en"
en_lang.final_value = "hello"
en_lang.translated_value = "hello"
en_lang.source_language_detected = "en"
fr_lang = MagicMock(spec=TranslationLanguage)
fr_lang.language_code = "fr"
fr_lang.final_value = "bonjour"
fr_lang.translated_value = "bonjour"
fr_lang.source_language_detected = "en"
de_lang = MagicMock(spec=TranslationLanguage)
de_lang.language_code = "de"
de_lang.final_value = "hallo"
de_lang.translated_value = "hallo"
de_lang.source_language_detected = "en"
rec = self._make_record(
source_sql="hello", target_sql="bonjour",
source_data={"id": 1}, languages=[en_lang, fr_lang, de_lang],
)
job = MagicMock(spec=TranslationJob)
job.target_key_cols = []
job.target_source_column = None
job.target_source_language_column = None
job.target_language_column = None
job.context_columns = []
rows = _build_insert_rows([rec], job, "translated_text", "en", [])
# Original + 2 translations (en skipped, fr + de kept)
assert len(rows) == 3
assert rows[0]["is_original"] == 1
assert rows[1]["is_original"] == 0
assert rows[2]["is_original"] == 0
# The first non-original should be fr (first in order after en skip)
assert rows[1]["translated_text"] == "bonjour"
assert rows[2]["translated_text"] == "hallo"
class TestInsertBatchToTargetEdgeCases:
"""insert_batch_to_target — edge case coverage for lines 47-48, 68."""
@pytest.mark.asyncio
async def test_empty_columns_fallback(self, db_session):
"""When _build_target_columns returns empty, falls back to single column (lines 47-48)."""
config_manager = MagicMock()
job = MagicMock(spec=TranslationJob)
job.target_column = None
job.translation_column = None
job.target_languages = None
job.target_schema = None
job.target_table = "translated_data"
job.target_key_cols = []
job.upsert_strategy = "MERGE"
job.environment_id = None
job.source_dialect = None
job.target_database_id = None
job.database_dialect = "postgresql"
job.target_dialect = None
rec = MagicMock(spec=TranslationRecord)
rec.target_sql = "bonjour"
# Force _build_target_columns to return empty, _build_insert_rows to return non-empty
from src.plugins.translate._batch_insert import _build_target_columns
original_build_columns = _build_target_columns
with patch("src.plugins.translate._batch_insert._build_target_columns", return_value=[]):
with patch("src.plugins.translate._batch_insert._build_context_keys", return_value=[]):
with patch("src.plugins.translate._batch_insert._build_insert_rows",
return_value=[{"text": "bonjour"}]):
with patch("src.plugins.translate._batch_insert._resolve_insert_backend",
return_value=("postgresql", AsyncMock(), None)):
with patch("src.plugins.translate._batch_insert.SQLGenerator.generate_batch",
return_value=[("INSERT INTO ...", 1)]):
with patch("src.plugins.translate._batch_insert._execute_insert_sql",
AsyncMock()):
with patch("src.plugins.translate._batch_insert._fetch_batch_records",
return_value=[rec]):
result = await insert_batch_to_target(
db_session, config_manager, job, "batch-1", "run-1"
)
assert result is None
@pytest.mark.asyncio
async def test_sql_chunk_is_none_skipped(self, db_session):
"""When a SQL chunk has sql=None, it is skipped (line 68)."""
config_manager = MagicMock()
job = MagicMock(spec=TranslationJob)
job.target_column = "text"
job.translation_column = None
job.target_languages = None
job.target_schema = None
job.target_table = "translated_data"
job.target_key_cols = []
job.upsert_strategy = "MERGE"
job.environment_id = None
job.source_dialect = None
job.target_database_id = None
job.database_dialect = "postgresql"
job.target_dialect = None
rec = MagicMock(spec=TranslationRecord)
rec.target_sql = "bonjour"
# generate_batch returns [ (None, 0), ("INSERT ...", 1) ]
# The None chunk should be skipped via continue, the second should execute
mock_executor = AsyncMock()
mock_executor.execute_and_poll.return_value = {"status": "success"}
with patch("src.plugins.translate._batch_insert._fetch_batch_records", return_value=[rec]):
with patch("src.plugins.translate._batch_insert._build_target_columns",
return_value=["text", "is_original"]):
with patch("src.plugins.translate._batch_insert._build_context_keys", return_value=[]):
with patch("src.plugins.translate._batch_insert._build_insert_rows",
return_value=[{"text": "bonjour", "is_original": 1}]):
with patch("src.plugins.translate._batch_insert._resolve_insert_backend",
return_value=("postgresql", mock_executor, None)):
with patch("src.plugins.translate._batch_insert.SQLGenerator.generate_batch",
return_value=[(None, 0), ("INSERT INTO target ...", 1)]):
result = await insert_batch_to_target(
db_session, config_manager, job, "batch-1", "run-1"
)
assert result is None
# execute_and_poll should be called only once (second chunk)
mock_executor.execute_and_poll.assert_called_once()
# #endregion Test.BatchInsert.Coverage