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.
247 lines
9.0 KiB
Python
247 lines
9.0 KiB
Python
# #region Test.ContextAwarePromptBuilder [C:2] [TYPE Module] [SEMANTICS test,prompt,builder,context]
|
|
# @BRIEF Tests for prompt_builder.py — ContextAwarePromptBuilder.
|
|
# @RELATION BINDS_TO -> [ContextAwarePromptBuilder]
|
|
|
|
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
|
|
|
|
from src.plugins.translate.prompt_builder import ContextAwarePromptBuilder
|
|
|
|
|
|
class TestRenderEntry:
|
|
"""ContextAwarePromptBuilder.render_entry."""
|
|
|
|
def test_basic_entry_dict(self):
|
|
"""Render a dict entry."""
|
|
entry = {"source_term": "hello", "target_term": "bonjour"}
|
|
line = ContextAwarePromptBuilder.render_entry(entry)
|
|
assert line == '"hello" -> "bonjour"'
|
|
|
|
def test_basic_entry_object(self):
|
|
"""Render an object entry."""
|
|
entry = MagicMock()
|
|
entry.source_term = "hello"
|
|
entry.target_term = "bonjour"
|
|
entry.has_context = False
|
|
entry.context_data = None
|
|
entry.usage_notes = None
|
|
|
|
line = ContextAwarePromptBuilder.render_entry(entry)
|
|
assert line == '"hello" -> "bonjour"'
|
|
|
|
def test_with_context(self):
|
|
"""Entry with context data."""
|
|
entry = MagicMock()
|
|
entry.source_term = "hello"
|
|
entry.target_term = "bonjour"
|
|
entry.has_context = True
|
|
entry.context_data = {"table": "users", "column": "name"}
|
|
entry.usage_notes = None
|
|
|
|
line = ContextAwarePromptBuilder.render_entry(entry)
|
|
assert "table=users" in line
|
|
assert "column=name" in line
|
|
assert "context:" in line
|
|
|
|
def test_with_context_dict_str(self):
|
|
"""Entry with context_data as JSON string."""
|
|
entry = MagicMock()
|
|
entry.source_term = "hello"
|
|
entry.target_term = "bonjour"
|
|
entry.has_context = True
|
|
entry.context_data = '{"table": "users"}'
|
|
entry.usage_notes = None
|
|
|
|
line = ContextAwarePromptBuilder.render_entry(entry)
|
|
assert "table=users" in line
|
|
|
|
def test_with_context_invalid_json(self):
|
|
"""Entry with context_data as non-JSON string."""
|
|
entry = MagicMock()
|
|
entry.source_term = "hello"
|
|
entry.target_term = "bonjour"
|
|
entry.has_context = True
|
|
entry.context_data = "plain string context"
|
|
entry.usage_notes = None
|
|
|
|
line = ContextAwarePromptBuilder.render_entry(entry)
|
|
assert "plain string context" in line
|
|
|
|
def test_with_context_string_direct(self):
|
|
"""Entry with context_data as string (not dict, not valid JSON)."""
|
|
entry = MagicMock()
|
|
entry.source_term = "hello"
|
|
entry.target_term = "bonjour"
|
|
entry.has_context = True
|
|
entry.context_data = "some raw context"
|
|
entry.usage_notes = None
|
|
|
|
line = ContextAwarePromptBuilder.render_entry(entry, priority=True)
|
|
assert "PRIORITY" in line
|
|
|
|
def test_usage_notes(self):
|
|
"""Entry with usage notes."""
|
|
entry = MagicMock()
|
|
entry.source_term = "hello"
|
|
entry.target_term = "bonjour"
|
|
entry.has_context = False
|
|
entry.context_data = None
|
|
entry.usage_notes = "Formal greeting"
|
|
|
|
line = ContextAwarePromptBuilder.render_entry(entry)
|
|
assert "Usage: Formal greeting" in line
|
|
|
|
def test_priority(self):
|
|
"""Priority entry gets prefix."""
|
|
entry = {"source_term": "hello", "target_term": "bonjour"}
|
|
line = ContextAwarePromptBuilder.render_entry(entry, priority=True)
|
|
assert line.startswith("# PRIORITY")
|
|
|
|
def test_context_truncation(self):
|
|
"""Very long context is truncated."""
|
|
entry = MagicMock()
|
|
entry.source_term = "hello"
|
|
entry.target_term = "bonjour"
|
|
entry.has_context = True
|
|
entry.context_data = {"long": "x" * 3000}
|
|
entry.usage_notes = None
|
|
|
|
line = ContextAwarePromptBuilder.render_entry(entry)
|
|
assert len(line) < 4000
|
|
assert "...[truncated]" in line
|
|
|
|
def test_context_items_dict_access(self):
|
|
"""Context items from dict-type entry."""
|
|
entry = {
|
|
"source_term": "hello", "target_term": "bonjour",
|
|
"has_context": True, "context_data": {"key": "value"},
|
|
"usage_notes": None,
|
|
}
|
|
line = ContextAwarePromptBuilder.render_entry(entry)
|
|
assert "key=value" in line
|
|
|
|
def test_context_data_json_array(self):
|
|
"""Context_data as JSON array string — parsed as non-dict (line 69)."""
|
|
entry = {
|
|
"source_term": "hello", "target_term": "bonjour",
|
|
"has_context": True, "context_data": '["val1", "val2"]',
|
|
"usage_notes": None,
|
|
}
|
|
line = ContextAwarePromptBuilder.render_entry(entry)
|
|
# Should use raw string since parsed JSON is not a dict
|
|
assert '["val1", "val2"]' in line
|
|
|
|
|
|
class TestComputeContextSimilarity:
|
|
"""ContextAwarePromptBuilder.compute_context_similarity."""
|
|
|
|
def test_exact_match(self):
|
|
"""Identical contexts => 1.0."""
|
|
ctx = {"a": "hello", "b": "world"}
|
|
sim = ContextAwarePromptBuilder.compute_context_similarity(ctx, ctx)
|
|
assert sim == 1.0
|
|
|
|
def test_no_match(self):
|
|
"""Disjoint contexts => 0.0."""
|
|
ctx1 = {"a": "hello"}
|
|
ctx2 = {"b": "world"}
|
|
sim = ContextAwarePromptBuilder.compute_context_similarity(ctx1, ctx2)
|
|
assert sim == 0.0
|
|
|
|
def test_partial_match(self):
|
|
"""Partial overlap yields value between 0 and 1."""
|
|
ctx1 = {"a": "hello", "b": "world"}
|
|
ctx2 = {"a": "hello", "c": "foo"}
|
|
sim = ContextAwarePromptBuilder.compute_context_similarity(ctx1, ctx2)
|
|
# intersection = {"hello"}, union = {"hello", "world", "foo"} => 1/3
|
|
assert 0.33 < sim < 0.34
|
|
|
|
def test_none_entry_context(self):
|
|
"""None entry_context => 0.0."""
|
|
sim = ContextAwarePromptBuilder.compute_context_similarity(None, {"a": "b"})
|
|
assert sim == 0.0
|
|
|
|
def test_none_row_context(self):
|
|
"""None row_context => 0.0."""
|
|
sim = ContextAwarePromptBuilder.compute_context_similarity({"a": "b"}, None)
|
|
assert sim == 0.0
|
|
|
|
def test_empty_values(self):
|
|
"""Context with all None values => 0.0."""
|
|
ctx1 = {"a": None, "b": None}
|
|
ctx2 = {"a": "hello"}
|
|
sim = ContextAwarePromptBuilder.compute_context_similarity(ctx1, ctx2)
|
|
assert sim == 0.0
|
|
|
|
def test_case_insensitive(self):
|
|
"""Values are lowercased."""
|
|
ctx1 = {"a": "Hello"}
|
|
ctx2 = {"a": "hello"}
|
|
sim = ContextAwarePromptBuilder.compute_context_similarity(ctx1, ctx2)
|
|
assert sim == 1.0
|
|
|
|
|
|
class TestBuildContextEntries:
|
|
"""ContextAwarePromptBuilder.build_context_entries."""
|
|
|
|
def test_basic_build(self):
|
|
"""Build list of rendered entries sorted by priority."""
|
|
entries = [
|
|
{"source_term": "hello", "target_term": "bonjour",
|
|
"has_context": False, "context_data": None, "usage_notes": None},
|
|
{"source_term": "world", "target_term": "monde",
|
|
"has_context": True, "context_data": {"table": "users"},
|
|
"usage_notes": None},
|
|
]
|
|
result = ContextAwarePromptBuilder.build_context_entries(entries)
|
|
assert len(result) == 2
|
|
|
|
def test_priority_by_similarity(self):
|
|
"""Entries matching row_context get priority prefix."""
|
|
entries = [
|
|
{"source_term": "hello", "target_term": "bonjour",
|
|
"has_context": True, "context_data": {"table": "users"},
|
|
"usage_notes": None},
|
|
{"source_term": "world", "target_term": "monde",
|
|
"has_context": True, "context_data": {"table": "orders"},
|
|
"usage_notes": None},
|
|
]
|
|
row_context = {"table": "users"}
|
|
result = ContextAwarePromptBuilder.build_context_entries(entries, row_context)
|
|
# First entry matches and gets priority
|
|
assert "# PRIORITY" in result[0]
|
|
assert "# PRIORITY" not in result[1]
|
|
|
|
def test_no_row_context(self):
|
|
"""No row_context => no priority."""
|
|
entries = [
|
|
{"source_term": "hello", "target_term": "bonjour",
|
|
"has_context": False, "context_data": None, "usage_notes": None},
|
|
]
|
|
result = ContextAwarePromptBuilder.build_context_entries(entries)
|
|
assert len(result) == 1
|
|
assert "# PRIORITY" not in result[0]
|
|
|
|
def test_empty_entries(self):
|
|
"""Empty entries returns empty list."""
|
|
result = ContextAwarePromptBuilder.build_context_entries([])
|
|
assert result == []
|
|
|
|
def test_object_entries(self):
|
|
"""Entries as objects work too."""
|
|
entry = MagicMock()
|
|
entry.source_term = "hello"
|
|
entry.target_term = "bonjour"
|
|
entry.has_context = True
|
|
entry.context_data = {"table": "users"}
|
|
entry.usage_notes = None
|
|
|
|
result = ContextAwarePromptBuilder.build_context_entries([entry], {"table": "users"})
|
|
assert len(result) == 1
|
|
assert "# PRIORITY" in result[0]
|
|
# #endregion Test.ContextAwarePromptBuilder
|