745 lines
26 KiB
Python
745 lines
26 KiB
Python
# #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
|