1279 lines
47 KiB
Python
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
|