Files
ss-tools/backend/tests/api/test_dataset_review_deps_unit.py

1279 lines
47 KiB
Python

#region Test.DatasetReview.DepsUnit [C:2] [TYPE Module] [SEMANTICS test,dataset,review,dependencies,unit]
# @BRIEF Direct unit tests for every private function in dataset_review_pkg/_dependencies.py.
# @RELATION BINDS_TO -> [DatasetReviewDependencies]
# @TEST_CONTRACT [PrivateDependencyFunctions] -> [CorrectErrorBehavior | CorrectDTOMapping]
# @TEST_EDGE: flag_disabled -> Feature guards raise 404 when dataset_review feature is off
# @TEST_EDGE: subflag_disabled -> Feature guards raise 404 when sub-feature flag is off
# @TEST_EDGE: flag_enabled -> Feature guards return True when all flags are on
# @TEST_EDGE: missing_header -> _require_session_version_header rejects missing X-Session-Version
# @TEST_EDGE: version_conflict_exc -> _build_session_version_conflict_http_exception returns 409
# @TEST_EDGE: enforce_version_pass -> _enforce_session_version passes through when OK
# @TEST_EDGE: enforce_version_fail -> _enforce_session_version raises 409 on conflict
# @TEST_EDGE: owned_session_404 -> _get_owned_session_or_404 raises 404
# @TEST_EDGE: owner_scope_forbidden -> _require_owner_mutation_scope raises 403 for non-owner
# @TEST_EDGE: prepare_mutation_pass -> _prepare_owned_session_mutation resolves + enforces
# @TEST_EDGE: session_no_clarification -> _get_latest_clarification_session_or_404 raises 404
# @TEST_EDGE: owned_mapping_404 -> _get_owned_mapping_or_404 raises 404
# @TEST_EDGE: owned_field_404 -> _get_owned_field_or_404 raises 404
# @TEST_EDGE: candidate_provenance_mapping -> _map_candidate_provenance covers all match types
# @TEST_EDGE: resolve_source_version -> _resolve_candidate_source_version handles edge cases
# @TEST_EDGE: update_semantic_field_no_input -> 400 when neither candidate_id nor manual fields provided
# @TEST_EDGE: update_semantic_field_invalid_candidate -> 404 when candidate_id not found
# @TEST_EDGE: sql_lab_redirect_missing_config -> 500 when URL or session ref missing
# @TEST_EDGE: export_format_validation -> _build_documentation_export/_build_validation_export handle markdown/json
# @TEST_EDGE: serialize_clarification_question_none -> _serialize_clarification_question_payload returns None
from __future__ import annotations
from datetime import UTC, datetime
from pathlib import Path
import sys
from typing import Any, cast
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
import pytest
from fastapi import HTTPException, Header, status
from unittest.mock import MagicMock, patch
from src.api.routes.dataset_review_pkg._dependencies import (
ApproveMappingRequest,
BatchApproveSemanticItemRequest,
ClarificationAnswerRequest,
FeedbackRequest,
FieldSemanticUpdateRequest,
StartSessionRequest,
UpdateSessionRequest,
_build_documentation_export,
_build_session_version_conflict_http_exception,
_build_sql_lab_redirect_url,
_build_validation_export,
_commit_owned_session_mutation,
_enforce_session_version,
_get_latest_clarification_session_or_404,
_get_clarification_engine,
_get_orchestrator,
_get_repository,
_get_owned_field_or_404,
_get_owned_mapping_or_404,
_get_owned_session_or_404,
_map_candidate_provenance,
_prepare_owned_session_mutation,
_record_session_event,
_require_auto_review_flag,
_require_clarification_flag,
_require_execution_flag,
_require_owner_mutation_scope,
_require_session_version_header,
_resolve_candidate_source_version,
_serialize_clarification_question_payload,
_serialize_clarification_state,
_serialize_empty_clarification_state,
_serialize_execution_mapping,
_serialize_preview,
_serialize_run_context,
_serialize_semantic_field,
_serialize_session_detail,
_serialize_session_summary,
_update_semantic_field_state,
)
from src.models.dataset_review import (
ApprovalState,
ArtifactFormat,
CandidateMatchType,
CandidateStatus,
ClarificationStatus,
FieldKind,
FieldProvenance,
LaunchStatus,
MappingMethod,
PreviewStatus,
QuestionState,
ReadinessState,
RecommendedAction,
SessionPhase,
SessionStatus,
)
from src.schemas.dataset_review import (
CompiledPreviewDto,
ExecutionMappingDto,
SessionDetail,
SessionSummary,
)
from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionVersionConflictError,
)
# ── Shared fixtures ────────────────────────────────────────────────────
def _make_config_manager_mock(*, dataset_review=True, ff_auto=True, ff_clarification=True, ff_execution=True):
"""Build a mock config_manager with specified feature flags."""
cm = MagicMock()
config = MagicMock()
settings = MagicMock()
features = MagicMock()
features.dataset_review = dataset_review
settings.features = features
settings.ff_dataset_auto_review = ff_auto
settings.ff_dataset_clarification = ff_clarification
settings.ff_dataset_execution = ff_execution
config.settings = settings
cm.get_config.return_value = config
return cm
def _make_session_mock(user_id="user-1", version=5):
"""Build a mock DatasetReviewSession with key attributes."""
s = MagicMock()
s.session_id = "sess-1"
s.user_id = user_id
s.version = version
s.dataset_ref = "public.sales"
s.environment_id = "env-1"
s.status = SessionStatus.ACTIVE
s.readiness_state = ReadinessState.REVIEW_READY
s.recommended_action = RecommendedAction.REVIEW_DOCUMENTATION
s.current_phase = SessionPhase.REVIEW
s.created_at = datetime.now(UTC)
s.updated_at = datetime.now(UTC)
s.last_activity_at = datetime.now(UTC)
s.profile = None
s.findings = []
s.collaborators = []
s.semantic_sources = []
s.semantic_fields = []
s.imported_filters = []
s.template_variables = []
s.execution_mappings = []
s.clarification_sessions = []
s.previews = []
s.run_contexts = []
s.source_kind = "superset_link"
s.source_input = "http://superset.local/dashboard/10"
s.dataset_id = 42
s.dashboard_id = 10
return s
def _make_repository_mock(session=None, commit_version_conflict=False):
repo = MagicMock()
repo.load_session_detail.return_value = session
repo.require_session_version.side_effect = lambda s, v: s
repo.commit_session_mutation.side_effect = (
DatasetReviewSessionVersionConflictError("sess-1", 5, 6)
if commit_version_conflict
else lambda s, **kw: s
)
repo.db = MagicMock()
repo.event_logger = MagicMock()
repo.event_logger.log_for_session.return_value = MagicMock(session_event_id="evt-1")
return repo
# ── DTO tests ──────────────────────────────────────────────────────────
#region test_start_session_request_validation [C:2]
# @BRIEF StartSessionRequest validates source_kind pattern and required fields.
def test_start_session_request_validation():
req = StartSessionRequest(
source_kind="superset_link",
source_input="http://example.com/dashboard/1",
environment_id="env-1",
)
assert req.source_kind == "superset_link"
assert req.source_input == "http://example.com/dashboard/1"
assert req.environment_id == "env-1"
# Invalid pattern
with pytest.raises(Exception):
StartSessionRequest(
source_kind="invalid_kind",
source_input="test",
environment_id="env-1",
)
#endregion
#region test_update_session_request [C:2]
# @BRIEF UpdateSessionRequest accepts status and optional note.
def test_update_session_request():
req = UpdateSessionRequest(status=SessionStatus.PAUSED, note="Testing pause")
assert req.status == SessionStatus.PAUSED
assert req.note == "Testing pause"
#endregion
#region test_clarification_answer_request [C:2]
# @BRIEF ClarificationAnswerRequest validates question_id and answer fields.
def test_clarification_answer_request():
from src.models.dataset_review import AnswerKind
req = ClarificationAnswerRequest(
question_id="q-1",
answer_kind=AnswerKind.SELECTED,
answer_value="my answer",
)
assert req.question_id == "q-1"
assert req.answer_kind == AnswerKind.SELECTED
assert req.answer_value == "my answer"
#endregion
#region test_feedback_request [C:2]
# @BRIEF FeedbackRequest validates pattern (up|down).
def test_feedback_request():
assert FeedbackRequest(feedback="up").feedback == "up"
assert FeedbackRequest(feedback="down").feedback == "down"
with pytest.raises(Exception):
FeedbackRequest(feedback="sideways")
#endregion
#region test_field_semantic_update_request [C:2]
# @BRIEF FieldSemanticUpdateRequest accepts various override patterns.
def test_field_semantic_update_request():
req = FieldSemanticUpdateRequest(candidate_id="cand-1", lock_field=True)
assert req.candidate_id == "cand-1"
assert req.lock_field is True
req2 = FieldSemanticUpdateRequest(verbose_name="Custom Name", description="Custom desc")
assert req2.verbose_name == "Custom Name"
assert req2.description == "Custom desc"
#endregion
#region test_approve_mapping_request [C:2]
# @BRIEF ApproveMappingRequest with and without approval note.
def test_approve_mapping_request():
req = ApproveMappingRequest(approval_note="Reviewed and approved")
assert req.approval_note == "Reviewed and approved"
assert ApproveMappingRequest().approval_note is None
#endregion
#region test_batch_approve_semantic_item_request [C:2]
# @BRIEF BatchApproveSemanticItemRequest requires field_id and candidate_id.
def test_batch_approve_semantic_item_request():
req = BatchApproveSemanticItemRequest(field_id="f-1", candidate_id="cand-1", lock_field=True)
assert req.field_id == "f-1"
assert req.candidate_id == "cand-1"
assert req.lock_field is True
with pytest.raises(Exception):
BatchApproveSemanticItemRequest(field_id="", candidate_id="")
#endregion
# ── Feature flag guards ────────────────────────────────────────────────
#region test_require_auto_review_flag_enabled [C:2]
# @BRIEF Returns True when all flags are on.
def test_require_auto_review_flag_enabled():
cm = _make_config_manager_mock(dataset_review=True, ff_auto=True)
result = _require_auto_review_flag(config_manager=cm)
assert result is True
#endregion
#region test_require_auto_review_flag_feature_disabled [C:2]
# @BRIEF Raises 404 when dataset_review feature is disabled.
def test_require_auto_review_flag_feature_disabled():
cm = _make_config_manager_mock(dataset_review=False, ff_auto=True)
with pytest.raises(HTTPException) as exc:
_require_auto_review_flag(config_manager=cm)
assert exc.value.status_code == 404
assert "Dataset review feature is disabled" in exc.value.detail
#endregion
#region test_require_auto_review_flag_subflag_disabled [C:2]
# @BRIEF Raises 404 when ff_dataset_auto_review is disabled.
def test_require_auto_review_flag_subflag_disabled():
cm = _make_config_manager_mock(dataset_review=True, ff_auto=False)
with pytest.raises(HTTPException) as exc:
_require_auto_review_flag(config_manager=cm)
assert exc.value.status_code == 404
assert "Dataset auto review feature is disabled" in exc.value.detail
#endregion
#region test_require_clarification_flag_enabled [C:2]
# @BRIEF Returns True when clarification flags are on.
def test_require_clarification_flag_enabled():
cm = _make_config_manager_mock(dataset_review=True, ff_clarification=True)
result = _require_clarification_flag(config_manager=cm)
assert result is True
#endregion
#region test_require_clarification_flag_disabled [C:2]
# @BRIEF Raises 404 when clarification feature is disabled (subflag).
def test_require_clarification_flag_disabled():
cm = _make_config_manager_mock(dataset_review=True, ff_clarification=False)
with pytest.raises(HTTPException) as exc:
_require_clarification_flag(config_manager=cm)
assert exc.value.status_code == 404
assert "Dataset clarification feature is disabled" in exc.value.detail
#endregion
#region test_require_execution_flag_enabled [C:2]
# @BRIEF Returns True when execution flags are on.
def test_require_execution_flag_enabled():
cm = _make_config_manager_mock(dataset_review=True, ff_execution=True)
result = _require_execution_flag(config_manager=cm)
assert result is True
#endregion
#region test_require_execution_flag_disabled [C:2]
# @BRIEF Raises 404 when execution feature is disabled (subflag).
def test_require_execution_flag_disabled():
cm = _make_config_manager_mock(dataset_review=True, ff_execution=False)
with pytest.raises(HTTPException) as exc:
_require_execution_flag(config_manager=cm)
assert exc.value.status_code == 404
assert "Dataset execution feature is disabled" in exc.value.detail
#endregion
#region test_flag_guards_feature_disabled_override [C:2]
# @BRIEF All three guards raise 404 when the base dataset_review feature flag is off.
def test_flag_guards_feature_disabled_override():
cm = _make_config_manager_mock(dataset_review=False)
for guard in [_require_auto_review_flag, _require_clarification_flag, _require_execution_flag]:
with pytest.raises(HTTPException) as exc:
guard(config_manager=cm)
assert exc.value.status_code == 404
assert "Dataset review feature is disabled" in exc.value.detail
#endregion
# ── Session version header ─────────────────────────────────────────────
#region test_require_session_version_header [C:2]
# @BRIEF Parses valid X-Session-Version header.
def test_require_session_version_header():
# Can't easily test the Header injection standalone;
# test via the HTTPException that would result from missing header at FastAPI level.
# The function itself just returns the parsed int.
# We test that it works with valid ints.
version = _require_session_version_header(session_version=5)
assert version == 5
assert isinstance(version, int)
#endregion
# ── Error builders and enforcement ────────────────────────────────────
#region test_build_session_version_conflict_exception [C:2]
# @BRIEF Builds HTTP 409 with structured detail payload.
def test_build_session_version_conflict_exception():
exc = DatasetReviewSessionVersionConflictError(
session_id="sess-1", expected_version=5, actual_version=8
)
http_exc = _build_session_version_conflict_http_exception(exc)
assert http_exc.status_code == 409
detail = http_exc.detail
assert detail["error_code"] == "session_version_conflict"
assert detail["session_id"] == "sess-1"
assert detail["expected_version"] == 5
assert detail["actual_version"] == 8
#endregion
#region test_enforce_session_version_pass [C:2]
# @BRIEF Passes through when versions match.
def test_enforce_session_version_pass():
session = _make_session_mock(version=5)
repo = MagicMock()
repo.require_session_version.return_value = session
result = _enforce_session_version(repo, session, 5)
assert result is session
repo.require_session_version.assert_called_once_with(session, 5)
#endregion
#region test_enforce_session_version_conflict [C:2]
# @BRIEF Raises HTTP 409 when versions conflict.
def test_enforce_session_version_conflict():
session = _make_session_mock(version=8)
repo = MagicMock()
repo.require_session_version.side_effect = DatasetReviewSessionVersionConflictError(
session_id="sess-1", expected_version=5, actual_version=8
)
with pytest.raises(HTTPException) as exc:
_enforce_session_version(repo, session, 5)
assert exc.value.status_code == 409
detail = exc.value.detail
assert detail["error_code"] == "session_version_conflict"
assert detail["expected_version"] == 5
assert detail["actual_version"] == 8
#endregion
# ── Session ownership helpers ──────────────────────────────────────────
#region test_get_owned_session_or_404_found [C:2]
# @BRIEF Returns session when accessible to user.
def test_get_owned_session_or_404_found():
session = _make_session_mock(user_id="user-1")
repo = MagicMock()
repo.load_session_detail.return_value = session
result = _get_owned_session_or_404(repo, "sess-1", MagicMock(id="user-1"))
assert result is session
#endregion
#region test_get_owned_session_or_404_not_found [C:2]
# @BRIEF Raises 404 when session not found.
def test_get_owned_session_or_404_not_found():
repo = MagicMock()
repo.load_session_detail.return_value = None
with pytest.raises(HTTPException) as exc:
_get_owned_session_or_404(repo, "sess-1", MagicMock(id="user-1"))
assert exc.value.status_code == 404
assert "Session not found" in exc.value.detail
#endregion
#region test_require_owner_mutation_scope_pass [C:2]
# @BRIEF Passes when user is the owner.
def test_require_owner_mutation_scope_pass():
session = _make_session_mock(user_id="user-1")
result = _require_owner_mutation_scope(session, MagicMock(id="user-1"))
assert result is session
#endregion
#region test_require_owner_mutation_scope_forbidden [C:2]
# @BRIEF Raises 403 when non-owner tries to mutate.
def test_require_owner_mutation_scope_forbidden():
session = _make_session_mock(user_id="user-1")
with pytest.raises(HTTPException) as exc:
_require_owner_mutation_scope(session, MagicMock(id="user-2"))
assert exc.value.status_code == 403
assert "Only the owner can mutate" in exc.value.detail
#endregion
#region test_prepare_owned_session_mutation [C:2]
# @BRIEF Combines resolve + ownership check + version enforcement.
def test_prepare_owned_session_mutation():
session = _make_session_mock(user_id="user-1")
repo = MagicMock()
repo.load_session_detail.return_value = session
repo.require_session_version.return_value = session
result = _prepare_owned_session_mutation(
repo, "sess-1", MagicMock(id="user-1"), 5
)
assert result is session
repo.load_session_detail.assert_called_once_with("sess-1", "user-1")
repo.require_session_version.assert_called_once()
#endregion
#region test_prepare_owned_session_mutation_not_owner [C:2]
# @BRIEF Raises 403 when session belongs to another user.
def test_prepare_owned_session_mutation_not_owner():
session = _make_session_mock(user_id="user-1")
repo = MagicMock()
repo.load_session_detail.return_value = session
with pytest.raises(HTTPException) as exc:
_prepare_owned_session_mutation(repo, "sess-1", MagicMock(id="user-2"), 5)
assert exc.value.status_code == 403
#endregion
# ── Commit helpers ──────────────────────────────────────────────────────
#region test_commit_owned_session_mutation_pass [C:2]
# @BRIEF Commits successfully through repository.
def test_commit_owned_session_mutation_pass():
session = _make_session_mock()
repo = MagicMock()
repo.commit_session_mutation.return_value = session
result = _commit_owned_session_mutation(repo, session)
assert result is session
repo.commit_session_mutation.assert_called_once_with(
session, refresh_targets=None
)
#endregion
#region test_commit_owned_session_mutation_with_refresh_targets [C:2]
# @BRIEF Passes refresh_targets through to repository.
def test_commit_owned_session_mutation_with_refresh_targets():
session = _make_session_mock()
field = MagicMock()
repo = MagicMock()
repo.commit_session_mutation.return_value = session
result = _commit_owned_session_mutation(repo, session, refresh_targets=[field])
assert result is session
repo.commit_session_mutation.assert_called_once_with(
session, refresh_targets=[field]
)
#endregion
#region test_commit_owned_session_mutation_conflict [C:2]
# @BRIEF Raises 409 when repository reports version conflict.
def test_commit_owned_session_mutation_conflict():
session = _make_session_mock()
repo = MagicMock()
repo.commit_session_mutation.side_effect = DatasetReviewSessionVersionConflictError(
session_id="sess-1", expected_version=5, actual_version=8
)
with pytest.raises(HTTPException) as exc:
_commit_owned_session_mutation(repo, session)
assert exc.value.status_code == 409
#endregion
# ── Event recorder ──────────────────────────────────────────────────────
#region test_record_session_event [C:2]
# @BRIEF Delegates to repository's event_logger.
def test_record_session_event():
session = _make_session_mock()
repo = MagicMock()
repo.event_logger = MagicMock()
user = MagicMock(id="user-1")
_record_session_event(
repo, session, user,
event_type="test_event",
event_summary="Test event summary",
event_details={"key": "value"},
)
repo.event_logger.log_for_session.assert_called_once_with(
session,
actor_user_id="user-1",
event_type="test_event",
event_summary="Test event summary",
event_details={"key": "value"},
)
#endregion
# ── Clarification helpers ──────────────────────────────────────────────
#region test_get_latest_clarification_session_or_404_found [C:2]
# @BRIEF Returns latest clarification session when available.
def test_get_latest_clarification_session_or_404_found():
session = _make_session_mock()
cs1 = MagicMock(clarification_session_id="cs-1", started_at=datetime(2025, 1, 1, tzinfo=UTC))
cs2 = MagicMock(clarification_session_id="cs-2", started_at=datetime(2025, 6, 1, tzinfo=UTC))
session.clarification_sessions = [cs1, cs2]
result = _get_latest_clarification_session_or_404(session)
assert result.clarification_session_id == "cs-2"
#endregion
#region test_get_latest_clarification_session_or_404_empty [C:2]
# @BRIEF Raises 404 when no clarification sessions exist.
def test_get_latest_clarification_session_or_404_empty():
session = _make_session_mock()
session.clarification_sessions = []
with pytest.raises(HTTPException) as exc:
_get_latest_clarification_session_or_404(session)
assert exc.value.status_code == 404
assert "Clarification session not found" in exc.value.detail
#endregion
# ── Mapping / field resolution ─────────────────────────────────────────
#region test_get_owned_mapping_or_404_found [C:2]
# @BRIEF Returns mapping when found in session.
def test_get_owned_mapping_or_404_found():
session = _make_session_mock()
mapping = MagicMock(mapping_id="map-1")
mapping2 = MagicMock(mapping_id="map-2")
session.execution_mappings = [mapping, mapping2]
result = _get_owned_mapping_or_404(session, "map-2")
assert result is mapping2
#endregion
#region test_get_owned_mapping_or_404_not_found [C:2]
# @BRIEF Raises 404 when mapping not found.
def test_get_owned_mapping_or_404_not_found():
session = _make_session_mock()
session.execution_mappings = []
with pytest.raises(HTTPException) as exc:
_get_owned_mapping_or_404(session, "map-999")
assert exc.value.status_code == 404
assert "Execution mapping not found" in exc.value.detail
#endregion
#region test_get_owned_field_or_404_found [C:2]
# @BRIEF Returns field when found in session.
def test_get_owned_field_or_404_found():
session = _make_session_mock()
field = MagicMock(field_id="field-1")
session.semantic_fields = [field]
result = _get_owned_field_or_404(session, "field-1")
assert result is field
#endregion
#region test_get_owned_field_or_404_not_found [C:2]
# @BRIEF Raises 404 when field not found.
def test_get_owned_field_or_404_not_found():
session = _make_session_mock()
session.semantic_fields = []
with pytest.raises(HTTPException) as exc:
_get_owned_field_or_404(session, "field-999")
assert exc.value.status_code == 404
assert "Semantic field not found" in exc.value.detail
#endregion
# ── Candidate provenance mapping ──────────────────────────────────────
#region test_map_candidate_provenance [C:2]
# @BRIEF Maps CandidateMatchType to FieldProvenance correctly.
def test_map_candidate_provenance():
exact = MagicMock()
exact.match_type.value = "exact"
assert _map_candidate_provenance(exact) == FieldProvenance.DICTIONARY_EXACT
ref = MagicMock()
ref.match_type.value = "reference"
assert _map_candidate_provenance(ref) == FieldProvenance.REFERENCE_IMPORTED
generated = MagicMock()
generated.match_type.value = "generated"
assert _map_candidate_provenance(generated) == FieldProvenance.AI_GENERATED
fuzzy = MagicMock()
fuzzy.match_type.value = "something_else"
assert _map_candidate_provenance(fuzzy) == FieldProvenance.FUZZY_INFERRED
#endregion
# ── Source version resolution ──────────────────────────────────────────
#region test_resolve_candidate_source_version [C:2]
# @BRIEF Resolves source version from field/session.
def test_resolve_candidate_source_version():
field = MagicMock()
session = MagicMock()
field.session = session
source = MagicMock(source_id="src-1", source_version="v2.0")
session.semantic_sources = [source]
assert _resolve_candidate_source_version(field, "src-1") == "v2.0"
#endregion
#region test_resolve_candidate_source_version_not_found [C:2]
# @BRIEF Returns None when source_id not found.
def test_resolve_candidate_source_version_not_found():
field = MagicMock()
session = MagicMock()
field.session = session
session.semantic_sources = [MagicMock(source_id="src-1", source_version="v1")]
assert _resolve_candidate_source_version(field, "src-999") is None
#endregion
#region test_resolve_candidate_source_version_no_source_id [C:2]
# @BRIEF Returns None when source_id is None.
def test_resolve_candidate_source_version_no_source_id():
field = MagicMock()
assert _resolve_candidate_source_version(field, None) is None
#endregion
#region test_resolve_candidate_source_version_no_session [C:2]
# @BRIEF Returns None when field has no session reference.
def test_resolve_candidate_source_version_no_session():
field = MagicMock()
field.session = None
assert _resolve_candidate_source_version(field, "src-1") is None
#endregion
#region test_resolve_candidate_source_version_empty_sources [C:2]
# @BRIEF Returns None when session has no semantic_sources.
def test_resolve_candidate_source_version_empty_sources():
field = MagicMock()
field.session = MagicMock()
field.session.semantic_sources = []
assert _resolve_candidate_source_version(field, "src-1") is None
#endregion
# ── Semantic field state update ────────────────────────────────────────
def _make_field_mock(field_id="field-1"):
field = MagicMock()
field.field_id = field_id
field.verbose_name = None
field.description = None
field.display_format = None
field.provenance = FieldProvenance.UNRESOLVED
field.source_id = None
field.source_version = None
field.confidence_rank = None
field.is_locked = False
field.has_conflict = False
field.needs_review = False
field.last_changed_by = None
candidate = MagicMock()
candidate.candidate_id = "cand-1"
candidate.match_type = MagicMock()
candidate.match_type.value = "exact"
candidate.proposed_verbose_name = "Recognized Revenue"
candidate.proposed_description = "Revenue recognized during period"
candidate.proposed_display_format = "$,.0f"
candidate.source_id = "src-1"
candidate.candidate_rank = 1
candidate.status = CandidateStatus.PROPOSED
field.candidates = [candidate]
field.session = MagicMock()
field.session.semantic_sources = [MagicMock(source_id="src-1", source_version="v2")]
return field
#region test_update_semantic_field_state_manual_override [C:2]
# @BRIEF Manual override sets provenance to MANUAL_OVERRIDE and locks the field.
def test_update_semantic_field_state_manual_override():
field = _make_field_mock()
req = FieldSemanticUpdateRequest(
verbose_name="Custom Name",
description="Custom description",
display_format="text",
)
result = _update_semantic_field_state(field, req, changed_by="user")
assert result.verbose_name == "Custom Name"
assert result.description == "Custom description"
assert result.display_format == "text"
assert result.provenance == FieldProvenance.MANUAL_OVERRIDE
assert result.is_locked is True
assert result.source_id is None
for c in result.candidates:
assert c.status == CandidateStatus.SUPERSEDED
#endregion
#region test_update_semantic_field_state_candidate_acceptance [C:2]
# @BRIEF Accepting a candidate applies its values with mapped provenance.
def test_update_semantic_field_state_candidate_acceptance():
field = _make_field_mock()
req = FieldSemanticUpdateRequest(candidate_id="cand-1", lock_field=True)
result = _update_semantic_field_state(field, req, changed_by="user")
assert result.verbose_name == "Recognized Revenue"
assert result.provenance == FieldProvenance.DICTIONARY_EXACT
assert result.source_id == "src-1"
assert result.source_version == "v2"
assert result.confidence_rank == 1
assert result.is_locked is True
assert result.needs_review is False
accepted = [c for c in result.candidates if c.status == CandidateStatus.ACCEPTED]
superseded = [c for c in result.candidates if c.status == CandidateStatus.SUPERSEDED]
assert len(accepted) == 1
assert len(superseded) == 0 # Only one candidate, it gets accepted
#endregion
#region test_update_semantic_field_state_no_input [C:2]
# @BRIEF Raises 400 when neither candidate_id nor manual override provided.
def test_update_semantic_field_state_no_input():
field = _make_field_mock()
req = FieldSemanticUpdateRequest()
with pytest.raises(HTTPException) as exc:
_update_semantic_field_state(field, req, changed_by="user")
assert exc.value.status_code == 400
#endregion
#region test_update_semantic_field_state_invalid_candidate [C:2]
# @BRIEF Raises 404 when candidate_id not found.
def test_update_semantic_field_state_invalid_candidate():
field = _make_field_mock()
req = FieldSemanticUpdateRequest(candidate_id="nonexistent")
with pytest.raises(HTTPException) as exc:
_update_semantic_field_state(field, req, changed_by="user")
assert exc.value.status_code == 404
assert "Semantic candidate not found" in exc.value.detail
#endregion
# ── SQL Lab redirect URL building ──────────────────────────────────────
#region test_build_sql_lab_redirect_url [C:2]
# @BRIEF Builds correct redirect URL with queryId parameter.
def test_build_sql_lab_redirect_url():
url = _build_sql_lab_redirect_url(
environment_url="http://superset.local",
sql_lab_session_ref="sql-lab-77",
)
assert url == "http://superset.local/superset/sqllab?queryId=sql-lab-77"
#endregion
#region test_build_sql_lab_redirect_url_trailing_slash [C:2]
# @BRIEF Handles trailing slash on base URL.
def test_build_sql_lab_redirect_url_trailing_slash():
url = _build_sql_lab_redirect_url(
environment_url="http://superset.local/",
sql_lab_session_ref="sql-lab-77",
)
assert url == "http://superset.local/superset/sqllab?queryId=sql-lab-77"
#endregion
#region test_build_sql_lab_redirect_url_empty_env [C:2]
# @BRIEF Raises 500 when environment URL is empty.
def test_build_sql_lab_redirect_url_empty_env():
with pytest.raises(HTTPException) as exc:
_build_sql_lab_redirect_url(environment_url="", sql_lab_session_ref="sql-lab-77")
assert exc.value.status_code == 500
assert "Superset environment URL is not configured" in exc.value.detail
#endregion
#region test_build_sql_lab_redirect_url_empty_ref [C:2]
# @BRIEF Raises 500 when SQL Lab session reference is empty.
def test_build_sql_lab_redirect_url_empty_ref():
with pytest.raises(HTTPException) as exc:
_build_sql_lab_redirect_url(
environment_url="http://superset.local",
sql_lab_session_ref="",
)
assert exc.value.status_code == 500
assert "SQL Lab session reference is missing" in exc.value.detail
#endregion
#region test_build_sql_lab_redirect_url_empty_ref_whitespace [C:2]
# @BRIEF Raises 500 when ref is just whitespace.
def test_build_sql_lab_redirect_url_empty_ref_whitespace():
with pytest.raises(HTTPException) as exc:
_build_sql_lab_redirect_url(
environment_url="http://superset.local/",
sql_lab_session_ref=" ",
)
assert exc.value.status_code == 500
assert "SQL Lab session reference is missing" in exc.value.detail
#endregion
# ── Export builders ────────────────────────────────────────────────────
def _make_export_session():
session = _make_session_mock()
session.profile = MagicMock(
business_summary="Revenue data summary",
dataset_name="sales",
confidence_state=MagicMock(value="mostly_confirmed"),
dataset_type="table",
)
finding = MagicMock()
finding.code = "TEST-001"
finding.severity = MagicMock(value="warning")
finding.title = "Test Issue"
finding.message = "This is a test finding"
finding.resolution_state = MagicMock(value="open")
session.findings = [finding]
return session
def _make_export_session_no_profile():
session = _make_session_mock()
session.profile = None
session.findings = []
return session
#region test_build_documentation_export_json [C:2]
# @BRIEF Produces JSON documentation export with profile data.
def test_build_documentation_export_json():
session = _make_export_session()
result = _build_documentation_export(session, ArtifactFormat.JSON)
assert result["storage_ref"].endswith("documentation.json")
assert result["content"]["session"]["session_id"] == "sess-1"
assert result["content"]["profile"]["business_summary"] == "Revenue data summary"
assert len(result["content"]["findings"]) == 1
#endregion
#region test_build_documentation_export_markdown [C:2]
# @BRIEF Produces Markdown documentation export.
def test_build_documentation_export_markdown():
session = _make_export_session()
result = _build_documentation_export(session, ArtifactFormat.MARKDOWN)
assert result["storage_ref"].endswith("documentation.md")
assert "# Dataset Review:" in result["content"]["markdown"]
assert "Revenue data summary" in result["content"]["markdown"]
assert "Test Issue" in result["content"]["markdown"]
#endregion
#region test_build_documentation_export_no_profile [C:2]
# @BRIEF Produces documentation export without profile data.
def test_build_documentation_export_no_profile():
session = _make_export_session_no_profile()
result = _build_documentation_export(session, ArtifactFormat.JSON)
assert result["content"]["profile"] is None
#endregion
#region test_build_documentation_export_no_findings [C:2]
# @BRIEF Markdown handles no findings gracefully.
def test_build_documentation_export_no_findings():
session = _make_export_session_no_profile()
result = _build_documentation_export(session, ArtifactFormat.MARKDOWN)
assert "No findings recorded" in result["content"]["markdown"]
#endregion
#region test_build_validation_export_json [C:2]
# @BRIEF Produces JSON validation export.
def test_build_validation_export_json():
session = _make_export_session()
result = _build_validation_export(session, ArtifactFormat.JSON)
assert result["storage_ref"].endswith("validation.json")
assert result["content"]["session_id"] == "sess-1"
assert len(result["content"]["findings"]) == 1
assert result["content"]["readiness_state"] == "review_ready"
#endregion
#region test_build_validation_export_markdown [C:2]
# @BRIEF Produces Markdown validation export.
def test_build_validation_export_markdown():
session = _make_export_session()
result = _build_validation_export(session, ArtifactFormat.MARKDOWN)
assert result["storage_ref"].endswith("validation.md")
assert "# Validation Report:" in result["content"]["markdown"]
assert "TEST-001" in result["content"]["markdown"]
#endregion
#region test_build_validation_export_no_findings [C:2]
# @BRIEF Markdown handles no findings in validation.
def test_build_validation_export_no_findings():
session = _make_export_session_no_profile()
result = _build_validation_export(session, ArtifactFormat.MARKDOWN)
assert "No findings recorded" in result["content"]["markdown"]
#endregion
# ── Serialization helpers ──────────────────────────────────────────────
#region test_serialize_session_summary [C:2]
# @BRIEF Maps session to SessionSummary DTO with session_version.
def test_serialize_session_summary():
session = _make_session_mock(version=7)
result = _serialize_session_summary(session)
assert isinstance(result, SessionSummary)
assert result.session_id == "sess-1"
assert result.session_version == 7
assert result.version == 7
#endregion
#region test_serialize_session_detail [C:2]
# @BRIEF Maps session to SessionDetail DTO with nested collections.
def test_serialize_session_detail():
session = _make_session_mock(version=3)
result = _serialize_session_detail(session)
assert isinstance(result, SessionDetail)
assert result.session_id == "sess-1"
assert result.session_version == 3
assert result.version == 3
assert result.findings == []
assert result.semantic_fields == []
#endregion
#region test_serialize_semantic_field [C:2]
# @BRIEF Maps semantic field to SemanticFieldEntryDto with session version.
def test_serialize_semantic_field():
field = MagicMock()
field.field_id = "field-1"
field.session_id = "sess-1"
field.field_name = "revenue"
field.field_kind = FieldKind.METRIC
field.verbose_name = "Revenue"
field.description = "Total revenue"
field.display_format = "$,.0f"
field.provenance = FieldProvenance.DICTIONARY_EXACT
field.source_id = None
field.source_version = None
field.confidence_rank = 1
field.is_locked = False
field.has_conflict = False
field.needs_review = False
field.last_changed_by = "user"
field.user_feedback = None
field.created_at = datetime.now(UTC)
field.updated_at = datetime.now(UTC)
field.candidates = []
session = MagicMock(version=4)
field.session = session
result = _serialize_semantic_field(field)
assert result.field_id == "field-1"
assert result.session_version == 4
#endregion
#region test_serialize_semantic_field_no_session [C:2]
# @BRIEF Handles field without session reference.
def test_serialize_semantic_field_no_session():
field = MagicMock()
field.field_id = "field-1"
field.session_id = "sess-1"
field.field_name = "revenue"
field.field_kind = FieldKind.METRIC
field.verbose_name = "Revenue"
field.description = "Total revenue"
field.display_format = "$,.0f"
field.provenance = FieldProvenance.DICTIONARY_EXACT
field.source_id = None
field.source_version = None
field.confidence_rank = None
field.is_locked = False
field.has_conflict = False
field.needs_review = False
field.last_changed_by = "system"
field.user_feedback = None
field.created_at = datetime.now(UTC)
field.updated_at = datetime.now(UTC)
field.candidates = []
field.session = None
result = _serialize_semantic_field(field)
assert result.session_version is None
#endregion
#region test_serialize_execution_mapping [C:2]
# @BRIEF Maps execution mapping to ExecutionMappingDto with session version.
def test_serialize_execution_mapping():
mapping = MagicMock()
mapping.mapping_id = "map-1"
mapping.session_id = "sess-1"
mapping.filter_id = "fl-1"
mapping.variable_id = "var-1"
mapping.mapping_method = MappingMethod.DIRECT_MATCH
mapping.raw_input_value = "DE"
mapping.effective_value = "DE"
mapping.transformation_note = None
mapping.warning_level = None
mapping.requires_explicit_approval = False
mapping.approval_state = ApprovalState.APPROVED
mapping.approved_by_user_id = None
mapping.approved_at = None
mapping.created_at = datetime.now(UTC)
mapping.updated_at = datetime.now(UTC)
mapping.session = MagicMock(version=5)
result = _serialize_execution_mapping(mapping)
assert isinstance(result, ExecutionMappingDto)
assert result.mapping_id == "map-1"
assert result.session_version == 5
#endregion
#region test_serialize_preview [C:2]
# @BRIEF Maps preview to CompiledPreviewDto with session version fallback.
def test_serialize_preview():
preview = MagicMock()
preview.preview_id = "prev-1"
preview.session_id = "sess-1"
preview.preview_status = PreviewStatus.READY
preview.compiled_sql = "SELECT 1"
preview.preview_fingerprint = "fp-1"
preview.compiled_by = "system"
preview.error_code = None
preview.error_details = None
preview.compiled_at = datetime.now(UTC)
preview.created_at = datetime.now(UTC)
preview.session = MagicMock(version=6)
result = _serialize_preview(preview)
assert isinstance(result, CompiledPreviewDto)
assert result.preview_id == "prev-1"
assert result.session_version == 6
#endregion
#region test_serialize_preview_fallback [C:2]
# @BRIEF Uses session_version_fallback when preview has no session reference.
def test_serialize_preview_fallback():
preview = MagicMock()
preview.preview_id = "prev-1"
preview.session_id = "sess-1"
preview.preview_status = PreviewStatus.PENDING
preview.compiled_sql = None
preview.preview_fingerprint = "fp-1"
preview.compiled_by = "system"
preview.error_code = None
preview.error_details = None
preview.compiled_at = None
preview.created_at = datetime.now(UTC)
preview.session = None
result = _serialize_preview(preview, session_version_fallback=3)
assert result.session_version == 3
#endregion
#region test_serialize_run_context [C:2]
# @BRIEF Maps run context to DatasetRunContextDto with session version.
def test_serialize_run_context():
rc = MagicMock()
rc.run_context_id = "rc-1"
rc.session_id = "sess-1"
rc.dataset_ref = "public.sales"
rc.environment_id = "env-1"
rc.preview_id = "prev-1"
rc.sql_lab_session_ref = "sql-lab-1"
rc.effective_filters = []
rc.template_params = {}
rc.approved_mapping_ids = []
rc.semantic_decision_refs = []
rc.open_warning_refs = []
rc.launch_status = LaunchStatus.STARTED
rc.launch_error = None
rc.created_at = datetime.now(UTC)
rc.session = MagicMock(version=7)
result = _serialize_run_context(rc)
assert result.run_context_id == "rc-1"
assert result.session_version == 7
assert result.dataset_ref == "public.sales"
#endregion
#region test_serialize_clarification_question_payload [C:2]
# @BRIEF Converts ClarificationQuestionPayload into DTO.
def test_serialize_clarification_question_payload():
payload = MagicMock()
payload.question_id = "q-1"
payload.clarification_session_id = "cs-1"
payload.topic_ref = "topic"
payload.question_text = "What is this?"
payload.why_it_matters = "It matters because..."
payload.current_guess = "Guess"
payload.priority = 1
payload.state = QuestionState.OPEN
payload.options = []
result = _serialize_clarification_question_payload(payload)
assert result is not None
assert result.question_id == "q-1"
assert result.question_text == "What is this?"
assert result.options == []
#endregion
#region test_serialize_clarification_question_payload_none [C:2]
# @BRIEF Returns None when payload is None.
def test_serialize_clarification_question_payload_none():
result = _serialize_clarification_question_payload(None)
assert result is None
#endregion
#region test_serialize_clarification_state [C:2]
# @BRIEF Converts ClarificationStateResult into API response.
def test_serialize_clarification_state():
state = MagicMock()
cs = MagicMock()
cs.clarification_session_id = "cs-1"
cs.session_id = "sess-1"
cs.status = ClarificationStatus.ACTIVE
cs.current_question_id = "q-1"
cs.resolved_count = 2
cs.remaining_count = 3
cs.summary_delta = "Progressing"
state.clarification_session = cs
question_payload = MagicMock()
question_payload.question_id = "q-1"
question_payload.clarification_session_id = "cs-1"
question_payload.topic_ref = "topic"
question_payload.question_text = "Question?"
question_payload.why_it_matters = "Because"
question_payload.current_guess = None
question_payload.priority = 1
question_payload.state = QuestionState.OPEN
question_payload.options = []
state.current_question = question_payload
result = _serialize_clarification_state(state)
assert result.clarification_session is not None
assert result.clarification_session.clarification_session_id == "cs-1"
assert result.clarification_session.resolved_count == 2
assert result.current_question is not None
assert result.current_question.question_id == "q-1"
#endregion
#region test_serialize_empty_clarification_state [C:2]
# @BRIEF Returns empty state with None values.
def test_serialize_empty_clarification_state():
result = _serialize_empty_clarification_state()
assert result.clarification_session is None
assert result.current_question is None
#endregion
# ── Dependency injection functions ──────────────────────────────────────
#region test_get_repository [C:2]
# @BRIEF _get_repository wraps a Session into DatasetReviewSessionRepository.
def test_get_repository():
mock_db = MagicMock()
repo = _get_repository(db=mock_db)
from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionRepository,
)
assert isinstance(repo, DatasetReviewSessionRepository)
assert repo.db is mock_db
#endregion
#region test_get_orchestrator [C:2]
# @BRIEF _get_orchestrator builds DatasetReviewOrchestrator with deps.
def test_get_orchestrator():
mock_repo = MagicMock()
mock_cm = MagicMock()
mock_tm = MagicMock()
orch = _get_orchestrator(repository=mock_repo, config_manager=mock_cm, task_manager=mock_tm)
from src.services.dataset_review.orchestrator import DatasetReviewOrchestrator
assert isinstance(orch, DatasetReviewOrchestrator)
assert orch.repository is mock_repo
#endregion
#region test_get_clarification_engine [C:2]
# @BRIEF _get_clarification_engine builds ClarificationEngine with repo.
def test_get_clarification_engine():
mock_repo = MagicMock()
engine = _get_clarification_engine(repository=mock_repo)
from src.services.dataset_review.clarification_engine import ClarificationEngine
assert isinstance(engine, ClarificationEngine)
assert engine.repository is mock_repo
#endregion
#endregion Test.DatasetReview.DepsUnit