#region Test.DatasetReview.RoutesExtended [C:2] [TYPE Module] [SEMANTICS test,dataset,review,routes,error,edge] # @BRIEF Extended API route tests for dataset_review_pkg/_routes.py — error paths and edge cases. # @RELATION BINDS_TO -> [DatasetReviewRoutes] # @TEST_EDGE: all_flags_disabled -> All endpoints return 404 when dataset_review feature is off # @TEST_EDGE: non_owner_mutation -> Mutation endpoints return 403 for non-owner access # @TEST_EDGE: missing_session_version_header -> 422 when X-Session-Version missing # @TEST_EDGE: preview_blocked -> 409 when preview is blocked by readiness gates # @TEST_EDGE: launch_blocked -> 409 when launch is blocked by readiness gates # @TEST_EDGE: unsupported_export_format -> 400 for non-json/non-markdown export format # @TEST_EDGE: start_session_env_not_found -> 404 when environment not found # @TEST_EDGE: clarification_feedback_no_answer -> 400 when question has no answer yet # @TEST_EDGE: mapping_update_no_effective_value -> 400 when effective_value is null # @TEST_EDGE: preview_session_not_found -> 404 when session not found by orchestrator 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, CandidateMatchType, CandidateStatus, ClarificationAnswer, ClarificationOption, ClarificationQuestion, ClarificationSession, ClarificationStatus, CompiledPreview, DatasetReviewSession, ExecutionMapping, FieldKind, FieldProvenance, FindingArea, FindingSeverity, ImportedFilter, LaunchStatus, MappingMethod, MappingWarningLevel, PreviewStatus, QuestionState, ReadinessState, RecommendedAction, ResolutionState, SemanticCandidate, SemanticFieldEntry, SemanticSource, SemanticSourceStatus, SemanticSourceType, SessionPhase, SessionStatus, TemplateVariable, TrustLevel, VariableKind, ) 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, StartSessionCommand, StartSessionResult, ) from src.services.dataset_review.repositories.session_repository import ( DatasetReviewSessionVersionConflictError, ) client = TestClient(app) # ── Shared fixture: setup mocks ──────────────────────────────────────── @pytest.fixture(autouse=True) def _clean_overrides(): """Reset dependency overrides before and after each test.""" app.dependency_overrides.clear() yield app.dependency_overrides.clear() def _make_user(roles=None): """Build a minimal User-like object with permissions.""" if roles is None: permissions = [ SimpleNamespace(resource="dataset:session", action="READ"), SimpleNamespace(resource="dataset:session", action="MANAGE"), SimpleNamespace(resource="dataset:execution:launch", action="EXECUTE"), ] role = SimpleNamespace(name="DatasetReviewOperator", permissions=permissions) roles = [role] return SimpleNamespace(id="user-1", username="tester", roles=roles) def _make_config_manager(feature_flag=True, ff_auto=True, ff_clarification=True, ff_execution=True): """Build a mock config_manager with controllable feature flags.""" 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 = feature_flag config.settings.ff_dataset_auto_review = ff_auto config.settings.ff_dataset_clarification = ff_clarification config.settings.ff_dataset_execution = ff_execution 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 _setup_feature_disabled(flags=("auto",)): """Override deps so all features are disabled.""" cm = _make_config_manager( feature_flag=False, ff_auto="auto" in flags, ff_clarification="clarification" in flags, ff_execution="execution" in flags, ) user = _make_user() app.dependency_overrides[get_config_manager] = lambda: cm app.dependency_overrides[get_current_user] = lambda: user def _setup_happy_deps(session=None, repository=None, orchestrator=None, clarification_engine=None, feature_flag=True, use_launch_permission=False): """Standard dependency setup with feature flags enabled.""" if use_launch_permission: user_roles = [ SimpleNamespace( name="DatasetReviewOperator", permissions=[ SimpleNamespace(resource="dataset:session", action="READ"), SimpleNamespace(resource="dataset:session", action="MANAGE"), SimpleNamespace(resource="dataset:execution:launch", action="EXECUTE"), ], ) ] else: user_roles = None user = _make_user(roles=user_roles) cm = _make_config_manager(feature_flag=feature_flag) effective_session = session or _make_minimal_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.side_effect = lambda s, v: s repository.bump_session_version.side_effect = ( lambda current: setattr( current, "version", int(getattr(current, "version", 0) or 0) + 1 ) or getattr(current, "version", 0) ) 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 if repository is not None: app.dependency_overrides[_get_repository] = lambda: repository if orchestrator is not None: app.dependency_overrides[_get_orchestrator] = lambda: orchestrator if clarification_engine is not None: app.dependency_overrides[_get_clarification_engine] = lambda: clarification_engine return { "user": user, "config_manager": cm, "repository": repository, "orchestrator": orchestrator, } def _make_minimal_session(): """Build a minimal session mock for error path tests.""" now = datetime.now(UTC) s = MagicMock(spec=DatasetReviewSession) s.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 = ReadinessState.REVIEW_READY s.recommended_action = RecommendedAction.REVIEW_DOCUMENTATION s.status = SessionStatus.ACTIVE s.current_phase = SessionPhase.REVIEW s.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 = [] s.imported_filters = [] s.template_variables = [] s.execution_mappings = [] s.clarification_sessions = [] s.previews = [] s.run_contexts = [] return s # ── Feature-flag disabled → 404 all endpoints ────────────────────────── FEATURE_DISABLED_ENDPOINTS = [ ("GET", "/api/dataset-orchestration/sessions"), ("POST", "/api/dataset-orchestration/sessions"), ("GET", "/api/dataset-orchestration/sessions/sess-1"), ("PATCH", "/api/dataset-orchestration/sessions/sess-1"), ("DELETE", "/api/dataset-orchestration/sessions/sess-1"), ("GET", "/api/dataset-orchestration/sessions/sess-1/exports/documentation"), ("GET", "/api/dataset-orchestration/sessions/sess-1/exports/validation"), ("GET", "/api/dataset-orchestration/sessions/sess-1/clarification"), ("POST", "/api/dataset-orchestration/sessions/sess-1/clarification/resume"), ("POST", "/api/dataset-orchestration/sessions/sess-1/clarification/answers"), ("PATCH", "/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic"), ("POST", "/api/dataset-orchestration/sessions/sess-1/fields/field-1/lock"), ("POST", "/api/dataset-orchestration/sessions/sess-1/fields/field-1/unlock"), ("POST", "/api/dataset-orchestration/sessions/sess-1/fields/semantic/approve-batch"), ("GET", "/api/dataset-orchestration/sessions/sess-1/mappings"), ("PATCH", "/api/dataset-orchestration/sessions/sess-1/mappings/map-1"), ("POST", "/api/dataset-orchestration/sessions/sess-1/mappings/map-1/approve"), ("POST", "/api/dataset-orchestration/sessions/sess-1/mappings/approve-batch"), ("POST", "/api/dataset-orchestration/sessions/sess-1/preview"), ("POST", "/api/dataset-orchestration/sessions/sess-1/launch"), ("POST", "/api/dataset-orchestration/sessions/sess-1/fields/field-1/feedback"), ("POST", "/api/dataset-orchestration/sessions/sess-1/clarification/questions/q-1/feedback"), ] #region test_all_endpoints_return_404_when_feature_disabled [C:2] # @BRIEF Every dataset-orchestration endpoint returns 404 when dataset_review feature is off. @pytest.mark.parametrize("method,path", FEATURE_DISABLED_ENDPOINTS) def test_all_endpoints_return_404_when_feature_disabled(method, path): _setup_feature_disabled() _setup_happy_deps(feature_flag=False) kwargs = {} if method in ("POST", "PATCH"): kwargs["json"] = {} kwargs["headers"] = {"X-Session-Version": "0"} response = client.request(method, path, **kwargs) assert response.status_code == 404, f"{method} {path} should return 404" assert "Dataset review feature is disabled" in response.json()["detail"] #endregion # ── Missing X-Session-Version header → 422 ────────────────────────────── #region test_missing_session_version_header_422 [C:2] # @BRIEF PATCH session returns 422 when X-Session-Version header missing. def test_missing_session_version_header_422(): _setup_happy_deps() response = client.patch( "/api/dataset-orchestration/sessions/sess-1", json={"status": "paused"}, # No X-Session-Version header ) # FastAPI returns 422 for missing required header assert response.status_code == 422 #endregion #region test_missing_session_version_header_all_mutation_endpoints [C:2] # @BRIEF All mutation endpoints return 422 when X-Session-Version missing. @pytest.mark.parametrize("method,path", [ ("PATCH", "/api/dataset-orchestration/sessions/sess-1"), ("DELETE", "/api/dataset-orchestration/sessions/sess-1"), ("POST", "/api/dataset-orchestration/sessions/sess-1/clarification/resume"), ("POST", "/api/dataset-orchestration/sessions/sess-1/clarification/answers"), ("PATCH", "/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic"), ("POST", "/api/dataset-orchestration/sessions/sess-1/fields/field-1/lock"), ("POST", "/api/dataset-orchestration/sessions/sess-1/fields/field-1/unlock"), ("POST", "/api/dataset-orchestration/sessions/sess-1/fields/semantic/approve-batch"), ("PATCH", "/api/dataset-orchestration/sessions/sess-1/mappings/map-1"), ("POST", "/api/dataset-orchestration/sessions/sess-1/mappings/map-1/approve"), ("POST", "/api/dataset-orchestration/sessions/sess-1/mappings/approve-batch"), ("POST", "/api/dataset-orchestration/sessions/sess-1/preview"), ("POST", "/api/dataset-orchestration/sessions/sess-1/launch"), ("POST", "/api/dataset-orchestration/sessions/sess-1/fields/field-1/feedback"), ("POST", "/api/dataset-orchestration/sessions/sess-1/clarification/questions/q-1/feedback"), ]) def test_missing_session_version_header_422_all(method, path): _setup_happy_deps() kwargs = {"json": {}} if method in ("POST", "PATCH") else {} response = client.request(method, path, **kwargs) assert response.status_code == 422, f"{method} {path} should return 422" #endregion # ── Non-owner mutation → 403 ──────────────────────────────────────────── def _make_non_owner_session(): s = _make_minimal_session() s.user_id = "user-2" # Different from current user (user-1) return s #region test_patch_session_non_owner_403 [C:2] # @BRIEF PATCH session returns 403 for non-owner. def test_patch_session_non_owner_403(): session = _make_non_owner_session() _setup_happy_deps(session=session) response = client.patch( "/api/dataset-orchestration/sessions/sess-1", json={"status": "paused"}, headers={"X-Session-Version": "0"}, ) assert response.status_code == 403 assert "Only the owner can mutate" in response.json()["detail"] #endregion #region test_delete_session_non_owner_403 [C:2] # @BRIEF DELETE session returns 403 for non-owner. def test_delete_session_non_owner_403(): session = _make_non_owner_session() _setup_happy_deps(session=session) response = client.delete( "/api/dataset-orchestration/sessions/sess-1", headers={"X-Session-Version": "0"}, ) assert response.status_code == 403 #endregion #region test_update_field_semantic_non_owner_403 [C:2] # @BRIEF Field semantic update returns 403 for non-owner. def test_update_field_semantic_non_owner_403(): session = _make_non_owner_session() _setup_happy_deps(session=session) response = client.patch( "/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic", json={"candidate_id": "cand-1"}, headers={"X-Session-Version": "0"}, ) assert response.status_code == 403 #endregion # ── Start session error paths ────────────────────────────────────────── #region test_start_session_environment_not_found_404 [C:2] # @BRIEF POST session returns 404 when environment not found. def test_start_session_environment_not_found_404(): orchestrator = MagicMock() orchestrator.start_session.side_effect = ValueError("Environment not found") _setup_happy_deps(orchestrator=orchestrator) response = client.post( "/api/dataset-orchestration/sessions", json={ "source_kind": "superset_link", "source_input": "http://superset.local/dashboard/10", "environment_id": "env-999", }, ) assert response.status_code == 404 assert "Environment not found" in response.json()["detail"] #endregion #region test_start_session_other_error_400 [C:2] # @BRIEF POST session returns 400 for generic ValueError. def test_start_session_other_error_400(): orchestrator = MagicMock() orchestrator.start_session.side_effect = ValueError("Invalid input") _setup_happy_deps(orchestrator=orchestrator) response = client.post( "/api/dataset-orchestration/sessions", json={ "source_kind": "superset_link", "source_input": "invalid", "environment_id": "env-1", }, ) assert response.status_code == 400 assert "Invalid input" in response.json()["detail"] #endregion # ── Export error paths ────────────────────────────────────────────────── #region test_export_documentation_unsupported_format_400 [C:2] # @BRIEF GET exports/documentation returns 400 for unsupported format. def test_export_documentation_unsupported_format_400(): _setup_happy_deps() response = client.get( "/api/dataset-orchestration/sessions/sess-1/exports/documentation?format=csv" ) assert response.status_code == 400 assert "Only json and markdown exports are supported" in response.json()["detail"] #endregion #region test_export_validation_unsupported_format_422 [C:2] # @BRIEF GET exports/validation returns 422 for unsupported format (FastAPI enum validation). def test_export_validation_unsupported_format_422(): _setup_happy_deps() response = client.get( "/api/dataset-orchestration/sessions/sess-1/exports/validation?format=xml" ) assert response.status_code == 422 # FastAPI validates against ArtifactFormat enum #endregion # ── Preview error paths ───────────────────────────────────────────────── #region test_preview_blocked_409 [C:2] # @BRIEF POST preview returns 409 when preview is blocked by readiness gates. def test_preview_blocked_409(): session = _make_minimal_session() orchestrator = MagicMock() orchestrator.prepare_launch_preview.side_effect = ValueError( "Preview blocked: unresolved mappings" ) _setup_happy_deps(session=session, orchestrator=orchestrator) response = client.post( "/api/dataset-orchestration/sessions/sess-1/preview", headers={"X-Session-Version": "0"}, ) assert response.status_code == 409 assert "Preview blocked" in response.json()["detail"] #endregion #region test_preview_session_not_found_404 [C:2] # @BRIEF POST preview returns 404 when session not found by orchestrator. def test_preview_session_not_found_404(): orchestrator = MagicMock() orchestrator.prepare_launch_preview.side_effect = ValueError("Session not found") _setup_happy_deps(orchestrator=orchestrator) response = client.post( "/api/dataset-orchestration/sessions/sess-1/preview", headers={"X-Session-Version": "0"}, ) assert response.status_code == 404 assert "Session not found" in response.json()["detail"] #endregion #region test_preview_env_not_found_404 [C:2] # @BRIEF POST preview returns 404 when environment not found. def test_preview_env_not_found_404(): orchestrator = MagicMock() orchestrator.prepare_launch_preview.side_effect = ValueError("Environment not found") _setup_happy_deps(orchestrator=orchestrator) response = client.post( "/api/dataset-orchestration/sessions/sess-1/preview", headers={"X-Session-Version": "0"}, ) assert response.status_code == 404 assert "Environment not found" in response.json()["detail"] #endregion #region test_preview_other_value_error_400 [C:2] # @BRIEF POST preview returns 400 for other ValueError. def test_preview_other_value_error_400(): orchestrator = MagicMock() orchestrator.prepare_launch_preview.side_effect = ValueError("Some other error") _setup_happy_deps(orchestrator=orchestrator) response = client.post( "/api/dataset-orchestration/sessions/sess-1/preview", headers={"X-Session-Version": "0"}, ) assert response.status_code == 400 assert "Some other error" in response.json()["detail"] #endregion #region test_preview_version_conflict_409 [C:2] # @BRIEF POST preview returns 409 when session version conflicts. def test_preview_version_conflict_409(): orchestrator = MagicMock() orchestrator.prepare_launch_preview.side_effect = ( DatasetReviewSessionVersionConflictError("sess-1", 0, 3) ) _setup_happy_deps(orchestrator=orchestrator) response = client.post( "/api/dataset-orchestration/sessions/sess-1/preview", headers={"X-Session-Version": "0"}, ) assert response.status_code == 409 payload = response.json()["detail"] assert payload["error_code"] == "session_version_conflict" #endregion # ── Launch error paths ────────────────────────────────────────────────── #region test_launch_blocked_409 [C:2] # @BRIEF POST launch returns 409 when launch is blocked. def test_launch_blocked_409(): session = _make_minimal_session() orchestrator = MagicMock() orchestrator.launch_dataset.side_effect = ValueError( "Launch blocked: preview required" ) _setup_happy_deps(session=session, orchestrator=orchestrator, use_launch_permission=True) response = client.post( "/api/dataset-orchestration/sessions/sess-1/launch", headers={"X-Session-Version": "0"}, ) assert response.status_code == 409 assert "Launch blocked" in response.json()["detail"] #endregion #region test_launch_session_not_found_404 [C:2] # @BRIEF POST launch returns 404 when session not found. def test_launch_session_not_found_404(): orchestrator = MagicMock() orchestrator.launch_dataset.side_effect = ValueError("Session not found") _setup_happy_deps(orchestrator=orchestrator, use_launch_permission=True) response = client.post( "/api/dataset-orchestration/sessions/sess-1/launch", headers={"X-Session-Version": "0"}, ) assert response.status_code == 404 #endregion #region test_launch_env_not_found_404 [C:2] # @BRIEF POST launch returns 404 when environment not found. def test_launch_env_not_found_404(): orchestrator = MagicMock() orchestrator.launch_dataset.side_effect = ValueError("Environment not found") _setup_happy_deps(orchestrator=orchestrator, use_launch_permission=True) response = client.post( "/api/dataset-orchestration/sessions/sess-1/launch", headers={"X-Session-Version": "0"}, ) assert response.status_code == 404 #endregion #region test_launch_version_conflict_409 [C:2] # @BRIEF POST launch returns 409 when session version conflicts. def test_launch_version_conflict_409(): orchestrator = MagicMock() orchestrator.launch_dataset.side_effect = ( DatasetReviewSessionVersionConflictError("sess-1", 0, 3) ) _setup_happy_deps(orchestrator=orchestrator, use_launch_permission=True) response = client.post( "/api/dataset-orchestration/sessions/sess-1/launch", headers={"X-Session-Version": "0"}, ) assert response.status_code == 409 payload = response.json()["detail"] assert payload["error_code"] == "session_version_conflict" #endregion # ── Clarification error paths ────────────────────────────────────────── #region test_clarification_feedback_no_answer_400 [C:2] # @BRIEF POST clarification feedback returns 400 when question has no answer yet. def test_clarification_feedback_no_answer_400(): """Feedback on unanswered question → 400.""" session = _make_minimal_session() q = MagicMock() q.question_id = "q-1" q.answer = None cs = MagicMock() cs.questions = [q] session.clarification_sessions = [cs] _setup_happy_deps(session=session) response = client.post( "/api/dataset-orchestration/sessions/sess-1/clarification/questions/q-1/feedback", json={"feedback": "up"}, headers={"X-Session-Version": "0"}, ) assert response.status_code == 400 assert "Clarification answer not found" in response.json()["detail"] #endregion #region test_clarification_feedback_question_not_found_404 [C:2] # @BRIEF POST clarification feedback returns 404 when question not found. def test_clarification_feedback_question_not_found_404(): session = _make_minimal_session() cs = MagicMock() cs.questions = [MagicMock(question_id="q-other")] session.clarification_sessions = [cs] _setup_happy_deps(session=session) response = client.post( "/api/dataset-orchestration/sessions/sess-1/clarification/questions/q-999/feedback", json={"feedback": "up"}, headers={"X-Session-Version": "0"}, ) assert response.status_code == 404 assert "Clarification question not found" in response.json()["detail"] #endregion #region test_clarification_answer_error_400 [C:2] # @BRIEF POST clarification answer returns 400 when engine raises ValueError. def test_clarification_answer_error_400(): session = _make_minimal_session() cs = MagicMock() cs.questions = [] session.clarification_sessions = [cs] clarification_engine = MagicMock() clarification_engine.record_answer.side_effect = ValueError("Invalid answer kind") _setup_happy_deps(session=session, clarification_engine=clarification_engine) response = client.post( "/api/dataset-orchestration/sessions/sess-1/clarification/answers", json={ "question_id": "q-1", "answer_kind": "selected", "answer_value": "test", }, headers={"X-Session-Version": "0"}, ) assert response.status_code == 400 #endregion # ── Execution mapping error paths ────────────────────────────────────── #region test_update_execution_mapping_no_effective_value_400 [C:2] # @BRIEF PATCH mapping returns 400 when effective_value is null. def test_update_execution_mapping_no_effective_value_400(): session = _make_minimal_session() mapping = MagicMock() mapping.mapping_id = "map-1" session.execution_mappings = [mapping] _setup_happy_deps(session=session) response = client.patch( "/api/dataset-orchestration/sessions/sess-1/mappings/map-1", json={"effective_value": None}, headers={"X-Session-Version": "0"}, ) assert response.status_code == 400 assert "effective_value is required" in response.json()["detail"] #endregion #region test_get_mapping_404 [C:2] # @BRIEF GET mappings returns empty list when session has no mappings. def test_get_mapping_404(): session = _make_minimal_session() session.execution_mappings = [] _setup_happy_deps(session=session) response = client.get( "/api/dataset-orchestration/sessions/sess-1/mappings", ) assert response.status_code == 200 assert response.json()["items"] == [] #endregion # ── Session not found for detail endpoint ─────────────────────────────── #region test_get_session_detail_404 [C:2] # @BRIEF GET session detail returns 404 when session not found. def test_get_session_detail_404(): repository = MagicMock() repository.load_session_detail.return_value = None _setup_happy_deps(repository=repository) response = client.get("/api/dataset-orchestration/sessions/sess-999") assert response.status_code == 404 #endregion # ── DELETE session hard_delete ────────────────────────────────────────── #region test_delete_session_hard_delete [C:2] # @BRIEF DELETE session with hard_delete=true deletes from DB. def test_delete_session_hard_delete(): session = _make_minimal_session() repository = MagicMock() repository.load_session_detail.return_value = session repository.require_session_version.side_effect = lambda s, v: s repository.db = MagicMock() _setup_happy_deps(session=session, repository=repository) response = client.delete( "/api/dataset-orchestration/sessions/sess-1?hard_delete=true", headers={"X-Session-Version": "0"}, ) assert response.status_code == 204 repository.db.delete.assert_called_once_with(session) repository.db.commit.assert_called_once() #endregion # ── Lock field ────────────────────────────────────────────────────────── #region test_lock_field_endpoint [C:2] # @BRIEF POST field lock locks the field and marks provenance. def test_lock_field_endpoint(): field = MagicMock() field.field_id = "field-1" field.session_id = "sess-1" field.field_name = "revenue" field.field_kind = FieldKind.METRIC 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 field.user_feedback = None field.created_at = datetime.now(UTC) field.updated_at = datetime.now(UTC) field.candidates = [] field.session = MagicMock(version=2) session = _make_minimal_session() session.semantic_fields = [field] repository = MagicMock() repository.load_session_detail.return_value = session repository.require_session_version.side_effect = lambda s, v: s repository.bump_session_version.side_effect = ( lambda current: setattr(current, "version", getattr(current, "version", 0) + 1) or getattr(current, "version", 0) ) repository.db = MagicMock() repository.event_logger = MagicMock(spec=SessionEventLogger) repository.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-1" ) _setup_happy_deps(session=session, repository=repository) response = client.post( "/api/dataset-orchestration/sessions/sess-1/fields/field-1/lock", headers={"X-Session-Version": "0"}, ) assert response.status_code == 200 assert response.json()["is_locked"] is True assert response.json()["field_id"] == "field-1" #endregion # ── Unlock field with non-manual provenance ───────────────────────────── #region test_unlock_field_non_manual_provenance [C:2] # @BRIEF Unlock field with non-manual provenance doesn't reset to UNRESOLVED. def test_unlock_field_non_manual_provenance(): field = MagicMock() field.field_id = "field-1" field.session_id = "sess-1" field.field_name = "revenue" field.field_kind = FieldKind.METRIC field.provenance = FieldProvenance.DICTIONARY_EXACT field.is_locked = True field.needs_review = False field.last_changed_by = "system" field.verbose_name = "Revenue" field.description = "Total revenue" field.display_format = "$,.0f" field.source_id = None field.source_version = None field.confidence_rank = 1 field.has_conflict = False field.user_feedback = None field.created_at = datetime.now(UTC) field.updated_at = datetime.now(UTC) field.candidates = [] field.session = MagicMock(version=2) session = _make_minimal_session() session.semantic_fields = [field] repository = MagicMock() repository.load_session_detail.return_value = session repository.require_session_version.side_effect = lambda s, v: s repository.bump_session_version.side_effect = ( lambda current: setattr(current, "version", getattr(current, "version", 0) + 1) or getattr(current, "version", 0) ) repository.db = MagicMock() repository.event_logger = MagicMock(spec=SessionEventLogger) repository.event_logger.log_for_session.return_value = SimpleNamespace( session_event_id="evt-1" ) _setup_happy_deps(session=session, repository=repository) response = client.post( "/api/dataset-orchestration/sessions/sess-1/fields/field-1/unlock", headers={"X-Session-Version": "0"}, ) assert response.status_code == 200 assert response.json()["is_locked"] is False # Non-MANUAL_OVERRIDE provenance stays intact assert response.json()["provenance"] == "dictionary_exact" #endregion # ── Endpoint not found ────────────────────────────────────────────────── #region test_endpoint_not_found_404 [C:2] # @BRIEF Returns 404 for unknown dataset-orchestration routes. def test_endpoint_not_found_404(): _setup_happy_deps() response = client.get("/api/dataset-orchestration/nonexistent") assert response.status_code == 404 #endregion #endregion Test.DatasetReview.RoutesExtended