673 lines
28 KiB
Python
673 lines
28 KiB
Python
# #region Test.DatasetReview.Orchestrator [C:3] [TYPE Module] [SEMANTICS test,dataset,orchestrator,session,launch,preview]
|
|
# @BRIEF Tests for orchestrator.py — DatasetReviewOrchestrator: start_session, prepare_launch_preview,
|
|
# launch_dataset, _build_recovery_bootstrap, _enqueue_recovery_task.
|
|
# @RELATION BINDS_TO -> [DatasetReviewOrchestrator]
|
|
# @TEST_EDGE: start_session -> Superset link, dataset selection, validation errors
|
|
# @TEST_EDGE: prepare_launch_preview -> Success, missing session, preview blockers
|
|
# @TEST_EDGE: launch_dataset -> Success, failed SQL Lab, launch blockers
|
|
# @TEST_EDGE: _build_recovery_bootstrap -> Filter recovery, template variable discovery, execution mappings
|
|
# @TEST_EDGE: _enqueue_recovery_task -> Task linked, task manager absent, incompatible contract
|
|
|
|
from __future__ import annotations
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
|
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import MagicMock, AsyncMock, patch, PropertyMock, call
|
|
import pytest
|
|
|
|
|
|
# ── Fixtures ────────────────────────────────────────────────
|
|
|
|
@pytest.fixture
|
|
def mock_repository():
|
|
"""Mock DatasetReviewSessionRepository."""
|
|
repo = MagicMock()
|
|
repo.db = MagicMock()
|
|
repo.event_logger = MagicMock()
|
|
repo.bump_session_version = MagicMock(return_value=2)
|
|
return repo
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_config_manager():
|
|
"""Mock ConfigManager.get_environment."""
|
|
cm = MagicMock()
|
|
env = MagicMock()
|
|
env.id = "env-1"
|
|
env.name = "Test Environment"
|
|
cm.get_environment.return_value = env
|
|
return cm
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_user():
|
|
from src.models.auth import User
|
|
user = MagicMock(spec=User)
|
|
user.id = "user-1"
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def orchestrator(mock_repository, mock_config_manager):
|
|
from src.services.dataset_review.orchestrator import DatasetReviewOrchestrator
|
|
return DatasetReviewOrchestrator(
|
|
repository=mock_repository,
|
|
config_manager=mock_config_manager,
|
|
)
|
|
|
|
|
|
# ── Tests for start_session ────────────────────────────────
|
|
|
|
class TestStartSession:
|
|
"""start_session — session creation with validation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_session_dataset_selection(self, orchestrator, mock_repository, mock_user):
|
|
"""Dataset selection creates session with REVIEW_READY state."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import StartSessionCommand
|
|
|
|
command = StartSessionCommand(
|
|
source_kind="dataset_selection",
|
|
source_input="42",
|
|
environment_id="env-1",
|
|
user=mock_user,
|
|
)
|
|
|
|
# Setup repository mock chain
|
|
persisted_session = MagicMock()
|
|
persisted_session.session_id = "sess-1"
|
|
persisted_session.current_phase = MagicMock(value="review")
|
|
persisted_session.readiness_state = MagicMock(value="review_ready")
|
|
persisted_session.source_kind = "dataset_selection"
|
|
persisted_session.dataset_ref = "dataset:42"
|
|
persisted_session.dataset_id = 42
|
|
persisted_session.dashboard_id = None
|
|
persisted_session.active_task_id = None
|
|
persisted_session.user_id = "user-1"
|
|
mock_repository.create_session.return_value = persisted_session
|
|
mock_repository.save_profile_and_findings.return_value = persisted_session
|
|
mock_repository.save_recovery_state.return_value = persisted_session
|
|
|
|
with patch("src.services.dataset_review.orchestrator.build_initial_profile") as mock_build_profile, \
|
|
patch("src.services.dataset_review.orchestrator.parse_dataset_selection", return_value=("dataset:42", 42)):
|
|
mock_profile = MagicMock()
|
|
mock_build_profile.return_value = mock_profile
|
|
|
|
result = await orchestrator.start_session(command)
|
|
|
|
assert result.session is not None
|
|
assert result.session.session_id == "sess-1"
|
|
mock_repository.create_session.assert_called_once()
|
|
mock_repository.save_profile_and_findings.assert_called_once()
|
|
mock_repository.event_logger.log_event.assert_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_session_empty_source_raises(self, orchestrator, mock_user):
|
|
"""Empty source_input raises ValueError."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import StartSessionCommand
|
|
|
|
command = StartSessionCommand(
|
|
source_kind="dataset_selection",
|
|
source_input="",
|
|
environment_id="env-1",
|
|
user=mock_user,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="non-empty"):
|
|
await orchestrator.start_session(command)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_session_invalid_source_kind_raises(self, orchestrator, mock_user):
|
|
"""Invalid source_kind raises ValueError."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import StartSessionCommand
|
|
|
|
command = StartSessionCommand(
|
|
source_kind="invalid_kind",
|
|
source_input="test",
|
|
environment_id="env-1",
|
|
user=mock_user,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="source_kind must be"):
|
|
await orchestrator.start_session(command)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_session_environment_not_found(self, orchestrator, mock_config_manager, mock_user):
|
|
"""Missing environment raises ValueError."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import StartSessionCommand
|
|
mock_config_manager.get_environment.return_value = None
|
|
|
|
command = StartSessionCommand(
|
|
source_kind="dataset_selection",
|
|
source_input="42",
|
|
environment_id="nonexistent",
|
|
user=mock_user,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Environment not found"):
|
|
await orchestrator.start_session(command)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_session_superset_link_partial_recovery(self, orchestrator, mock_repository, mock_config_manager, mock_user):
|
|
"""Superset link with partial recovery creates RECOVERY_REQUIRED session."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import StartSessionCommand
|
|
|
|
# Mock parsed context
|
|
parsed_context = MagicMock()
|
|
parsed_context.partial_recovery = True
|
|
parsed_context.unresolved_references = ["dataset_column_missing"]
|
|
parsed_context.dataset_ref = "schema.table"
|
|
parsed_context.dataset_id = 42
|
|
parsed_context.dashboard_id = 99
|
|
parsed_context.dataset_payload = {"id": 42}
|
|
|
|
command = StartSessionCommand(
|
|
source_kind="superset_link",
|
|
source_input="http://superset.example.com/dashboard/99",
|
|
environment_id="env-1",
|
|
user=mock_user,
|
|
)
|
|
|
|
persisted_session = MagicMock()
|
|
persisted_session.session_id = "sess-2"
|
|
persisted_session.current_phase = MagicMock(value="recovery")
|
|
persisted_session.readiness_state = MagicMock(value="recovery_required")
|
|
persisted_session.source_kind = "superset_link"
|
|
persisted_session.dataset_ref = "schema.table"
|
|
persisted_session.dataset_id = 42
|
|
persisted_session.dashboard_id = 99
|
|
persisted_session.active_task_id = None
|
|
persisted_session.user_id = "user-1"
|
|
mock_repository.create_session.return_value = persisted_session
|
|
mock_repository.save_profile_and_findings.return_value = persisted_session
|
|
mock_repository.save_recovery_state.return_value = persisted_session
|
|
|
|
with patch("src.services.dataset_review.orchestrator.SupersetContextExtractor") as mock_extractor_cls, \
|
|
patch("src.services.dataset_review.orchestrator.build_initial_profile") as mock_build_profile, \
|
|
patch("src.services.dataset_review.orchestrator.build_partial_recovery_findings") as mock_build_findings:
|
|
|
|
mock_extractor = MagicMock()
|
|
mock_extractor.parse_superset_link = AsyncMock(return_value=parsed_context)
|
|
mock_extractor.recover_imported_filters.return_value = []
|
|
mock_extractor.discover_template_variables.return_value = []
|
|
mock_extractor_cls.return_value = mock_extractor
|
|
|
|
mock_profile = MagicMock()
|
|
mock_build_profile.return_value = mock_profile
|
|
mock_build_findings.return_value = [MagicMock()]
|
|
|
|
result = await orchestrator.start_session(command)
|
|
|
|
assert result is not None
|
|
assert result.session.session_id == "sess-2"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_session_with_task_manager(self, orchestrator, mock_repository, mock_config_manager, mock_user):
|
|
"""Session start with task manager enqueues recovery task."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import StartSessionCommand
|
|
|
|
# Attach task manager
|
|
mock_task_manager = MagicMock()
|
|
task_object = MagicMock()
|
|
task_object.id = "task-1"
|
|
mock_task_manager.create_task.return_value = task_object
|
|
orchestrator.task_manager = mock_task_manager
|
|
|
|
command = StartSessionCommand(
|
|
source_kind="dataset_selection",
|
|
source_input="42",
|
|
environment_id="env-1",
|
|
user=mock_user,
|
|
)
|
|
|
|
persisted_session = MagicMock()
|
|
persisted_session.session_id = "sess-3"
|
|
persisted_session.current_phase = MagicMock(value="review")
|
|
persisted_session.readiness_state = MagicMock(value="review_ready")
|
|
persisted_session.source_kind = "dataset_selection"
|
|
persisted_session.dataset_ref = "dataset:42"
|
|
persisted_session.dataset_id = 42
|
|
persisted_session.dashboard_id = None
|
|
persisted_session.active_task_id = None
|
|
persisted_session.user_id = "user-1"
|
|
persisted_session.environment_id = "env-1"
|
|
persisted_session.source_input = "42"
|
|
mock_repository.create_session.return_value = persisted_session
|
|
mock_repository.save_profile_and_findings.return_value = persisted_session
|
|
|
|
with patch("src.services.dataset_review.orchestrator.build_initial_profile") as mock_build_profile, \
|
|
patch("src.services.dataset_review.orchestrator.parse_dataset_selection", return_value=("dataset:42", 42)):
|
|
mock_profile = MagicMock()
|
|
mock_build_profile.return_value = mock_profile
|
|
|
|
result = await orchestrator.start_session(command)
|
|
|
|
assert result.session.active_task_id == "task-1"
|
|
mock_repository.bump_session_version.assert_called()
|
|
mock_repository.db.commit.assert_called()
|
|
mock_repository.db.refresh.assert_called()
|
|
|
|
|
|
# ── Tests for prepare_launch_preview ───────────────────────
|
|
|
|
class TestPrepareLaunchPreview:
|
|
"""prepare_launch_preview — preview compilation."""
|
|
|
|
def test_prepare_launch_preview_success(self, orchestrator, mock_repository, mock_config_manager):
|
|
"""Successful preview compilation returns PreparePreviewResult."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import PreparePreviewCommand
|
|
from src.services.dataset_review.orchestrator_pkg._helpers import PreviewStatus
|
|
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.user_id = "user-1"
|
|
session.dataset_id = 42
|
|
session.environment_id = "env-1"
|
|
session.current_phase = MagicMock(value="review")
|
|
session.last_activity_at = None
|
|
session.version = 1
|
|
|
|
mock_repository.load_session_detail.return_value = session
|
|
|
|
# Mock execution_snapshot with no blockers
|
|
with patch("src.services.dataset_review.orchestrator.build_execution_snapshot") as mock_build_snapshot, \
|
|
patch("src.services.dataset_review.orchestrator.SupersetCompilationAdapter") as mock_adapter_cls, \
|
|
patch("src.services.dataset_review.orchestrator.build_launch_blockers") as mock_build_blockers:
|
|
|
|
mock_snapshot = {
|
|
"preview_blockers": [],
|
|
"preview_fingerprint": "fp-1",
|
|
"template_params": {"region": "US"},
|
|
"effective_filters": [],
|
|
}
|
|
mock_build_snapshot.return_value = mock_snapshot
|
|
|
|
mock_adapter = MagicMock()
|
|
mock_adapter_cls.return_value = mock_adapter
|
|
preview = MagicMock()
|
|
preview.preview_id = "pv-1"
|
|
preview.preview_status = PreviewStatus.READY
|
|
preview.preview_fingerprint = "fp-1"
|
|
mock_adapter.compile_preview.return_value = preview
|
|
|
|
persisted_preview = MagicMock()
|
|
persisted_preview.preview_id = "pv-1"
|
|
persisted_preview.preview_status = PreviewStatus.READY
|
|
mock_repository.save_preview.return_value = persisted_preview
|
|
|
|
mock_build_blockers.return_value = []
|
|
|
|
command = PreparePreviewCommand(
|
|
session_id="sess-1",
|
|
expected_version=1,
|
|
user=MagicMock(id="user-1"),
|
|
)
|
|
|
|
result = orchestrator.prepare_launch_preview(command)
|
|
|
|
assert result.session is not None
|
|
assert result.session.session_id == "sess-1"
|
|
|
|
def test_prepare_preview_session_not_found(self, orchestrator, mock_repository):
|
|
"""Missing session raises ValueError."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import PreparePreviewCommand
|
|
|
|
mock_repository.load_session_detail.return_value = None
|
|
|
|
command = PreparePreviewCommand(
|
|
session_id="nonexistent",
|
|
user=MagicMock(id="user-1"),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Session not found"):
|
|
orchestrator.prepare_launch_preview(command)
|
|
|
|
def test_prepare_preview_no_dataset_id(self, orchestrator, mock_repository):
|
|
"""Missing dataset_id raises ValueError."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import PreparePreviewCommand
|
|
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.user_id = "user-1"
|
|
session.dataset_id = None
|
|
session.environment_id = "env-1"
|
|
mock_repository.load_session_detail.return_value = session
|
|
|
|
command = PreparePreviewCommand(
|
|
session_id="sess-1",
|
|
user=MagicMock(id="user-1"),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="requires a resolved dataset_id"):
|
|
orchestrator.prepare_launch_preview(command)
|
|
|
|
def test_prepare_preview_env_not_found(self, orchestrator, mock_repository, mock_config_manager):
|
|
"""Missing environment raises ValueError."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import PreparePreviewCommand
|
|
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.user_id = "user-1"
|
|
session.dataset_id = 42
|
|
session.environment_id = "nonexistent"
|
|
session.current_phase = MagicMock(value="review")
|
|
mock_repository.load_session_detail.return_value = session
|
|
mock_config_manager.get_environment.return_value = None
|
|
|
|
command = PreparePreviewCommand(
|
|
session_id="sess-1",
|
|
user=MagicMock(id="user-1"),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Environment not found"):
|
|
orchestrator.prepare_launch_preview(command)
|
|
|
|
|
|
# ── Tests for launch_dataset ───────────────────────────────
|
|
|
|
class TestLaunchDataset:
|
|
"""launch_dataset — dataset execution launch."""
|
|
|
|
def test_launch_success(self, orchestrator, mock_repository, mock_config_manager):
|
|
"""Successful launch returns LaunchDatasetResult."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import LaunchDatasetCommand
|
|
from src.models.dataset_review import LaunchStatus
|
|
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.user_id = "user-1"
|
|
session.dataset_id = 42
|
|
session.environment_id = "env-1"
|
|
session.dataset_ref = "schema.table"
|
|
session.current_phase = MagicMock(value="preview")
|
|
session.last_activity_at = None
|
|
session.version = 1
|
|
mock_repository.load_session_detail.return_value = session
|
|
|
|
with patch("src.services.dataset_review.orchestrator.build_execution_snapshot") as mock_build_snapshot, \
|
|
patch("src.services.dataset_review.orchestrator.get_latest_preview") as mock_get_preview, \
|
|
patch("src.services.dataset_review.orchestrator.build_launch_blockers") as mock_build_blockers, \
|
|
patch("src.services.dataset_review.orchestrator.SupersetCompilationAdapter") as mock_adapter_cls:
|
|
|
|
mock_snapshot = {
|
|
"preview_blockers": [],
|
|
"preview_fingerprint": "fp-1",
|
|
"template_params": {"region": "US"},
|
|
"effective_filters": [],
|
|
"approved_mapping_ids": ["map-1"],
|
|
"semantic_decision_refs": [],
|
|
"open_warning_refs": [],
|
|
}
|
|
mock_build_snapshot.return_value = mock_snapshot
|
|
|
|
preview = MagicMock()
|
|
preview.preview_id = "pv-1"
|
|
preview.compiled_sql = "SELECT * FROM table"
|
|
preview.preview_fingerprint = "fp-1"
|
|
mock_get_preview.return_value = preview
|
|
|
|
mock_build_blockers.return_value = []
|
|
|
|
mock_adapter = MagicMock()
|
|
mock_adapter.create_sql_lab_session.return_value = "sql-lab-ref-1"
|
|
mock_adapter_cls.return_value = mock_adapter
|
|
|
|
run_context = MagicMock()
|
|
run_context.run_context_id = "rc-1"
|
|
run_context.launch_status = LaunchStatus.STARTED
|
|
run_context.preview_id = "pv-1"
|
|
run_context.sql_lab_session_ref = "sql-lab-ref-1"
|
|
mock_repository.save_run_context.return_value = run_context
|
|
|
|
command = LaunchDatasetCommand(
|
|
session_id="sess-1",
|
|
expected_version=1,
|
|
user=MagicMock(id="user-1"),
|
|
)
|
|
|
|
result = orchestrator.launch_dataset(command)
|
|
|
|
assert result.run_context is not None
|
|
assert result.run_context.run_context_id == "rc-1"
|
|
|
|
def test_launch_session_not_found(self, orchestrator, mock_repository):
|
|
"""Missing session raises ValueError."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import LaunchDatasetCommand
|
|
|
|
mock_repository.load_session_detail.return_value = None
|
|
|
|
command = LaunchDatasetCommand(
|
|
session_id="nonexistent",
|
|
user=MagicMock(id="user-1"),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Session not found"):
|
|
orchestrator.launch_dataset(command)
|
|
|
|
def test_launch_blocked_by_validation(self, orchestrator, mock_repository):
|
|
"""Launch blockers raise ValueError."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import LaunchDatasetCommand
|
|
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.user_id = "user-1"
|
|
session.dataset_id = 42
|
|
session.environment_id = "env-1"
|
|
session.current_phase = MagicMock(value="preview")
|
|
mock_repository.load_session_detail.return_value = session
|
|
|
|
with patch("src.services.dataset_review.orchestrator.build_execution_snapshot") as mock_build_snapshot, \
|
|
patch("src.services.dataset_review.orchestrator.get_latest_preview") as mock_get_preview, \
|
|
patch("src.services.dataset_review.orchestrator.build_launch_blockers") as mock_build_blockers:
|
|
|
|
mock_build_snapshot.return_value = {"preview_blockers": []}
|
|
preview = MagicMock()
|
|
preview.preview_fingerprint = "fp-1"
|
|
mock_get_preview.return_value = preview
|
|
mock_build_blockers.return_value = ["finding:MISSING:blocking"]
|
|
|
|
command = LaunchDatasetCommand(
|
|
session_id="sess-1",
|
|
user=MagicMock(id="user-1"),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Launch blocked"):
|
|
orchestrator.launch_dataset(command)
|
|
|
|
def test_launch_sql_lab_failure(self, orchestrator, mock_repository, mock_config_manager):
|
|
"""SQL Lab failure sets launch status to FAILED without raising."""
|
|
from src.services.dataset_review.orchestrator_pkg._commands import LaunchDatasetCommand
|
|
from src.models.dataset_review import LaunchStatus
|
|
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.user_id = "user-1"
|
|
session.dataset_id = 42
|
|
session.environment_id = "env-1"
|
|
session.dataset_ref = "schema.table"
|
|
session.current_phase = MagicMock(value="preview")
|
|
session.last_activity_at = None
|
|
session.version = 1
|
|
mock_repository.load_session_detail.return_value = session
|
|
|
|
with patch("src.services.dataset_review.orchestrator.build_execution_snapshot") as mock_build_snapshot, \
|
|
patch("src.services.dataset_review.orchestrator.get_latest_preview") as mock_get_preview, \
|
|
patch("src.services.dataset_review.orchestrator.build_launch_blockers") as mock_build_blockers, \
|
|
patch("src.services.dataset_review.orchestrator.SupersetCompilationAdapter") as mock_adapter_cls:
|
|
|
|
mock_build_snapshot.return_value = {
|
|
"preview_blockers": [],
|
|
"preview_fingerprint": "fp-1",
|
|
"template_params": {},
|
|
"effective_filters": [],
|
|
"approved_mapping_ids": [],
|
|
"semantic_decision_refs": [],
|
|
"open_warning_refs": [],
|
|
}
|
|
preview = MagicMock()
|
|
preview.preview_id = "pv-1"
|
|
preview.compiled_sql = "SELECT * FROM table"
|
|
preview.preview_fingerprint = "fp-1"
|
|
mock_get_preview.return_value = preview
|
|
mock_build_blockers.return_value = []
|
|
|
|
mock_adapter = MagicMock()
|
|
mock_adapter.create_sql_lab_session.side_effect = Exception("SQL Lab error")
|
|
mock_adapter_cls.return_value = mock_adapter
|
|
|
|
run_context = MagicMock()
|
|
run_context.run_context_id = "rc-1"
|
|
run_context.launch_status = LaunchStatus.FAILED
|
|
run_context.preview_id = "pv-1"
|
|
run_context.sql_lab_session_ref = "unavailable"
|
|
mock_repository.save_run_context.return_value = run_context
|
|
|
|
command = LaunchDatasetCommand(
|
|
session_id="sess-1",
|
|
user=MagicMock(id="user-1"),
|
|
)
|
|
|
|
result = orchestrator.launch_dataset(command)
|
|
assert result.run_context.launch_status == LaunchStatus.FAILED
|
|
|
|
|
|
# ── Tests for _build_recovery_bootstrap ────────────────────
|
|
|
|
class TestBuildRecoveryBootstrap:
|
|
"""_build_recovery_bootstrap — recovery from Superset context."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_build_recovery_bootstrap(self, orchestrator, mock_repository, mock_config_manager):
|
|
"""Recovery bootstrap builds filters, variables, and mappings."""
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.dataset_id = 42
|
|
|
|
parsed_context = MagicMock()
|
|
parsed_context.dataset_payload = {"id": 42}
|
|
parsed_context.unresolved_references = []
|
|
|
|
environment = MagicMock()
|
|
|
|
with patch("src.services.dataset_review.orchestrator.SupersetContextExtractor") as mock_extractor_cls:
|
|
mock_extractor = MagicMock()
|
|
mock_extractor.recover_imported_filters.return_value = [
|
|
{"filter_name": "region", "display_name": "Region", "raw_value": "us", "normalized_value": "US", "source": "superset_url", "confidence_state": "confirmed", "requires_confirmation": False, "recovery_status": "recovered"}
|
|
]
|
|
mock_extractor.discover_template_variables.return_value = [
|
|
{"variable_name": "region", "expression_source": "table", "variable_kind": "native_filter", "is_required": True, "default_value": None}
|
|
]
|
|
mock_extractor_cls.return_value = mock_extractor
|
|
|
|
imported_filters, template_vars, mappings, findings = (
|
|
await orchestrator._build_recovery_bootstrap(environment, session, parsed_context, [])
|
|
)
|
|
|
|
assert len(imported_filters) == 1
|
|
assert imported_filters[0].filter_name == "region"
|
|
assert len(template_vars) == 1
|
|
assert template_vars[0].variable_name == "region"
|
|
assert len(mappings) == 1
|
|
assert mappings[0].mapping_method.value == "direct_match"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recovery_bootstrap_handles_discovery_error(self, orchestrator, mock_repository):
|
|
"""Template variable discovery failure adds warning finding."""
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.dataset_id = 42
|
|
|
|
parsed_context = MagicMock()
|
|
parsed_context.dataset_payload = None # Will trigger fetch which fails
|
|
parsed_context.unresolved_references = []
|
|
|
|
environment = MagicMock()
|
|
|
|
with patch("src.services.dataset_review.orchestrator.SupersetContextExtractor") as mock_extractor_cls:
|
|
mock_extractor = MagicMock()
|
|
mock_extractor.recover_imported_filters.return_value = []
|
|
# Make the client call fail
|
|
mock_client = MagicMock()
|
|
mock_client.get_dataset_detail.side_effect = Exception("API error")
|
|
mock_extractor.client = mock_client
|
|
mock_extractor_cls.return_value = mock_extractor
|
|
|
|
imported_filters, template_vars, mappings, findings = (
|
|
await orchestrator._build_recovery_bootstrap(environment, session, parsed_context, [])
|
|
)
|
|
|
|
assert len(findings) == 1
|
|
assert findings[0].code == "TEMPLATE_VARIABLE_DISCOVERY_FAILED"
|
|
|
|
|
|
# ── Tests for _enqueue_recovery_task ───────────────────────
|
|
|
|
class TestEnqueueRecoveryTask:
|
|
"""_enqueue_recovery_task — task linking."""
|
|
|
|
def test_task_enqueued(self, orchestrator):
|
|
"""Task is created and ID returned when task_manager is present."""
|
|
mock_task_manager = MagicMock()
|
|
task = MagicMock()
|
|
task.id = "task-1"
|
|
mock_task_manager.create_task.return_value = task
|
|
orchestrator.task_manager = mock_task_manager
|
|
|
|
command = MagicMock()
|
|
command.user.id = "user-1"
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.environment_id = "env-1"
|
|
session.source_kind = "dataset_selection"
|
|
session.source_input = "42"
|
|
session.dataset_ref = "dataset:42"
|
|
session.dataset_id = 42
|
|
session.dashboard_id = None
|
|
|
|
task_id = orchestrator._enqueue_recovery_task(command, session, None)
|
|
assert task_id == "task-1"
|
|
|
|
def test_no_task_manager_returns_none(self, orchestrator):
|
|
"""No task_manager returns None."""
|
|
orchestrator.task_manager = None
|
|
command = MagicMock()
|
|
session = MagicMock()
|
|
result = orchestrator._enqueue_recovery_task(command, session, None)
|
|
assert result is None
|
|
|
|
def test_task_manager_missing_create_task(self, orchestrator):
|
|
"""Task manager without create_task returns None."""
|
|
mock_task_manager = MagicMock(spec=[])
|
|
orchestrator.task_manager = mock_task_manager
|
|
|
|
command = MagicMock()
|
|
session = MagicMock()
|
|
result = orchestrator._enqueue_recovery_task(command, session, None)
|
|
assert result is None
|
|
|
|
def test_create_task_type_error_returns_none(self, orchestrator):
|
|
"""TypeError from create_task returns None."""
|
|
mock_task_manager = MagicMock()
|
|
mock_task_manager.create_task.side_effect = TypeError()
|
|
orchestrator.task_manager = mock_task_manager
|
|
|
|
command = MagicMock()
|
|
command.user.id = "user-1"
|
|
session = MagicMock()
|
|
session.session_id = "sess-1"
|
|
session.environment_id = "env-1"
|
|
session.source_kind = "dataset_selection"
|
|
session.source_input = "42"
|
|
session.dataset_ref = "dataset:42"
|
|
session.dataset_id = 42
|
|
session.dashboard_id = None
|
|
|
|
result = orchestrator._enqueue_recovery_task(command, session, None)
|
|
assert result is None
|
|
|
|
|
|
# #endregion Test.DatasetReview.Orchestrator
|