"""Unit tests for snapshot export.""" from __future__ import annotations import json import tempfile from pathlib import Path from unittest.mock import AsyncMock, MagicMock import pytest from libs.export.snapshot_export import _temporal_split, export_dataset_snapshot @pytest.mark.unit class TestTemporalSplit: def test_split_proportions(self) -> None: """Temporal split produces correct proportions for 100 rows.""" rows = [{"event_date": "2026-01-01"} for _ in range(100)] splits = _temporal_split(rows, "temporal_70_15_15") assert len(splits["train"]) == 70 assert len(splits["valid"]) == 15 assert len(splits["test"]) == 15 def test_split_preserves_temporal_order(self) -> None: """Train set contains earliest dates, test contains latest.""" rows = [{"event_date": f"2026-{m:02d}-01"} for m in range(1, 13)] splits = _temporal_split(rows, "temporal_70_15_15") if splits["train"] and splits["test"]: assert splits["train"][-1]["event_date"] <= splits["test"][0]["event_date"] def test_empty_rows_returns_empty_splits(self) -> None: splits = _temporal_split([], "temporal_70_15_15") assert splits == {"train": [], "valid": [], "test": []} @pytest.mark.unit class TestExportDatasetSnapshot: @pytest.mark.asyncio async def test_manifest_is_written(self) -> None: """export_dataset_snapshot writes a manifest.json with expected fields.""" # Mock DB session returning empty results (no feature+label pairs) mock_session = AsyncMock() mock_result = MagicMock() mock_result.all.return_value = [] # no rows mock_session.execute = AsyncMock(return_value=mock_result) with tempfile.TemporaryDirectory() as tmpdir: manifest = await export_dataset_snapshot( session=mock_session, snapshot_id="test-snapshot-001", split_policy="temporal_70_15_15", output_dir=tmpdir, ) assert manifest["snapshot_id"] == "test-snapshot-001" assert "created_at_utc" in manifest assert "row_counts" in manifest assert manifest["split_policy"] == "temporal_70_15_15" assert manifest["total_rows"] == 0 @pytest.mark.asyncio async def test_parquet_files_created(self) -> None: """Parquet files are created for each split partition.""" mock_session = AsyncMock() mock_result = MagicMock() mock_result.all.return_value = [] mock_session.execute = AsyncMock(return_value=mock_result) with tempfile.TemporaryDirectory() as tmpdir: await export_dataset_snapshot( session=mock_session, snapshot_id="test-parquet-002", split_policy="temporal_70_15_15", output_dir=tmpdir, ) snap_dir = Path(tmpdir) / "test-parquet-002" assert (snap_dir / "train.parquet").exists() assert (snap_dir / "valid.parquet").exists() assert (snap_dir / "test.parquet").exists() assert (snap_dir / "manifest.json").exists() manifest_data = json.loads((snap_dir / "manifest.json").read_text()) assert manifest_data["snapshot_id"] == "test-parquet-002"