"""Legacy migration tests. Tests the idempotent, interruption-safe import of history.jsonl into the observation store. """ import json import os import sqlite3 import tempfile from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict import pytest # Add src to path for imports import sys sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from fenris.legacy import import_legacy_history, is_legacy_imported, _parse_history_line from fenris.store import init_store # Fixtures @pytest.fixture def history_fixture() -> str: """Minimal history.jsonl content with two samples.""" samples = [ { "timestamp": "2026-08-30T10:00:00Z", "device": "/dev/nvme0", "model": "Samsung SSD 970 EVO Plus 1TB", "serial": "S4EWNX0N123456", "firmware_version": "2B2QEXM7", "capacity_bytes": 1024000000000, "data_units_written": 1000000, "data_units_read": 500000, "percentage_used": 5, "power_on_hours": 8765, "temperature": 35, "available_spare": 100, "media_errors": 0, "power_cycles": 1234, "unsafe_shutdowns": 5, "critical_warning": 0, }, { "timestamp": "2026-08-30T11:00:00Z", "device": "/dev/nvme0", "model": "Samsung SSD 970 EVO Plus 1TB", "serial": "S4EWNX0N123456", "firmware_version": "2B2QEXM7", "capacity_bytes": 1024000000000, "data_units_written": 1001000, "data_units_read": 501000, "percentage_used": 5, "power_on_hours": 8766, "temperature": 36, "available_spare": 100, "media_errors": 0, "power_cycles": 1234, "unsafe_shutdowns": 5, "critical_warning": 0, }, ] return "\n".join(json.dumps(s) for s in samples) @pytest.fixture def hourly_fixture() -> str: """Minimal hourly.jsonl content for diffing.""" hours = [ { "hour": "2026-08-30T10:00:00Z", "bytes_written_delta": 512000000, "bytes_read_delta": 256000000, "sample_count": 1, }, { "hour": "2026-08-30T11:00:00Z", "bytes_written_delta": 513000000, "bytes_read_delta": 257000000, "sample_count": 1, }, ] return "\n".join(json.dumps(h) for h in hours) @pytest.fixture def config_fixture(tmp_path: Path) -> Dict[str, Any]: """Configuration fixture.""" return { "device": "/dev/nvme0", "store_path": str(tmp_path / "observations.db"), "data_dir": str(tmp_path), } @pytest.fixture def clock_fixture(): """Injected clock returning fixed time.""" class FakeClock: def __init__(self): self.now = datetime(2026, 9, 1, 12, 0, 0, tzinfo=timezone.utc) def utcnow(self): return self.now return FakeClock() # Test: Idempotency - second run no-ops def test_import_idempotent( history_fixture: str, config_fixture: Dict[str, Any], clock_fixture, ): """Given a store with legacy import marker, when import is called again, then it no-ops.""" # Create history file history_path = Path(config_fixture["data_dir"]) / "history.jsonl" history_path.write_text(history_fixture) # Initialize store store_path = Path(config_fixture["store_path"]) conn = init_store(store_path) # First import result1 = import_legacy_history(conn, history_path, clock=clock_fixture) assert result1["ok"] is True assert result1["skipped"] is False # Second import (should no-op) history_path2 = Path(config_fixture["data_dir"]) / "history.jsonl" history_path2.write_text(history_fixture) result2 = import_legacy_history(conn, history_path2, clock=clock_fixture) assert result2["ok"] is True assert result2["skipped"] is True conn.close() # Test: Interruption safety - single transaction def test_import_single_transaction( history_fixture: str, config_fixture: Dict[str, Any], clock_fixture, ): """Given history.jsonl, when import runs, then the entire import is a single transaction.""" # Create history file history_path = Path(config_fixture["data_dir"]) / "history.jsonl" history_path.write_text(history_fixture) # Initialize store store_path = Path(config_fixture["store_path"]) conn = init_store(store_path) # Import result = import_legacy_history(conn, history_path, clock=clock_fixture) assert result["ok"] is True # Verify all data was imported atomically cursor = conn.execute("SELECT COUNT(*) FROM samples") assert cursor.fetchone()[0] == 2 cursor = conn.execute("SELECT COUNT(*) FROM hour_observations") assert cursor.fetchone()[0] == 2 cursor = conn.execute("SELECT COUNT(*) FROM monitoring_periods") assert cursor.fetchone()[0] == 1 conn.close() # Test: Legacy files renamed to *.migrated after commit def test_legacy_files_renamed( history_fixture: str, config_fixture: Dict[str, Any], clock_fixture, ): """Given history.jsonl and hourly.jsonl, when import commits, then files are renamed to *.migrated.""" # Create files history_path = Path(config_fixture["data_dir"]) / "history.jsonl" history_path.write_text(history_fixture) hourly_path = Path(config_fixture["data_dir"]) / "hourly.jsonl" hourly_path.write_text("{}") # Initialize store store_path = Path(config_fixture["store_path"]) conn = init_store(store_path) # Import result = import_legacy_history(conn, history_path, hourly_path=hourly_path, clock=clock_fixture) assert result["ok"] is True # Verify files renamed assert not history_path.exists() assert history_path.with_suffix(history_path.suffix + ".migrated").exists() assert not hourly_path.exists() assert hourly_path.with_suffix(hourly_path.suffix + ".migrated").exists() conn.close() # Test: Malformed lines quarantined with logged count def test_malformed_lines_quarantined( config_fixture: Dict[str, Any], clock_fixture, ): """Given history.jsonl with malformed lines, when import runs, then malformed lines are quarantined with logged count.""" # Create history with malformed lines history_content = "\n".join([ '{"timestamp": "2026-08-30T10:00:00Z", "model": "Test", "serial": "123", "firmware_version": "1.0", "data_units_written": 1000, "data_units_read": 500, "percentage_used": 5, "power_on_hours": 100, "temperature": 35}', 'NOT JSON', '{"timestamp": "2026-08-30T11:00:00Z", "model": "Test", "serial": "123", "firmware_version": "1.0", "data_units_written": 1001, "data_units_read": 501, "percentage_used": 5, "power_on_hours": 101, "temperature": 36}', ]) history_path = Path(config_fixture["data_dir"]) / "history.jsonl" history_path.write_text(history_content) # Initialize store store_path = Path(config_fixture["store_path"]) conn = init_store(store_path) # Import result = import_legacy_history(conn, history_path, clock=clock_fixture) assert result["ok"] is True assert result["malformed_lines"] == 1 assert result["samples_imported"] == 2 conn.close() # Test: hourly.jsonl diffed and logged but never trusted def test_hourly_jsonl_diffed( history_fixture: str, hourly_fixture: str, config_fixture: Dict[str, Any], clock_fixture, ): """Given history.jsonl and hourly.jsonl with mismatches, when import runs, then mismatches are diffed and logged.""" # Create files history_path = Path(config_fixture["data_dir"]) / "history.jsonl" history_path.write_text(history_fixture) hourly_path = Path(config_fixture["data_dir"]) / "hourly.jsonl" hourly_path.write_text(hourly_fixture) # Initialize store store_path = Path(config_fixture["store_path"]) conn = init_store(store_path) # Import (should not fail even with mismatches) result = import_legacy_history(conn, history_path, hourly_path=hourly_path, clock=clock_fixture) assert result["ok"] is True # Verify data was imported from history.jsonl, not hourly.jsonl cursor = conn.execute("SELECT bytes_written_delta FROM hour_observations ORDER BY hour") deltas = [row[0] for row in cursor.fetchall()] # Should match history.jsonl derived values, not hourly.jsonl assert len(deltas) == 2 conn.close() # Test: Legacy identity is mn-only def test_legacy_identity_mn_only( history_fixture: str, config_fixture: Dict[str, Any], clock_fixture, ): """Given history.jsonl, when import runs, then legacy segment has mn-only identity.""" # Create history file history_path = Path(config_fixture["data_dir"]) / "history.jsonl" history_path.write_text(history_fixture) # Initialize store store_path = Path(config_fixture["store_path"]) conn = init_store(store_path) # Import result = import_legacy_history(conn, history_path, clock=clock_fixture) assert result["ok"] is True assert result["legacy_identity_key"] == "legacy|Samsung SSD 970 EVO Plus 1TB" # Verify segment has mn-only identity cursor = conn.execute("SELECT identity_key, mn, sn, subnqn FROM controller_segments") row = cursor.fetchone() assert row[0] == "legacy|Samsung SSD 970 EVO Plus 1TB" assert row[1] == "Samsung SSD 970 EVO Plus 1TB" assert row[2] is None # sn is None for legacy assert row[3] is None # subnqn is None for legacy conn.close() # Test: No synthetic baseline created def test_no_synthetic_baseline( history_fixture: str, config_fixture: Dict[str, Any], clock_fixture, ): """Given history.jsonl, when import runs, then no endurance baseline is created.""" # Create history file history_path = Path(config_fixture["data_dir"]) / "history.jsonl" history_path.write_text(history_fixture) # Initialize store store_path = Path(config_fixture["store_path"]) conn = init_store(store_path) # Import result = import_legacy_history(conn, history_path, clock=clock_fixture) assert result["ok"] is True # Verify no baseline created cursor = conn.execute("SELECT COUNT(*) FROM endurance_baseline") assert cursor.fetchone()[0] == 0 conn.close() # Test: Monitoring period opened and closed def test_monitoring_period_opened_closed( history_fixture: str, config_fixture: Dict[str, Any], clock_fixture, ): """Given history.jsonl, when import runs, then one monitoring period is opened at first sample and closed at migration.""" # Create history file history_path = Path(config_fixture["data_dir"]) / "history.jsonl" history_path.write_text(history_fixture) # Initialize store store_path = Path(config_fixture["store_path"]) conn = init_store(store_path) # Import result = import_legacy_history(conn, history_path, clock=clock_fixture) assert result["ok"] is True # Verify monitoring period cursor = conn.execute("SELECT started_at, ended_at, end_cause FROM monitoring_periods") row = cursor.fetchone() assert row[0] == "2026-08-30T10:00:00+00:00" # First sample time assert row[1] == clock_fixture.now.isoformat() # Migration time assert row[2] == "migrated" conn.close() # Test: Parse history line def test_parse_history_line_valid(): """Given a valid history line, when parsed, then returns the record.""" line = '{"timestamp": "2026-08-30T10:00:00Z", "model": "Test", "serial": "123", "firmware_version": "1.0", "data_units_written": 1000, "data_units_read": 500, "percentage_used": 5, "power_on_hours": 100, "temperature": 35}' result = _parse_history_line(line, 1) assert result is not None assert result["model"] == "Test" def test_parse_history_line_malformed(): """Given a malformed JSON line, when parsed, then returns None.""" result = _parse_history_line("NOT JSON", 1) assert result is None def test_parse_history_line_missing_field(): """Given a line with missing required field, when parsed, then returns None.""" line = '{"timestamp": "2026-08-30T10:00:00Z", "model": "Test"}' result = _parse_history_line(line, 1) assert result is None