- 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
500 lines
20 KiB
Python
500 lines
20 KiB
Python
# #region DictionaryIntegrationTests [C:4] [TYPE Module] [SEMANTICS test, integration, dictionary, postgres, testcontainers]
|
|
# @BRIEF Integration tests for DictionaryCRUD and DictionaryEntryCRUD with real PostgreSQL.
|
|
# @RELATION BINDS_TO -> [DictionaryCRUD]
|
|
# @RELATION BINDS_TO -> [DictionaryEntryCRUD]
|
|
# @RELATION BINDS_TO -> [TerminologyDictionary]
|
|
# @RELATION BINDS_TO -> [DictionaryEntry]
|
|
# @TEST_CONTRACT DictionaryCRUD ->
|
|
# {
|
|
# invariants: [
|
|
# "create_dictionary persists with all fields",
|
|
# "update_dictionary modifies only specified fields",
|
|
# "delete_dictionary cascades to entries and job associations",
|
|
# "delete_dictionary blocked when attached to active jobs",
|
|
# "add_entry enforces uniqueness on (dict_id, source_term_norm, source_lang, target_lang)",
|
|
# "same term with different language pairs allowed"
|
|
# ]
|
|
# }
|
|
# @TEST_EDGE duplicate_entry -> ValueError on repeated normalized term
|
|
# @TEST_EDGE delete_active_job -> ValueError with active/scheduled message
|
|
# @TEST_EDGE same_term_different_lang_pair -> allowed (not duplicate)
|
|
# @TEST_EDGE invalid_regex -> ValueError on invalid regex pattern
|
|
# @TEST_INVARIANT unique_normalized -> VERIFIED_BY: [test_add_entry_duplicate_normalized, test_same_term_different_lang_pair_allowed]
|
|
# @TEST_INVARIANT cascade_integrity -> VERIFIED_BY: [test_delete_dictionary_cascades_to_entries]
|
|
|
|
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 src.models.translate import (
|
|
DictionaryEntry,
|
|
TerminologyDictionary,
|
|
TranslationJob,
|
|
TranslationJobDictionary,
|
|
)
|
|
from src.plugins.translate.dictionary import DictionaryManager
|
|
|
|
|
|
# #region TestDictionaryCRUDIntegration [C:3] [TYPE Class]
|
|
# @BRIEF Integration tests for dictionary-level CRUD with real PostgreSQL.
|
|
class TestDictionaryCRUDIntegration:
|
|
"""Verify dictionary CRUD operations with real PostgreSQL."""
|
|
|
|
# region test_create_dictionary_persists_all_fields [C:2] [TYPE Function]
|
|
# @BRIEF Verify dictionary creation persists all fields.
|
|
def test_create_dictionary_persists_all_fields(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(
|
|
db_session,
|
|
name="Finance Terms",
|
|
description="Financial terminology mappings",
|
|
created_by="test_user",
|
|
is_active=True,
|
|
)
|
|
|
|
assert dictionary.id is not None
|
|
assert dictionary.name == "Finance Terms"
|
|
assert dictionary.description == "Financial terminology mappings"
|
|
assert dictionary.created_by == "test_user"
|
|
assert dictionary.is_active is True
|
|
assert dictionary.created_at is not None
|
|
|
|
# Verify persisted
|
|
fetched = DictionaryManager.get_dictionary(db_session, dictionary.id)
|
|
assert fetched.id == dictionary.id
|
|
assert fetched.name == "Finance Terms"
|
|
# endregion test_create_dictionary_persists_all_fields
|
|
|
|
# region test_update_dictionary_partial_update [C:2] [TYPE Function]
|
|
# @BRIEF Verify partial update only modifies specified fields.
|
|
def test_update_dictionary_partial_update(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(
|
|
db_session, name="Original", description="Original desc", created_by="user1",
|
|
)
|
|
original_id = dictionary.id
|
|
original_created_by = dictionary.created_by
|
|
|
|
# Update only name
|
|
updated = DictionaryManager.update_dictionary(
|
|
db_session, original_id, name="Updated Name",
|
|
)
|
|
|
|
assert updated.id == original_id
|
|
assert updated.name == "Updated Name"
|
|
assert updated.description == "Original desc" # Unchanged
|
|
assert updated.created_by == original_created_by # Unchanged
|
|
# endregion test_update_dictionary_partial_update
|
|
|
|
# region test_delete_dictionary_cascades_to_entries [C:2] [TYPE Function]
|
|
# @BRIEF Verify deleting dictionary removes all entries.
|
|
def test_delete_dictionary_cascades_to_entries(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="To Delete")
|
|
|
|
# Add entries
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "hola",
|
|
source_language="en", target_language="es",
|
|
)
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "world", "mundo",
|
|
source_language="en", target_language="es",
|
|
)
|
|
|
|
# Verify entries exist
|
|
entries, total = DictionaryManager.list_entries(db_session, dictionary.id)
|
|
assert total == 2
|
|
|
|
# Delete dictionary
|
|
DictionaryManager.delete_dictionary(db_session, dictionary.id)
|
|
|
|
# Verify dictionary gone
|
|
with pytest.raises(ValueError, match="Dictionary not found"):
|
|
DictionaryManager.get_dictionary(db_session, dictionary.id)
|
|
|
|
# Verify entries gone (FK cascade)
|
|
remaining_entries = (
|
|
db_session.query(DictionaryEntry)
|
|
.filter(DictionaryEntry.dictionary_id == dictionary.id)
|
|
.all()
|
|
)
|
|
assert len(remaining_entries) == 0
|
|
# endregion test_delete_dictionary_cascades_to_entries
|
|
|
|
# region test_delete_dictionary_blocked_by_active_job [C:2] [TYPE Function]
|
|
# @BRIEF Verify deletion blocked when attached to active/scheduled jobs.
|
|
def test_delete_dictionary_blocked_by_active_job(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Protected")
|
|
|
|
# Create active job with association
|
|
job = TranslationJob(
|
|
name="Active Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
status="ACTIVE",
|
|
created_by="test_user",
|
|
)
|
|
db_session.add(job)
|
|
db_session.flush()
|
|
|
|
link = TranslationJobDictionary(job_id=job.id, dictionary_id=dictionary.id)
|
|
db_session.add(link)
|
|
db_session.commit()
|
|
|
|
# Attempt delete should fail
|
|
with pytest.raises(ValueError, match="active/scheduled"):
|
|
DictionaryManager.delete_dictionary(db_session, dictionary.id)
|
|
|
|
# Verify dictionary still exists
|
|
fetched = DictionaryManager.get_dictionary(db_session, dictionary.id)
|
|
assert fetched is not None
|
|
# endregion test_delete_dictionary_blocked_by_active_job
|
|
|
|
# region test_delete_dictionary_blocked_by_scheduled_job [C:2] [TYPE Function]
|
|
# @BRIEF Verify deletion blocked when attached to SCHEDULED jobs.
|
|
def test_delete_dictionary_blocked_by_scheduled_job(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Scheduled Protected")
|
|
|
|
# Create scheduled job with association
|
|
job = TranslationJob(
|
|
name="Scheduled Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
status="SCHEDULED",
|
|
created_by="test_user",
|
|
)
|
|
db_session.add(job)
|
|
db_session.flush()
|
|
|
|
link = TranslationJobDictionary(job_id=job.id, dictionary_id=dictionary.id)
|
|
db_session.add(link)
|
|
db_session.commit()
|
|
|
|
with pytest.raises(ValueError, match="active/scheduled"):
|
|
DictionaryManager.delete_dictionary(db_session, dictionary.id)
|
|
# endregion test_delete_dictionary_blocked_by_scheduled_job
|
|
|
|
# region test_delete_dictionary_allowed_with_completed_job [C:2] [TYPE Function]
|
|
# @BRIEF Verify deletion allowed when only completed/failed jobs reference it.
|
|
def test_delete_dictionary_allowed_with_completed_job(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Completed OK")
|
|
|
|
# Create completed job with association
|
|
job = TranslationJob(
|
|
name="Completed Job",
|
|
source_dialect="postgresql",
|
|
target_dialect="clickhouse",
|
|
status="COMPLETED",
|
|
created_by="test_user",
|
|
)
|
|
db_session.add(job)
|
|
db_session.flush()
|
|
|
|
link = TranslationJobDictionary(job_id=job.id, dictionary_id=dictionary.id)
|
|
db_session.add(link)
|
|
db_session.commit()
|
|
|
|
# Should succeed
|
|
DictionaryManager.delete_dictionary(db_session, dictionary.id)
|
|
|
|
with pytest.raises(ValueError, match="Dictionary not found"):
|
|
DictionaryManager.get_dictionary(db_session, dictionary.id)
|
|
# endregion test_delete_dictionary_allowed_with_completed_job
|
|
|
|
# region test_list_dictionaries_pagination [C:2] [TYPE Function]
|
|
# @BRIEF Verify paginated listing works correctly.
|
|
def test_list_dictionaries_pagination(self, db_session: Session):
|
|
# Create 7 dictionaries
|
|
for i in range(7):
|
|
DictionaryManager.create_dictionary(db_session, name=f"Dict {i}")
|
|
|
|
# Page 1
|
|
dicts, total = DictionaryManager.list_dictionaries(db_session, page=1, page_size=3)
|
|
assert total == 7
|
|
assert len(dicts) == 3
|
|
|
|
# Page 2
|
|
dicts, total = DictionaryManager.list_dictionaries(db_session, page=2, page_size=3)
|
|
assert total == 7
|
|
assert len(dicts) == 3
|
|
|
|
# Page 3
|
|
dicts, total = DictionaryManager.list_dictionaries(db_session, page=3, page_size=3)
|
|
assert total == 7
|
|
assert len(dicts) == 1
|
|
# endregion test_list_dictionaries_pagination
|
|
|
|
# #endregion TestDictionaryCRUDIntegration
|
|
|
|
|
|
# #region TestDictionaryEntryCRUDIntegration [C:3] [TYPE Class]
|
|
# @BRIEF Integration tests for entry-level CRUD with real PostgreSQL.
|
|
class TestDictionaryEntryCRUDIntegration:
|
|
"""Verify entry CRUD operations with real PostgreSQL."""
|
|
|
|
# region test_add_entry_persists_all_fields [C:2] [TYPE Function]
|
|
# @BRIEF Verify entry creation persists all fields.
|
|
def test_add_entry_persists_all_fields(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Test")
|
|
|
|
entry = DictionaryManager.add_entry(
|
|
db_session,
|
|
dictionary.id,
|
|
"hello world",
|
|
"привет мир",
|
|
source_language="en",
|
|
target_language="ru",
|
|
context_notes="greeting",
|
|
is_regex=False,
|
|
)
|
|
|
|
assert entry.id is not None
|
|
assert entry.dictionary_id == dictionary.id
|
|
assert entry.source_term == "hello world"
|
|
assert entry.target_term == "привет мир"
|
|
assert entry.source_language == "en"
|
|
assert entry.target_language == "ru"
|
|
assert entry.context_notes == "greeting"
|
|
assert entry.is_regex is False
|
|
|
|
# Verify persisted
|
|
entries, total = DictionaryManager.list_entries(db_session, dictionary.id)
|
|
assert total == 1
|
|
assert entries[0].source_term == "hello world"
|
|
# endregion test_add_entry_persists_all_fields
|
|
|
|
# region test_add_entry_duplicate_normalized [C:2] [TYPE Function]
|
|
# @BRIEF Verify duplicate detection uses normalized (lowercased) term.
|
|
def test_add_entry_duplicate_normalized(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Norm Test")
|
|
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "Hello", "Hola",
|
|
source_language="en", target_language="es",
|
|
)
|
|
|
|
# Same term, different case — should be duplicate
|
|
with pytest.raises(ValueError, match="already exists"):
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "HELLO", "Bonjour",
|
|
source_language="en", target_language="es",
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="already exists"):
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "Ciao",
|
|
source_language="en", target_language="es",
|
|
)
|
|
# endregion test_add_entry_duplicate_normalized
|
|
|
|
# region test_same_term_different_lang_pair_allowed [C:2] [TYPE Function]
|
|
# @BRIEF Verify same term with different language pair is allowed.
|
|
def test_same_term_different_lang_pair_allowed(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Multi Lang")
|
|
|
|
entry1 = DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "привет",
|
|
source_language="en", target_language="ru",
|
|
)
|
|
entry2 = DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "hallo",
|
|
source_language="en", target_language="de",
|
|
)
|
|
entry3 = DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "bonjour",
|
|
source_language="en", target_language="fr",
|
|
)
|
|
|
|
assert entry1.id != entry2.id != entry3.id
|
|
assert entry1.target_language == "ru"
|
|
assert entry2.target_language == "de"
|
|
assert entry3.target_language == "fr"
|
|
|
|
entries, total = DictionaryManager.list_entries(db_session, dictionary.id)
|
|
assert total == 3
|
|
# endregion test_same_term_different_lang_pair_allowed
|
|
|
|
# region test_same_term_different_dictionary_allowed [C:2] [TYPE Function]
|
|
# @BRIEF Verify same term in different dictionaries is allowed.
|
|
def test_same_term_different_dictionary_allowed(self, db_session: Session):
|
|
dict1 = DictionaryManager.create_dictionary(db_session, name="Dict 1")
|
|
dict2 = DictionaryManager.create_dictionary(db_session, name="Dict 2")
|
|
|
|
entry1 = DictionaryManager.add_entry(
|
|
db_session, dict1.id, "hello", "hola",
|
|
source_language="en", target_language="es",
|
|
)
|
|
entry2 = DictionaryManager.add_entry(
|
|
db_session, dict2.id, "hello", "bonjour",
|
|
source_language="en", target_language="fr",
|
|
)
|
|
|
|
assert entry1.id != entry2.id
|
|
assert entry1.dictionary_id == dict1.id
|
|
assert entry2.dictionary_id == dict2.id
|
|
# endregion test_same_term_different_dictionary_allowed
|
|
|
|
# region test_edit_entry_updates_fields [C:2] [TYPE Function]
|
|
# @BRIEF Verify editing entry updates specified fields.
|
|
def test_edit_entry_updates_fields(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Edit Test")
|
|
|
|
entry = DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "hola",
|
|
source_language="en", target_language="es",
|
|
context_notes="greeting",
|
|
)
|
|
|
|
updated = DictionaryManager.edit_entry(
|
|
db_session, entry.id,
|
|
target_term="HOLA!",
|
|
context_notes="formal greeting",
|
|
)
|
|
|
|
assert updated.target_term == "HOLA!"
|
|
assert updated.context_notes == "formal greeting"
|
|
assert updated.source_term == "hello" # Unchanged
|
|
# endregion test_edit_entry_updates_fields
|
|
|
|
# region test_edit_entry_source_term_checks_uniqueness [C:2] [TYPE Function]
|
|
# @BRIEF Verify editing source_term checks uniqueness constraint.
|
|
def test_edit_entry_source_term_checks_uniqueness(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Uniq Test")
|
|
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "hola",
|
|
source_language="en", target_language="es",
|
|
)
|
|
entry2 = DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "world", "mundo",
|
|
source_language="en", target_language="es",
|
|
)
|
|
|
|
# Try to change entry2's source to "hello" — should fail
|
|
with pytest.raises(ValueError, match="already exists"):
|
|
DictionaryManager.edit_entry(db_session, entry2.id, source_term="HELLO")
|
|
# endregion test_edit_entry_source_term_checks_uniqueness
|
|
|
|
# region test_delete_entry [C:2] [TYPE Function]
|
|
# @BRIEF Verify entry deletion.
|
|
def test_delete_entry(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Delete Test")
|
|
|
|
entry = DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "hola",
|
|
source_language="en", target_language="es",
|
|
)
|
|
|
|
DictionaryManager.delete_entry(db_session, entry.id)
|
|
|
|
entries, total = DictionaryManager.list_entries(db_session, dictionary.id)
|
|
assert total == 0
|
|
# endregion test_delete_entry
|
|
|
|
# region test_clear_entries [C:2] [TYPE Function]
|
|
# @BRIEF Verify clearing all entries for a dictionary.
|
|
def test_clear_entries(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Clear Test")
|
|
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "hola",
|
|
source_language="en", target_language="es",
|
|
)
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "world", "mundo",
|
|
source_language="en", target_language="es",
|
|
)
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "goodbye", "adiós",
|
|
source_language="en", target_language="es",
|
|
)
|
|
|
|
deleted = DictionaryManager.clear_entries(db_session, dictionary.id)
|
|
assert deleted == 3
|
|
|
|
entries, total = DictionaryManager.list_entries(db_session, dictionary.id)
|
|
assert total == 0
|
|
# endregion test_clear_entries
|
|
|
|
# region test_add_entry_regex_valid_pattern [C:2] [TYPE Function]
|
|
# @BRIEF Verify regex entry with valid pattern.
|
|
def test_add_entry_regex_valid_pattern(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Regex Valid")
|
|
|
|
entry = DictionaryManager.add_entry(
|
|
db_session, dictionary.id,
|
|
r"\d{3}-\d{2}-\d{4}",
|
|
"ID-MASKED",
|
|
source_language="en",
|
|
target_language="ru",
|
|
is_regex=True,
|
|
)
|
|
|
|
assert entry.is_regex is True
|
|
assert entry.source_term == r"\d{3}-\d{2}-\d{4}"
|
|
|
|
entries, total = DictionaryManager.list_entries(db_session, dictionary.id)
|
|
assert total == 1
|
|
assert entries[0].is_regex is True
|
|
# endregion test_add_entry_regex_valid_pattern
|
|
|
|
# region test_add_entry_regex_invalid_pattern_raises [C:2] [TYPE Function]
|
|
# @BRIEF Verify invalid regex pattern raises ValueError.
|
|
def test_add_entry_regex_invalid_pattern_raises(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Regex Invalid")
|
|
|
|
with pytest.raises(ValueError, match="Invalid regex pattern"):
|
|
DictionaryManager.add_entry(
|
|
db_session, dictionary.id,
|
|
r"[unclosed",
|
|
"broken",
|
|
source_language="en",
|
|
target_language="ru",
|
|
is_regex=True,
|
|
)
|
|
# endregion test_add_entry_regex_invalid_pattern_raises
|
|
|
|
# region test_edit_entry_toggle_regex [C:2] [TYPE Function]
|
|
# @BRIEF Verify toggling is_regex via edit_entry.
|
|
def test_edit_entry_toggle_regex(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Regex Toggle")
|
|
|
|
entry = DictionaryManager.add_entry(
|
|
db_session, dictionary.id, "hello", "hola",
|
|
source_language="en", target_language="es",
|
|
is_regex=False,
|
|
)
|
|
assert entry.is_regex is False
|
|
|
|
updated = DictionaryManager.edit_entry(db_session, entry.id, is_regex=True)
|
|
assert updated.is_regex is True
|
|
|
|
updated2 = DictionaryManager.edit_entry(db_session, entry.id, is_regex=False)
|
|
assert updated2.is_regex is False
|
|
# endregion test_edit_entry_toggle_regex
|
|
|
|
# region test_edit_entry_regex_source_validates_pattern [C:2] [TYPE Function]
|
|
# @BRIEF Verify editing source_term on regex entry validates pattern.
|
|
def test_edit_entry_regex_source_validates_pattern(self, db_session: Session):
|
|
dictionary = DictionaryManager.create_dictionary(db_session, name="Regex Edit Val")
|
|
|
|
entry = DictionaryManager.add_entry(
|
|
db_session, dictionary.id,
|
|
r"\d+",
|
|
"NUMBER",
|
|
source_language="en",
|
|
target_language="ru",
|
|
is_regex=True,
|
|
)
|
|
|
|
# Try to set invalid regex
|
|
with pytest.raises(ValueError, match="Invalid regex pattern"):
|
|
DictionaryManager.edit_entry(db_session, entry.id, source_term=r"[unclosed")
|
|
# endregion test_edit_entry_regex_source_validates_pattern
|
|
|
|
# #endregion TestDictionaryEntryCRUDIntegration
|
|
|
|
# #endregion DictionaryIntegrationTests
|