test: add 7 more test modules — assistant cmd parser, history, dispatch, admin routes, dataset review, maintenance + semantic resolver

This commit is contained in:
2026-06-15 16:06:18 +03:00
parent f75c15dbc6
commit fc7e6c3c15
9 changed files with 3241 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
# #region Test.SessionEventLogger [C:2] [TYPE Module] [SEMANTICS test,dataset,event,logger,session,audit]
# @BRIEF Tests for event_logger.py — session event logging and persistence.
# @RELATION BINDS_TO -> [SessionEventLoggerModule]
# @TEST_EDGE: log_event -> Persists event payload correctly
# @TEST_EDGE: log_event_validation -> Empty fields raise ValueError
# @TEST_EDGE: log_event_with_details -> Event details stored
# @TEST_EDGE: log_for_session -> Convenience wrapper from session object
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import MagicMock, patch
import pytest
class TestSessionEventPayload:
"""SessionEventPayload — dataclass contract."""
def test_payload_creation(self):
from src.services.dataset_review.event_logger import SessionEventPayload
payload = SessionEventPayload(
session_id="sess-1",
actor_user_id="user-1",
event_type="session_started",
event_summary="Session created",
current_phase="RECOVERY",
readiness_state="IMPORTING",
event_details={"key": "val"},
)
assert payload.session_id == "sess-1"
assert payload.event_type == "session_started"
assert payload.event_details == {"key": "val"}
class TestSessionEventLoggerLogEvent:
"""SessionEventLogger.log_event — persist event."""
@pytest.fixture
def db(self):
return MagicMock()
@pytest.fixture
def logger(self, db):
from src.services.dataset_review.event_logger import SessionEventLogger
return SessionEventLogger(db)
def test_log_event_success(self, logger, db):
from src.services.dataset_review.event_logger import SessionEventPayload
# Mock SessionEvent
mock_event = MagicMock()
mock_event.session_event_id = "evt-1"
mock_event.event_type = "session_started"
db.add.return_value = None
db.refresh.return_value = None
payload = SessionEventPayload(
session_id="sess-1",
actor_user_id="user-1",
event_type="session_started",
event_summary="Session created",
current_phase="RECOVERY",
readiness_state="IMPORTING",
event_details={"source": "superset"},
)
result = logger.log_event(payload)
db.add.assert_called_once()
db.commit.assert_called_once()
db.refresh.assert_called_once()
def test_log_event_minimal(self, logger, db):
from src.services.dataset_review.event_logger import SessionEventPayload
payload = SessionEventPayload(
session_id="sess-1",
actor_user_id="user-1",
event_type="update",
event_summary="Updated",
)
result = logger.log_event(payload)
db.add.assert_called_once()
db.commit.assert_called_once()
def test_empty_session_id_raises(self, logger, db):
from src.services.dataset_review.event_logger import SessionEventPayload
payload = SessionEventPayload(
session_id="",
actor_user_id="user-1",
event_type="test",
event_summary="test",
)
with pytest.raises(ValueError, match="session_id"):
logger.log_event(payload)
db.add.assert_not_called()
def test_empty_actor_user_id_raises(self, logger, db):
from src.services.dataset_review.event_logger import SessionEventPayload
payload = SessionEventPayload(
session_id="sess-1",
actor_user_id="",
event_type="test",
event_summary="test",
)
with pytest.raises(ValueError, match="actor_user_id"):
logger.log_event(payload)
db.add.assert_not_called()
def test_empty_event_type_raises(self, logger, db):
from src.services.dataset_review.event_logger import SessionEventPayload
payload = SessionEventPayload(
session_id="sess-1",
actor_user_id="user-1",
event_type="",
event_summary="test",
)
with pytest.raises(ValueError, match="event_type"):
logger.log_event(payload)
db.add.assert_not_called()
def test_empty_event_summary_raises(self, logger, db):
from src.services.dataset_review.event_logger import SessionEventPayload
payload = SessionEventPayload(
session_id="sess-1",
actor_user_id="user-1",
event_type="test",
event_summary="",
)
with pytest.raises(ValueError, match="event_summary"):
logger.log_event(payload)
db.add.assert_not_called()
def test_event_with_complex_details(self, logger, db):
from src.services.dataset_review.event_logger import SessionEventPayload
payload = SessionEventPayload(
session_id="sess-1",
actor_user_id="user-1",
event_type="mapping_approved",
event_summary="Mappings approved by assistant",
event_details={
"mapping_ids": ["m-1", "m-2"],
"count": 2,
"nested": {"key": "value"},
},
)
result = logger.log_event(payload)
db.add.assert_called_once()
# Verify the event_details dict
added = db.add.call_args[0][0]
assert added.event_details["mapping_ids"] == ["m-1", "m-2"]
class TestSessionEventLoggerLogForSession:
"""SessionEventLogger.log_for_session — convenience wrapper."""
def test_log_for_session(self):
from src.services.dataset_review.event_logger import SessionEventLogger, SessionEventPayload
from src.models.dataset_review import DatasetReviewSession, SessionPhase, ReadinessState
db = MagicMock()
session = MagicMock(spec=DatasetReviewSession)
session.session_id = "sess-1"
session.current_phase = MagicMock(value="review")
session.readiness_state = MagicMock(value="review_ready")
logger = SessionEventLogger(db)
with patch.object(logger, "log_event") as mock_log:
mock_log.return_value = MagicMock(session_event_id="evt-1")
result = logger.log_for_session(
session,
actor_user_id="user-1",
event_type="preview_generated",
event_summary="Preview ready",
event_details={"preview_id": "pv-1"},
)
mock_log.assert_called_once()
payload = mock_log.call_args[0][0]
assert payload.session_id == "sess-1"
assert payload.event_type == "preview_generated"
assert payload.current_phase == "review"
assert payload.readiness_state == "review_ready"
def test_log_for_session_no_details(self):
from src.services.dataset_review.event_logger import SessionEventLogger
from src.models.dataset_review import DatasetReviewSession
db = MagicMock()
session = MagicMock(spec=DatasetReviewSession)
session.session_id = "sess-1"
session.current_phase = None
session.readiness_state = None
logger = SessionEventLogger(db)
with patch.object(logger, "log_event") as mock_log:
mock_log.return_value = MagicMock(session_event_id="evt-1")
logger.log_for_session(
session,
actor_user_id="user-1",
event_type="test",
event_summary="test",
)
payload = mock_log.call_args[0][0]
assert payload.current_phase is None
assert payload.readiness_state is None
# #endregion Test.SessionEventLogger

