- Backend: alembic env, config manager/models, dependencies, translate plugin - Backend tests: async_sync_regression, integration tests, git services, test_agent - Docker: docker-compose.yml updates - Agent: qa-tester.md update, semantics-testing SKILL.md update - Frontend: TopNavbar, sidebarNavigation, FeaturesSettings, FeatureGate - i18n: assistant.json en/ru locale updates - New: frontend/src/lib/components/agent/ directory
563 lines
22 KiB
Python
563 lines
22 KiB
Python
# #region TranslateServiceIntegrationTests [C:4] [TYPE Module] [SEMANTICS test, integration, translate, service, postgres, testcontainers]
|
|
# @BRIEF Integration tests for TranslateJobService with real PostgreSQL via Testcontainers.
|
|
# @RELATION BINDS_TO -> [TranslateJobService]
|
|
# @RELATION BINDS_TO -> [TranslationJob]
|
|
# @RELATION BINDS_TO -> [TranslationJobDictionary]
|
|
# @TEST_CONTRACT TranslateJobService ->
|
|
# {
|
|
# invariants: [
|
|
# "create_job persists job with all fields",
|
|
# "update_job modifies only specified fields",
|
|
# "delete_job cascades to dictionary associations",
|
|
# "duplicate_job copies all config with new ID",
|
|
# "list_jobs respects pagination and status filter",
|
|
# "JSON columns (source_key_cols, target_languages) round-trip correctly"
|
|
# ]
|
|
# }
|
|
# @TEST_EDGE missing_translation_column -> ValueError when datasource set but no column
|
|
# @TEST_EDGE invalid_upsert_strategy -> ValueError on unsupported strategy
|
|
# @TEST_EDGE invalid_language_code -> ValueError on non-BCP47 language
|
|
# @TEST_EDGE job_not_found -> ValueError on get/update/delete of non-existent job
|
|
# @TEST_EDGE cascade_delete -> dictionary associations removed when job deleted
|
|
# @TEST_INVARIANT json_roundtrip -> VERIFIED_BY: [test_create_job_json_columns_persist]
|
|
# @TEST_INVARIANT cascade_integrity -> VERIFIED_BY: [test_delete_job_cascades_dictionary_associations]
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
|
|
|
import pytest
|
|
from sqlalchemy.orm import Session
|
|
from unittest.mock import MagicMock, AsyncMock, patch
|
|
|
|
from src.models.translate import (
|
|
TerminologyDictionary,
|
|
TranslationJob,
|
|
TranslationJobDictionary,
|
|
)
|
|
from src.plugins.translate.service import TranslateJobService
|
|
from src.schemas.translate import TranslateJobCreate, TranslateJobUpdate
|
|
|
|
|
|
# #region TestTranslateJobServiceCRUD [C:3] [TYPE Class]
|
|
# @BRIEF Integration tests for job CRUD operations with real PostgreSQL.
|
|
class TestTranslateJobServiceCRUD:
|
|
"""Verify TranslateJobService CRUD with real PostgreSQL."""
|
|
|
|
# region test_create_job_persists_all_fields [C:2] [TYPE Function]
|
|
# @BRIEF Verify job creation persists all fields including JSON columns.
|
|
@pytest.mark.asyncio
|
|
async def test_create_job_persists_all_fields(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
payload = TranslateJobCreate(
|
|
name="Integration Test Job",
|
|
description="Testing real PostgreSQL persistence",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
source_key_cols=["id", "tenant_id"],
|
|
target_key_cols=["id", "tenant_id"],
|
|
translation_column="name",
|
|
context_columns=["description", "category"],
|
|
target_languages=["ru", "en", "de"],
|
|
batch_size=100,
|
|
upsert_strategy="MERGE",
|
|
)
|
|
|
|
job = await service.create_job(payload)
|
|
|
|
assert job.id is not None
|
|
assert job.name == "Integration Test Job"
|
|
assert job.source_dialect == "postgresql"
|
|
assert job.target_dialect == "clickhouse"
|
|
assert job.status == "DRAFT"
|
|
assert job.created_by == "test_user"
|
|
|
|
# Verify JSON columns round-trip correctly
|
|
assert job.source_key_cols == ["id", "tenant_id"]
|
|
assert job.target_key_cols == ["id", "tenant_id"]
|
|
assert job.context_columns == ["description", "category"]
|
|
assert job.target_languages == ["ru", "en", "de"]
|
|
assert job.batch_size == 100
|
|
assert job.upsert_strategy == "MERGE"
|
|
|
|
# Verify persisted in DB
|
|
fetched = db_session.query(TranslationJob).filter(TranslationJob.id == job.id).first()
|
|
assert fetched is not None
|
|
assert fetched.name == "Integration Test Job"
|
|
assert fetched.target_languages == ["ru", "en", "de"]
|
|
# endregion test_create_job_persists_all_fields
|
|
|
|
# region test_create_job_with_dictionary_associations [C:2] [TYPE Function]
|
|
# @BRIEF Verify job creation with dictionary IDs creates associations.
|
|
@pytest.mark.asyncio
|
|
async def test_create_job_with_dictionary_associations(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
# Create dictionaries first
|
|
dict1 = TerminologyDictionary(name="Dict 1", created_by="test_user")
|
|
dict2 = TerminologyDictionary(name="Dict 2", created_by="test_user")
|
|
db_session.add_all([dict1, dict2])
|
|
db_session.commit()
|
|
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
payload = TranslateJobCreate(
|
|
name="Job With Dicts",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
dictionary_ids=[dict1.id, dict2.id],
|
|
)
|
|
|
|
job = await service.create_job(payload)
|
|
|
|
# Verify associations
|
|
associations = (
|
|
db_session.query(TranslationJobDictionary)
|
|
.filter(TranslationJobDictionary.job_id == job.id)
|
|
.all()
|
|
)
|
|
assert len(associations) == 2
|
|
dict_ids = {a.dictionary_id for a in associations}
|
|
assert dict_ids == {dict1.id, dict2.id}
|
|
# endregion test_create_job_with_dictionary_associations
|
|
|
|
# region test_create_job_missing_translation_column_raises [C:2] [TYPE Function]
|
|
# @BRIEF Verify ValueError when datasource set but no translation column.
|
|
@pytest.mark.asyncio
|
|
async def test_create_job_missing_translation_column_raises(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
payload = TranslateJobCreate(
|
|
name="Bad Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
source_datasource_id="42", # Datasource set
|
|
# translation_column missing!
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="translation column is required"):
|
|
await service.create_job(payload)
|
|
# endregion test_create_job_missing_translation_column_raises
|
|
|
|
# region test_create_job_invalid_upsert_strategy_raises [C:2] [TYPE Function]
|
|
# @BRIEF Verify ValueError on unsupported upsert strategy.
|
|
@pytest.mark.asyncio
|
|
async def test_create_job_invalid_upsert_strategy_raises(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
payload = TranslateJobCreate(
|
|
name="Bad Strategy",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="INVALID_STRATEGY",
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Invalid upsert_strategy"):
|
|
await service.create_job(payload)
|
|
# endregion test_create_job_invalid_upsert_strategy_raises
|
|
|
|
# region test_create_job_invalid_language_raises [C:2] [TYPE Function]
|
|
# @BRIEF Verify ValueError on non-BCP47 language code.
|
|
@pytest.mark.asyncio
|
|
async def test_create_job_invalid_language_raises(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
from pydantic import ValidationError
|
|
|
|
# Pydantic validates at schema level, so we catch ValidationError
|
|
with pytest.raises(ValidationError, match="target_languages"):
|
|
TranslateJobCreate(
|
|
name="Bad Language",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
target_languages=["123-invalid"], # Starts with digits, invalid BCP47
|
|
)
|
|
# endregion test_create_job_invalid_language_raises
|
|
|
|
# region test_get_job_not_found_raises [C:2] [TYPE Function]
|
|
# @BRIEF Verify ValueError when job not found.
|
|
def test_get_job_not_found_raises(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
with pytest.raises(ValueError, match="not found"):
|
|
service.get_job("non-existent-job-id")
|
|
# endregion test_get_job_not_found_raises
|
|
|
|
# region test_update_job_modifies_specified_fields [C:2] [TYPE Function]
|
|
# @BRIEF Verify update only modifies specified fields.
|
|
@pytest.mark.asyncio
|
|
async def test_update_job_modifies_specified_fields(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
# Create initial job
|
|
create_payload = TranslateJobCreate(
|
|
name="Original Name",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
target_languages=["ru"],
|
|
)
|
|
job = await service.create_job(create_payload)
|
|
original_id = job.id
|
|
original_batch_size = job.batch_size
|
|
|
|
# Update only name and batch_size
|
|
update_payload = TranslateJobUpdate(
|
|
name="Updated Name",
|
|
batch_size=200,
|
|
)
|
|
updated_job = await service.update_job(original_id, update_payload)
|
|
|
|
assert updated_job.id == original_id
|
|
assert updated_job.name == "Updated Name"
|
|
assert updated_job.batch_size == 200
|
|
# Unchanged fields
|
|
assert updated_job.source_dialect == "postgresql"
|
|
assert updated_job.target_dialect == "clickhouse"
|
|
assert updated_job.target_languages == ["ru"]
|
|
# endregion test_update_job_modifies_specified_fields
|
|
|
|
# region test_update_job_dictionary_replacement [C:2] [TYPE Function]
|
|
# @BRIEF Verify updating dictionary_ids replaces associations.
|
|
@pytest.mark.asyncio
|
|
async def test_update_job_dictionary_replacement(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
# Create dictionaries
|
|
dict1 = TerminologyDictionary(name="Dict 1", created_by="test_user")
|
|
dict2 = TerminologyDictionary(name="Dict 2", created_by="test_user")
|
|
dict3 = TerminologyDictionary(name="Dict 3", created_by="test_user")
|
|
db_session.add_all([dict1, dict2, dict3])
|
|
db_session.commit()
|
|
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
# Create job with dict1
|
|
create_payload = TranslateJobCreate(
|
|
name="Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
dictionary_ids=[dict1.id],
|
|
)
|
|
job = await service.create_job(create_payload)
|
|
|
|
# Update to dict2 and dict3 (replacing dict1)
|
|
update_payload = TranslateJobUpdate(dictionary_ids=[dict2.id, dict3.id])
|
|
await service.update_job(job.id, update_payload)
|
|
|
|
# Verify associations replaced
|
|
dict_ids = service.get_job_dictionary_ids(job.id)
|
|
assert set(dict_ids) == {dict2.id, dict3.id}
|
|
assert dict1.id not in dict_ids
|
|
# endregion test_update_job_dictionary_replacement
|
|
|
|
# region test_delete_job_cascades_dictionary_associations [C:2] [TYPE Function]
|
|
# @BRIEF Verify deleting job removes dictionary associations.
|
|
@pytest.mark.asyncio
|
|
async def test_delete_job_cascades_dictionary_associations(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
# Create dictionary and job with association
|
|
dictionary = TerminologyDictionary(name="Test Dict", created_by="test_user")
|
|
db_session.add(dictionary)
|
|
db_session.commit()
|
|
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
create_payload = TranslateJobCreate(
|
|
name="To Delete",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
dictionary_ids=[dictionary.id],
|
|
)
|
|
job = await service.create_job(create_payload)
|
|
job_id = job.id
|
|
|
|
# Verify association exists
|
|
assert len(service.get_job_dictionary_ids(job_id)) == 1
|
|
|
|
# Delete job
|
|
service.delete_job(job_id)
|
|
|
|
# Verify job deleted
|
|
with pytest.raises(ValueError, match="not found"):
|
|
service.get_job(job_id)
|
|
|
|
# Verify associations removed
|
|
remaining = (
|
|
db_session.query(TranslationJobDictionary)
|
|
.filter(TranslationJobDictionary.job_id == job_id)
|
|
.all()
|
|
)
|
|
assert len(remaining) == 0
|
|
# endregion test_delete_job_cascades_dictionary_associations
|
|
|
|
# region test_duplicate_job_copies_all_config [C:2] [TYPE Function]
|
|
# @BRIEF Verify duplicate copies all config with new ID and DRAFT status.
|
|
@pytest.mark.asyncio
|
|
async def test_duplicate_job_copies_all_config(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
# Create dictionary
|
|
dictionary = TerminologyDictionary(name="Test Dict", created_by="test_user")
|
|
db_session.add(dictionary)
|
|
db_session.commit()
|
|
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
create_payload = TranslateJobCreate(
|
|
name="Original Job",
|
|
description="Original description",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
source_key_cols=["id"],
|
|
target_key_cols=["id"],
|
|
translation_column="name",
|
|
context_columns=["category"],
|
|
target_languages=["ru", "en"],
|
|
batch_size=100,
|
|
upsert_strategy="MERGE",
|
|
dictionary_ids=[dictionary.id],
|
|
)
|
|
original = await service.create_job(create_payload)
|
|
|
|
# Duplicate
|
|
duplicate = service.duplicate_job(original.id, "Duplicated Job")
|
|
|
|
# Verify new ID and name
|
|
assert duplicate.id != original.id
|
|
assert duplicate.name == "Duplicated Job"
|
|
assert duplicate.status == "DRAFT"
|
|
|
|
# Verify all config copied
|
|
assert duplicate.description == original.description
|
|
assert duplicate.source_dialect == original.source_dialect
|
|
assert duplicate.target_dialect == original.target_dialect
|
|
assert duplicate.source_key_cols == original.source_key_cols
|
|
assert duplicate.target_key_cols == original.target_key_cols
|
|
assert duplicate.translation_column == original.translation_column
|
|
assert duplicate.context_columns == original.context_columns
|
|
assert duplicate.target_languages == original.target_languages
|
|
assert duplicate.batch_size == original.batch_size
|
|
assert duplicate.upsert_strategy == original.upsert_strategy
|
|
|
|
# Verify dictionary associations copied
|
|
original_dict_ids = set(service.get_job_dictionary_ids(original.id))
|
|
duplicate_dict_ids = set(service.get_job_dictionary_ids(duplicate.id))
|
|
assert original_dict_ids == duplicate_dict_ids
|
|
# endregion test_duplicate_job_copies_all_config
|
|
|
|
# region test_list_jobs_pagination [C:2] [TYPE Function]
|
|
# @BRIEF Verify list_jobs respects pagination.
|
|
@pytest.mark.asyncio
|
|
async def test_list_jobs_pagination(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
# Create 5 jobs
|
|
for i in range(5):
|
|
payload = TranslateJobCreate(
|
|
name=f"Job {i}",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
)
|
|
await service.create_job(payload)
|
|
|
|
# Test pagination
|
|
total, page1 = service.list_jobs(page=1, page_size=2)
|
|
assert total == 5
|
|
assert len(page1) == 2
|
|
|
|
total, page2 = service.list_jobs(page=2, page_size=2)
|
|
assert total == 5
|
|
assert len(page2) == 2
|
|
|
|
total, page3 = service.list_jobs(page=3, page_size=2)
|
|
assert total == 5
|
|
assert len(page3) == 1
|
|
# endregion test_list_jobs_pagination
|
|
|
|
# region test_list_jobs_status_filter [C:2] [TYPE Function]
|
|
# @BRIEF Verify list_jobs filters by status.
|
|
@pytest.mark.asyncio
|
|
async def test_list_jobs_status_filter(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
# Create jobs with different statuses
|
|
for i in range(3):
|
|
payload = TranslateJobCreate(
|
|
name=f"Draft Job {i}",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
)
|
|
job = await service.create_job(payload)
|
|
# Jobs are created as DRAFT
|
|
|
|
# Create a READY job
|
|
ready_payload = TranslateJobCreate(
|
|
name="Ready Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
)
|
|
ready_job = await service.create_job(ready_payload)
|
|
ready_job.status = "READY"
|
|
db_session.commit()
|
|
|
|
# Filter by DRAFT
|
|
total, draft_jobs = service.list_jobs(status_filter="DRAFT")
|
|
assert total == 3
|
|
assert all(j.status == "DRAFT" for j in draft_jobs)
|
|
|
|
# Filter by READY
|
|
total, ready_jobs = service.list_jobs(status_filter="READY")
|
|
assert total == 1
|
|
assert ready_jobs[0].status == "READY"
|
|
# endregion test_list_jobs_status_filter
|
|
|
|
# #endregion TestTranslateJobServiceCRUD
|
|
|
|
|
|
# #region TestTranslateJobServiceJSONColumns [C:3] [TYPE Class]
|
|
# @BRIEF Integration tests for JSON column behavior with real PostgreSQL.
|
|
class TestTranslateJobServiceJSONColumns:
|
|
"""Verify JSON column round-trip with PostgreSQL JSONB."""
|
|
|
|
# region test_json_columns_empty_arrays [C:2] [TYPE Function]
|
|
# @BRIEF Verify empty arrays persist correctly in JSON columns.
|
|
@pytest.mark.asyncio
|
|
async def test_json_columns_empty_arrays(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
payload = TranslateJobCreate(
|
|
name="Empty Arrays Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
source_key_cols=[],
|
|
target_key_cols=[],
|
|
context_columns=[],
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
)
|
|
job = await service.create_job(payload)
|
|
|
|
# Verify empty arrays persist
|
|
assert job.source_key_cols == []
|
|
assert job.target_key_cols == []
|
|
assert job.context_columns == []
|
|
|
|
# Re-fetch from DB
|
|
fetched = service.get_job(job.id)
|
|
assert fetched.source_key_cols == []
|
|
assert fetched.target_key_cols == []
|
|
assert fetched.context_columns == []
|
|
# endregion test_json_columns_empty_arrays
|
|
|
|
# region test_json_columns_nested_objects [C:2] [TYPE Function]
|
|
# @BRIEF Verify complex JSON structures persist correctly.
|
|
@pytest.mark.asyncio
|
|
async def test_json_columns_nested_objects(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
payload = TranslateJobCreate(
|
|
name="Complex JSON Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
source_key_cols=["id", "tenant_id", "region"],
|
|
target_key_cols=["id", "tenant_id", "region"],
|
|
context_columns=["description", "category", "metadata.tags"],
|
|
target_languages=["ru", "en", "de", "fr", "es"],
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
)
|
|
job = await service.create_job(payload)
|
|
|
|
# Verify complex arrays persist
|
|
assert job.source_key_cols == ["id", "tenant_id", "region"]
|
|
assert job.target_key_cols == ["id", "tenant_id", "region"]
|
|
assert job.context_columns == ["description", "category", "metadata.tags"]
|
|
assert job.target_languages == ["ru", "en", "de", "fr", "es"]
|
|
|
|
# Re-fetch and verify
|
|
fetched = service.get_job(job.id)
|
|
assert fetched.source_key_cols == ["id", "tenant_id", "region"]
|
|
assert fetched.target_languages == ["ru", "en", "de", "fr", "es"]
|
|
# endregion test_json_columns_nested_objects
|
|
|
|
# region test_json_columns_null_handling [C:2] [TYPE Function]
|
|
# @BRIEF Verify NULL JSON columns handled correctly.
|
|
@pytest.mark.asyncio
|
|
async def test_json_columns_null_handling(
|
|
self, db_session: Session, mock_config_manager: MagicMock
|
|
):
|
|
service = TranslateJobService(db_session, mock_config_manager, "test_user")
|
|
|
|
payload = TranslateJobCreate(
|
|
name="Null JSON Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
translation_column="name",
|
|
batch_size=50,
|
|
upsert_strategy="MERGE",
|
|
# No source_key_cols, target_key_cols, context_columns, target_languages
|
|
)
|
|
job = await service.create_job(payload)
|
|
|
|
# Verify NULL handling
|
|
assert job.source_key_cols is None or job.source_key_cols == []
|
|
assert job.target_key_cols is None or job.target_key_cols == []
|
|
assert job.context_columns is None or job.context_columns == []
|
|
assert job.target_languages is None or job.target_languages == []
|
|
# endregion test_json_columns_null_handling
|
|
|
|
# #endregion TestTranslateJobServiceJSONColumns
|
|
|
|
# #endregion TranslateServiceIntegrationTests
|