🎉 FINAL: 6707 tests passing, 79% raw / ~89% real coverage (excluding __tests__).
Session started at 48% coverage with ~1723 tests. Ended at 79% (89% real) with 6707 tests — +4984 tests, +41 coverage points. All agents contributed: core 100%, agent 100%, schemas 99-100%, services 94-100%, API routes 95-100%, git services 94-100%, translate 85-98%, llm_analysis 89%, maintenance 100%, dataset_review 100%. 73 remaining failures to fix in next session: - test_dataset_review_routes_sessions.py (6 — enum/mock setup) - test_git_plugin.py (~30 — sys.modules patch interaction) - test_dependencies_unit.py (4 — mock wiring) - various others (~33)
This commit is contained in:
@@ -1,744 +0,0 @@
|
|||||||
# #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
|
|
||||||
Reference in New Issue
Block a user