View File

@@ -0,0 +1,376 @@
# #region Test.SemanticSourceResolver [C:2] [TYPE Module] [SEMANTICS test,dataset,semantic,resolver,mapping]
# @BRIEF Tests for semantic_resolver.py — semantic candidate resolution, ranking, and conflict detection.
# @RELATION BINDS_TO -> [SemanticSourceResolver]
# @TEST_EDGE: resolve_from_file -> Returns basic resolution result
# @TEST_EDGE: resolve_from_dictionary -> Exact, fuzzy, and unresolved field matching
# @TEST_EDGE: resolve_from_dictionary_validation -> Missing source_ref or rows raises ValueError
# @TEST_EDGE: resolve_from_reference_dataset -> Returns basic result
# @TEST_EDGE: rank_candidates -> Confidence ordering
# @TEST_EDGE: detect_conflicts -> Multi-candidate detection
# @TEST_EDGE: apply_field_decision -> Field merging
# @TEST_EDGE: propagate_source_version_update -> Version propagation with locks
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import MagicMock, patch
import pytest
class TestSemanticSourceResolverResolveFromFile:
"""SemanticSourceResolver.resolve_from_file — basic file resolution."""
def test_resolve_from_file(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
result = resolver.resolve_from_file(
{"source_ref": "upload.csv"},
[{"field_name": "region"}],
)
assert result.source_ref == "uploaded_file"
class TestSemanticSourceResolverResolveFromDictionary:
"""SemanticSourceResolver.resolve_from_dictionary — dictionary resolution."""
def test_exact_match(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
source_payload = {
"source_ref": "dict_1",
"rows": [
{"column_name": "region", "verbose_name": "Region", "description": "Geographic region"},
{"column_name": "product", "verbose_name": "Product", "description": "Product name"},
],
}
fields = [{"field_name": "region"}]
result = resolver.resolve_from_dictionary(source_payload, fields)
assert len(result.resolved_fields) == 1
assert result.resolved_fields[0]["field_name"] == "region"
assert result.resolved_fields[0]["provenance"] == "dictionary_exact"
assert result.resolved_fields[0]["needs_review"] is False
def test_fuzzy_match(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
source_payload = {
"source_ref": "dict_1",
"rows": [
{"column_name": "region_code", "verbose_name": "Region Code", "description": "Region code"},
],
}
fields = [{"field_name": "region"}]
result = resolver.resolve_from_dictionary(source_payload, fields)
assert len(result.resolved_fields) == 1
assert result.resolved_fields[0]["provenance"] == "fuzzy_inferred"
assert result.resolved_fields[0]["needs_review"] is True
def test_unresolved_field(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
source_payload = {
"source_ref": "dict_1",
"rows": [
{"column_name": "product", "verbose_name": "Product"},
],
}
fields = [{"field_name": "zzz_nonexistent"}]
result = resolver.resolve_from_dictionary(source_payload, fields)
assert len(result.resolved_fields) == 1
assert result.resolved_fields[0]["status"] == "unresolved"
assert result.resolved_fields[0]["provenance"] == "unresolved"
assert result.unresolved_fields == ["zzz_nonexistent"]
assert result.partial_recovery is True
def test_missing_source_ref_raises_value_error(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
with pytest.raises(ValueError, match="source_ref"):
resolver.resolve_from_dictionary({"rows": [{"column_name": "x"}]}, [{"field_name": "x"}])
def test_empty_rows_raises_value_error(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
with pytest.raises(ValueError, match="rows"):
resolver.resolve_from_dictionary({"source_ref": "dict_1", "rows": []}, [{"field_name": "x"}])
def test_non_list_rows_raises_value_error(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
with pytest.raises(ValueError, match="rows"):
resolver.resolve_from_dictionary({"source_ref": "dict_1", "rows": "not_a_list"}, [{"field_name": "x"}])
def test_locked_field_preserved(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
source_payload = {
"source_ref": "dict_1",
"rows": [{"column_name": "region", "verbose_name": "Region"}],
}
fields = [{"field_name": "region", "is_locked": True}]
result = resolver.resolve_from_dictionary(source_payload, fields)
assert len(result.resolved_fields) == 1
assert result.resolved_fields[0]["status"] == "preserved_manual"
assert result.resolved_fields[0]["is_locked"] is True
def test_skip_empty_field_name(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
source_payload = {
"source_ref": "dict_1",
"rows": [{"column_name": "region", "verbose_name": "Region"}],
}
fields = [{"field_name": ""}, {"field_name": "region"}]
result = resolver.resolve_from_dictionary(source_payload, fields)
assert len(result.resolved_fields) == 1
assert result.resolved_fields[0]["field_name"] == "region"
def test_mixed_resolved_and_unresolved(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
source_payload = {
"source_ref": "dict_1",
"rows": [{"column_name": "region", "verbose_name": "Region"}],
}
fields = [
{"field_name": "region"},
{"field_name": "product"},
{"field_name": "sales_amount"},
]
result = resolver.resolve_from_dictionary(source_payload, fields)
assert len(result.resolved_fields) == 3
region_field = next(f for f in result.resolved_fields if f["field_name"] == "region")
assert region_field["provenance"] == "dictionary_exact"
product_field = next(f for f in result.resolved_fields if f["field_name"] == "product")
assert product_field["status"] == "unresolved"
assert len(result.unresolved_fields) == 2
class TestSemanticSourceResolverResolveFromReference:
"""SemanticSourceResolver.resolve_from_reference_dataset — reference dataset."""
def test_resolve_from_reference(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
result = resolver.resolve_from_reference_dataset(
{"source_ref": "ref_1"},
[{"field_name": "region"}],
)
assert result.source_ref == "reference_dataset"
class TestSemanticSourceResolverRankCandidates:
"""SemanticSourceResolver.rank_candidates — confidence ordering."""
def test_exact_before_fuzzy(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
from src.models.dataset_review import CandidateMatchType
resolver = SemanticSourceResolver()
candidates = [
{"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.95, "candidate_rank": 999},
{"match_type": CandidateMatchType.EXACT.value, "confidence_score": 1.0, "candidate_rank": 999},
]
ranked = resolver.rank_candidates(candidates)
assert ranked[0]["match_type"] == CandidateMatchType.EXACT.value
assert ranked[1]["match_type"] == CandidateMatchType.FUZZY.value
def test_higher_confidence_first(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
from src.models.dataset_review import CandidateMatchType
resolver = SemanticSourceResolver()
candidates = [
{"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.8, "candidate_rank": 999},
{"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.9, "candidate_rank": 999},
]
ranked = resolver.rank_candidates(candidates)
assert ranked[0]["confidence_score"] == 0.9
assert ranked[1]["confidence_score"] == 0.8
def test_ranks_assigned(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
from src.models.dataset_review import CandidateMatchType
resolver = SemanticSourceResolver()
candidates = [
{"match_type": CandidateMatchType.EXACT.value, "confidence_score": 1.0, "candidate_rank": 999},
{"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.9, "candidate_rank": 999},
{"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.8, "candidate_rank": 999},
]
ranked = resolver.rank_candidates(candidates)
assert ranked[0]["candidate_rank"] == 1
assert ranked[1]["candidate_rank"] == 2
assert ranked[2]["candidate_rank"] == 3
class TestSemanticSourceResolverDetectConflicts:
"""SemanticSourceResolver.detect_conflicts — multi-candidate detection."""
def test_single_candidate_no_conflict(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
assert resolver.detect_conflicts([{"rank": 1}]) is False
def test_multiple_candidates_conflict(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
assert resolver.detect_conflicts([{"rank": 1}, {"rank": 2}]) is True
class TestSemanticSourceResolverApplyFieldDecision:
"""SemanticSourceResolver.apply_field_decision — field merging."""
def test_merge_fields(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
field_state = {"field_name": "region", "status": "unresolved"}
decision = {"status": "resolved", "verbose_name": "Region"}
merged = resolver.apply_field_decision(field_state, decision)
assert merged["field_name"] == "region"
assert merged["status"] == "resolved"
assert merged["verbose_name"] == "Region"
class TestSemanticSourceResolverPropagateSourceVersion:
"""SemanticSourceResolver.propagate_source_version_update — version propagation."""
def test_propagate_success(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
from src.models.dataset_review import SemanticSource
resolver = SemanticSourceResolver()
source = SemanticSource(source_id="src-1", source_version="v2")
field_mock = MagicMock()
field_mock.source_id = "src-1"
field_mock.is_locked = False
field_mock.provenance = "dictionary_exact"
field_mock.source_version = "v1"
field_mock.needs_review = False
field_mock.has_conflict = False
result = resolver.propagate_source_version_update(source, [field_mock])
assert result["propagated"] == 1
assert result["preserved_locked"] == 0
assert result["untouched"] == 0
assert field_mock.source_version == "v2"
assert field_mock.needs_review is True
def test_preserve_locked(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
from src.models.dataset_review import SemanticSource
resolver = SemanticSourceResolver()
source = SemanticSource(source_id="src-1", source_version="v2")
field_mock = MagicMock()
field_mock.source_id = "src-1"
field_mock.is_locked = True
field_mock.provenance = "manual_override"
field_mock.source_version = "v1"
result = resolver.propagate_source_version_update(source, [field_mock])
assert result["propagated"] == 0
assert result["preserved_locked"] == 1
def test_untouched_different_source(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
from src.models.dataset_review import SemanticSource
resolver = SemanticSourceResolver()
source = SemanticSource(source_id="src-1", source_version="v2")
field_mock = MagicMock()
field_mock.source_id = "src-2"
field_mock.is_locked = False
field_mock.provenance = "dictionary_exact"
result = resolver.propagate_source_version_update(source, [field_mock])
assert result["propagated"] == 0
assert result["untouched"] == 1
def test_missing_source_metadata_raises(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
from src.models.dataset_review import SemanticSource
resolver = SemanticSourceResolver()
source = SemanticSource(source_id="src-1", source_version="")
with pytest.raises(ValueError, match="source_version"):
resolver.propagate_source_version_update(source, [])
class TestNormalizeDictionaryRow:
"""SemanticSourceResolver._normalize_dictionary_row — row normalization."""
def test_field_name_variants(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
r1 = resolver._normalize_dictionary_row({"column_name": "col1", "verbose_name": "Col 1"})
assert r1["field_name"] == "col1"
r2 = resolver._normalize_dictionary_row({"name": "named_field"})
assert r2["field_name"] == "named_field"
r3 = resolver._normalize_dictionary_row({"field": "field_val"})
assert r3["field_name"] == "field_val"
r4 = resolver._normalize_dictionary_row({"field_name": "direct"})
assert r4["field_name"] == "direct"
def test_verbose_name_fallback(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
r1 = resolver._normalize_dictionary_row({"field_name": "a", "label": "Label A"})
assert r1["verbose_name"] == "Label A"
r2 = resolver._normalize_dictionary_row({"field_name": "b"})
assert r2.get("verbose_name") is None
class TestMatchPriority:
"""SemanticSourceResolver._match_priority — priority encoding."""
def test_priority_ordering(self):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
from src.models.dataset_review import CandidateMatchType
resolver = SemanticSourceResolver()
assert resolver._match_priority(CandidateMatchType.EXACT.value) == 0
assert resolver._match_priority(CandidateMatchType.FUZZY.value) == 2
assert resolver._match_priority(CandidateMatchType.GENERATED.value) == 3
assert resolver._match_priority(None) == 99
assert resolver._match_priority("UNKNOWN") == 99
class TestNormalizeKey:
"""SemanticSourceResolver._normalize_key — key normalization."""
@pytest.mark.parametrize("input_val,expected", [
("Hello World", "helloworld"),
("region_code", "region_code"),
("UPPER_CASE", "uppercase"),
(" with spaces ", "withspaces"),
("special!@#chars", "specialchars"),
("", ""),
])
def test_normalize_key(self, input_val, expected):
from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
resolver = SemanticSourceResolver()
assert resolver._normalize_key(input_val) == expected
# #endregion Test.SemanticSourceResolver