# #region Test.DatasetReview.Comprehensive [C:3] [TYPE Module] [SEMANTICS test,dataset,review,routes,coverage] # @BRIEF Comprehensive coverage for dataset_review_pkg/_routes.py — happy paths for all endpoints. # @RELATION BINDS_TO -> [DatasetReviewRoutes] # @TEST_EDGE: start_session_success -> POST /sessions returns 201 # @TEST_EDGE: get_session_detail -> GET /sessions/{id} returns session detail # @TEST_EDGE: delete_session_soft_archive -> DELETE session without hard_delete archives # @TEST_EDGE: export_documentation_json -> GET exports/documentation returns content # @TEST_EDGE: get_clarification_state -> GET clarification state with active session # @TEST_EDGE: resume_clarification -> POST clarification/resume returns state # @TEST_EDGE: record_clarification_answer -> POST clarification/answers persists answer # @TEST_EDGE: update_field_semantic -> PATCH field semantic applies update # @TEST_EDGE: unlock_field_manual_override -> Unlock resets MANUAL_OVERRIDE provenance # @TEST_EDGE: approve_batch_semantic_multiple -> Batch approve multiple fields # @TEST_EDGE: update_execution_mapping_full -> Full execution mapping update # @TEST_EDGE: approve_execution_mapping -> POST mapping/approve # @TEST_EDGE: approve_batch_execution_mappings -> POST mappings/approve-batch # @TEST_EDGE: trigger_preview_pending -> Preview queued returns 202 # @TEST_EDGE: launch_dataset_success -> POST launch returns 201 with redirect_url # @TEST_EDGE: list_execution_mappings -> GET mappings returns list from __future__ import annotations from datetime import UTC, datetime from pathlib import Path import sys from types import SimpleNamespace sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import pytest from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient from fastapi import status from src.api.routes.dataset_review import ( _get_orchestrator, _get_repository, _get_clarification_engine, ) from src.app import app from src.core.config_models import AppConfig, Environment, GlobalSettings from src.dependencies import get_config_manager, get_current_user, get_task_manager from src.models.dataset_review import ( ApprovalState, ArtifactFormat, FieldProvenance, MappingMethod, PreviewStatus, ReadinessState, RecommendedAction, SessionStatus, SessionPhase, ) from src.models.auth import User from src.services.dataset_review.event_logger import SessionEventLogger from src.services.dataset_review.orchestrator import ( DatasetReviewOrchestrator, LaunchDatasetResult, PreparePreviewResult, RunContextResult, StartSessionCommand, StartSessionResult, ) client = TestClient(app) # ── Shared fixture ── @pytest.fixture(autouse=True) def _clean_overrides(): app.dependency_overrides.clear() yield app.dependency_overrides.clear() # ── Builders ── def _make_user(extra_permissions=None): perms = [ SimpleNamespace(resource="dataset:session", action="READ"), SimpleNamespace(resource="dataset:session", action="MANAGE"), SimpleNamespace(resource="dataset:execution:launch", action="EXECUTE"), ] if extra_permissions: perms.extend(extra_permissions) role = SimpleNamespace(name="DatasetReviewOperator", permissions=perms) return SimpleNamespace(id="user-1", username="tester", roles=[role]) def _make_config_manager(): env = Environment( id="env-1", name="DEV", url="http://superset.local", username="demo", password="secret", ) config = AppConfig(environments=[env], settings=GlobalSettings()) config.settings.features.dataset_review = True config.settings.ff_dataset_auto_review = True config.settings.ff_dataset_clarification = True config.settings.ff_dataset_execution = True manager = MagicMock() manager.get_environment.side_effect = ( lambda env_id: env if env_id == "env-1" else None ) manager.get_config.return_value = config return manager def _make_session(**kw): now = datetime.now(UTC) s = MagicMock() s.session_id = kw.get("session_id", "sess-1") s.user_id = "user-1" s.environment_id = "env-1" s.source_kind = "superset_link" s.source_input = "http://superset.local/dashboard/10" s.dataset_ref = "public.sales" s.dataset_id = 42 s.dashboard_id = 10 s.readiness_state = kw.get("readiness_state", ReadinessState.REVIEW_READY) s.recommended_action = kw.get("recommended_action", RecommendedAction.REVIEW_DOCUMENTATION) s.status = kw.get("status", SessionStatus.ACTIVE) s.current_phase = SessionPhase.REVIEW.value s.version = kw.get("version", 1) s.created_at = now s.updated_at = now s.last_activity_at = now s.profile = None s.findings = [] s.collaborators = [] s.semantic_sources = [] s.semantic_fields = kw.get("semantic_fields", []) s.imported_filters = [] s.template_variables = [] s.execution_mappings = kw.get("execution_mappings", []) s.clarification_sessions = kw.get("clarification_sessions", []) s.previews = kw.get("previews", []) s.run_contexts = [] return s def _make_field(field_id="field-1", **kw): f = MagicMock() f.field_id = field_id f.session_id = "sess-1" f.field_name = kw.get("field_name", "revenue") f.is_locked = kw.get("is_locked", False) f.provenance = kw.get("provenance", FieldProvenance.AI_GENERATED) f.last_changed_by = "system" f.needs_review = kw.get("needs_review", True) f.user_feedback = None f.verbose_name = kw.get("verbose_name", "Revenue") f.description = None f.display_format = None f.source_id = None f.source_version = None f.confidence_rank = None f.has_conflict = False f.created_at = datetime.now(UTC) f.updated_at = datetime.now(UTC) f.candidates = [] f.session = MagicMock(version=2) return f def _make_mapping(mapping_id="map-1", **kw): m = MagicMock() m.mapping_id = mapping_id m.effective_value = kw.get("effective_value", "old_value") m.mapping_method = MappingMethod.SEMANTIC_MATCH m.transformation_note = kw.get("transformation_note", None) m.approval_state = kw.get("approval_state", ApprovalState.PENDING) m.approved_by_user_id = kw.get("approved_by_user_id", None) m.approved_at = kw.get("approved_at", None) m.preferred_name = kw.get("preferred_name", "field") m.source_field = "src_field" m.target_field = "tgt_field" m.warning_level = None m.session = MagicMock(version=2) return m def _setup_deps(repository=None, orchestrator=None, clarification_engine=None, session=None): user = _make_user() cm = _make_config_manager() effective_session = session or _make_session() if repository is None: repository = MagicMock() repository.load_session_detail.return_value = effective_session repository.list_sessions_for_user.return_value = [effective_session] repository.require_session_version = MagicMock(return_value=effective_session) repository.bump_session_version = MagicMock(return_value=2) repository.commit_session_mutation = MagicMock() repository.db = MagicMock() repository.event_logger = MagicMock(spec=SessionEventLogger) repository.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) if orchestrator is None: orchestrator = MagicMock() orchestrator.start_session.return_value = StartSessionResult( session=effective_session ) app.dependency_overrides[get_config_manager] = lambda: cm app.dependency_overrides[get_current_user] = lambda: user app.dependency_overrides[_get_repository] = lambda: repository app.dependency_overrides[_get_orchestrator] = lambda: orchestrator if clarification_engine is not None: app.dependency_overrides[_get_clarification_engine] = lambda: clarification_engine return {"repository": repository, "orchestrator": orchestrator} # ── start_session ── class TestStartSession: """POST /api/dataset-orchestration/sessions""" def test_start_session_success(self): """Happy: session started returns 201 with summary.""" s = _make_session(session_id="new-sess-1") orch = MagicMock() orch.start_session.return_value = StartSessionResult(session=s) _setup_deps(orchestrator=orch) resp = client.post( "/api/dataset-orchestration/sessions", json={ "source_kind": "superset_link", "source_input": "http://superset.local/dashboard/10", "environment_id": "env-1", }, ) assert resp.status_code == 201 data = resp.json() assert data["session_id"] == "new-sess-1" # ── get_session_detail ── class TestGetSessionDetail: """GET /api/dataset-orchestration/sessions/{session_id}""" def test_get_session_detail(self): """Happy: returns session detail.""" s = _make_session() _setup_deps(session=s) resp = client.get("/api/dataset-orchestration/sessions/sess-1") assert resp.status_code == 200 data = resp.json() assert data["session_id"] == "sess-1" # ── delete_session soft archive ── class TestDeleteSession: """DELETE /api/dataset-orchestration/sessions/{session_id}""" def test_delete_session_soft_archive(self): """Soft archive: no hard_delete param archives session.""" session = _make_session(version=1) repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() repo.event_logger = MagicMock(spec=SessionEventLogger) repo.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) _setup_deps(repository=repo, session=session) resp = client.delete( "/api/dataset-orchestration/sessions/sess-1", headers={"X-Session-Version": "1"}, ) assert resp.status_code == 204 # ── export_documentation ── class TestExportDocumentation: """GET /api/dataset-orchestration/sessions/{session_id}/exports/documentation""" def test_export_documentation_json(self): """Happy: returns documentation export.""" session = _make_session() _setup_deps(session=session) resp = client.get( "/api/dataset-orchestration/sessions/sess-1/exports/documentation?format=json" ) assert resp.status_code == 200 data = resp.json() assert data["artifact_type"] == "documentation" def test_export_documentation_markdown(self): """Happy: markdown documentation export.""" session = _make_session() _setup_deps(session=session) resp = client.get( "/api/dataset-orchestration/sessions/sess-1/exports/documentation?format=markdown" ) assert resp.status_code == 200 data = resp.json() assert data["artifact_type"] == "documentation" # ── get_clarification_state ── class TestGetClarificationState: """GET /api/dataset-orchestration/sessions/{session_id}/clarification""" def test_get_clarification_state_with_session(self): """Happy: returns clarification state with active session.""" q = MagicMock() q.question_id = "q-1" q.question_text = "What is this field?" cs = MagicMock() cs.clarification_session_id = "cs-1" cs.questions = [q] cs.status = "active" session = _make_session(clarification_sessions=[cs]) clar_engine = MagicMock() clar_engine.build_question_payload = MagicMock(return_value={ "question_id": "q-1", "question_text": "What is this field?", }) _setup_deps(session=session, clarification_engine=clar_engine) resp = client.get( "/api/dataset-orchestration/sessions/sess-1/clarification" ) assert resp.status_code == 200 data = resp.json() assert "clarification_session" in data # ── resume_clarification ── class TestResumeClarification: """POST /api/dataset-orchestration/sessions/{session_id}/clarification/resume""" def test_resume_clarification(self): """Happy: resumes clarification.""" q = MagicMock() q.question_id = "q-1" cs = MagicMock() cs.clarification_session_id = "cs-1" cs.questions = [q] cs.status = "active" session = _make_session(clarification_sessions=[cs], version=1) clar_engine = MagicMock() clar_engine.build_question_payload.return_value = {"question_id": "q-1"} repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() repo.event_logger = MagicMock(spec=SessionEventLogger) repo.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) _setup_deps(session=session, repository=repo, clarification_engine=clar_engine) resp = client.post( "/api/dataset-orchestration/sessions/sess-1/clarification/resume", headers={"X-Session-Version": "1"}, ) assert resp.status_code == 200 data = resp.json() assert "clarification_session" in data # ── record_clarification_answer ── class TestRecordClarificationAnswer: """POST /api/dataset-orchestration/sessions/{session_id}/clarification/answers""" def test_record_clarification_answer(self): """Happy: records answer and returns updated state.""" q = MagicMock() q.question_id = "q-1" cs = MagicMock() cs.clarification_session_id = "cs-1" cs.questions = [q] cs.status = "active" session = _make_session(clarification_sessions=[cs], version=1) clar_engine = MagicMock() answer_result = MagicMock() answer_result.clarification_session = cs answer_result.current_question = None answer_result.session = session answer_result.changed_findings = [] clar_engine.record_answer.return_value = answer_result repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() repo.event_logger = MagicMock(spec=SessionEventLogger) repo.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) _setup_deps(session=session, repository=repo, clarification_engine=clar_engine) resp = client.post( "/api/dataset-orchestration/sessions/sess-1/clarification/answers", json={ "question_id": "q-1", "answer_kind": "free_text", "answer_value": "Test answer", }, headers={"X-Session-Version": "1"}, ) assert resp.status_code == 200 data = resp.json() assert "clarification_state" in data assert "session" in data # ── update_field_semantic ── class TestUpdateFieldSemantic: """PATCH /api/dataset-orchestration/sessions/{session_id}/fields/{field_id}/semantic""" def test_update_field_semantic(self): """Happy: applies field semantic update.""" field = _make_field(field_id="field-1") session = _make_session(semantic_fields=[field], version=1) repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() repo.event_logger = MagicMock(spec=SessionEventLogger) repo.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) _setup_deps(repository=repo, session=session) resp = client.patch( "/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic", json={"candidate_id": "cand-1", "lock_field": False}, headers={"X-Session-Version": "1"}, ) assert resp.status_code == 200 data = resp.json() assert data["field_id"] == "field-1" # ── unlock_field with MANUAL_OVERRIDE provenance ── class TestUnlockFieldManualOverride: """POST /api/dataset-orchestration/sessions/{session_id}/fields/{field_id}/unlock""" def test_unlock_manual_override_resets_provenance(self): """Unlock with MANUAL_OVERRIDE resets provenance to UNRESOLVED.""" field = _make_field(field_id="field-1", provenance=FieldProvenance.MANUAL_OVERRIDE, is_locked=True) session = _make_session(semantic_fields=[field], version=1) repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() repo.event_logger = MagicMock(spec=SessionEventLogger) repo.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) _setup_deps(repository=repo, session=session) resp = client.post( "/api/dataset-orchestration/sessions/sess-1/fields/field-1/unlock", headers={"X-Session-Version": "1"}, ) assert resp.status_code == 200 data = resp.json() assert data["is_locked"] is False assert data["provenance"] == "unresolved" # ── approve_batch_semantic_fields ── class TestApproveBatchSemantic: """POST /api/dataset-orchestration/sessions/{session_id}/fields/semantic/approve-batch""" def test_approve_multiple_fields(self): """Happy: batch approve multiple semantic fields.""" field1 = _make_field(field_id="f1") field2 = _make_field(field_id="f2") session = _make_session(semantic_fields=[field1, field2], version=1) repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() repo.event_logger = MagicMock(spec=SessionEventLogger) repo.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) _setup_deps(repository=repo, session=session) resp = client.post( "/api/dataset-orchestration/sessions/sess-1/fields/semantic/approve-batch", json={ "items": [ {"field_id": "f1", "candidate_id": "cand-1", "lock_field": False}, {"field_id": "f2", "candidate_id": "cand-2", "lock_field": True}, ] }, headers={"X-Session-Version": "1"}, ) assert resp.status_code == 200 data = resp.json() assert len(data) == 2 # ── list_execution_mappings ── class TestListExecutionMappings: """GET /api/dataset-orchestration/sessions/{session_id}/mappings""" def test_list_execution_mappings(self): """Happy: returns execution mappings list.""" mapping = _make_mapping(mapping_id="map-1") session = _make_session(execution_mappings=[mapping]) _setup_deps(session=session) resp = client.get( "/api/dataset-orchestration/sessions/sess-1/mappings" ) assert resp.status_code == 200 data = resp.json() assert len(data["items"]) == 1 # ── update_execution_mapping ── class TestUpdateExecutionMapping: """PATCH /api/dataset-orchestration/sessions/{session_id}/mappings/{mapping_id}""" def test_update_execution_mapping_full(self): """Happy: full execution mapping update.""" mapping = _make_mapping(mapping_id="map-1") session = _make_session(execution_mappings=[mapping], version=1, readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED) repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() repo.event_logger = MagicMock(spec=SessionEventLogger) repo.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) _setup_deps(repository=repo, session=session) resp = client.patch( "/api/dataset-orchestration/sessions/sess-1/mappings/map-1", json={ "effective_value": "new_db_value", "mapping_method": "manual_override", "transformation_note": "User approved", }, headers={"X-Session-Version": "1"}, ) assert resp.status_code == 200 data = resp.json() assert data["mapping_id"] == "map-1" # ── approve_execution_mapping ── class TestApproveExecutionMapping: """POST /api/dataset-orchestration/sessions/{session_id}/mappings/{mapping_id}/approve""" def test_approve_execution_mapping(self): """Happy: approve execution mapping.""" mapping = _make_mapping(mapping_id="map-1") session = _make_session(execution_mappings=[mapping], version=1, readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED) repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() repo.event_logger = MagicMock(spec=SessionEventLogger) repo.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) _setup_deps(repository=repo, session=session) resp = client.post( "/api/dataset-orchestration/sessions/sess-1/mappings/map-1/approve", json={"approval_note": "Approved by reviewer"}, headers={"X-Session-Version": "1"}, ) assert resp.status_code == 200 data = resp.json() assert data["mapping_id"] == "map-1" # ── approve_batch_execution_mappings ── class TestApproveBatchMappings: """POST /api/dataset-orchestration/sessions/{session_id}/mappings/approve-batch""" def test_approve_batch_execution_mappings(self): """Happy: batch approve mappings.""" mapping1 = _make_mapping(mapping_id="map-1") mapping2 = _make_mapping(mapping_id="map-2") session = _make_session(execution_mappings=[mapping1, mapping2], version=1, readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED) repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() repo.event_logger = MagicMock(spec=SessionEventLogger) repo.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-0" ) _setup_deps(repository=repo, session=session) resp = client.post( "/api/dataset-orchestration/sessions/sess-1/mappings/approve-batch", json={"mapping_ids": ["map-1", "map-2"], "approval_note": "Batch approved"}, headers={"X-Session-Version": "1"}, ) assert resp.status_code == 200 data = resp.json() assert len(data) == 2 # ── trigger_preview_generation (PENDING path) ── class TestTriggerPreviewPending: """POST /api/dataset-orchestration/sessions/{session_id}/preview""" def test_preview_pending_202(self): """Preview queued returns 202 Accepted.""" session = _make_session(version=1, readiness_state=ReadinessState.COMPILED_PREVIEW_READY) orch = MagicMock() preview_result = MagicMock(spec=PreparePreviewResult) preview_result.preview = MagicMock() preview_result.preview.preview_status = PreviewStatus.PENDING preview_result.session = session orch.prepare_launch_preview.return_value = preview_result repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() _setup_deps(repository=repo, orchestrator=orch, session=session) resp = client.post( "/api/dataset-orchestration/sessions/sess-1/preview", headers={"X-Session-Version": "1"}, ) assert resp.status_code == 202 data = resp.json() assert "preview_status" in data # ── launch_dataset ── class TestLaunchDataset: """POST /api/dataset-orchestration/sessions/{session_id}/launch""" def test_launch_dataset_success(self): """Happy: launch returns 201 with redirect URL.""" session = _make_session(version=1, environment_id="env-1") run_ctx = MagicMock(spec=RunContextResult) run_ctx.sql_lab_session_ref = "sql-lab-ref-123" launch_result = MagicMock(spec=LaunchDatasetResult) launch_result.run_context = run_ctx launch_result.session = session orch = MagicMock() orch.launch_dataset.return_value = launch_result repo = MagicMock() repo.load_session_detail.return_value = session repo.require_session_version = MagicMock(return_value=session) repo.commit_session_mutation = MagicMock() repo.db = MagicMock() cm = _make_config_manager() _setup_deps(repository=repo, orchestrator=orch, session=session) resp = client.post( "/api/dataset-orchestration/sessions/sess-1/launch", headers={"X-Session-Version": "1"}, ) assert resp.status_code == 201 data = resp.json() assert "redirect_url" in data