#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