import contextlib import io import json import os import unittest from datetime import datetime, timezone os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test") from scripts import run_database_readiness class DatabaseReadinessTests(unittest.TestCase): def test_seed_readiness_reports_missing_seed_data(self) -> None: result = run_database_readiness.evaluate_seed_readiness( found_venues={"OKX"}, found_instruments=set(), required_venues=("OKX",), required_instruments=("BTC-USDT",), ) self.assertFalse(result["ready"]) self.assertEqual(result["missing_instruments"], ["BTC-USDT"]) self.assertIn("missing_required_instruments", result["reasons"]) def test_candle_readiness_can_require_minimum_count(self) -> None: result = run_database_readiness.evaluate_candle_readiness(candle_count=10, min_candles=20) self.assertFalse(result["ready"]) self.assertIn("not_enough_candles", result["reasons"]) def test_candle_readiness_can_require_span_and_freshness(self) -> None: result = run_database_readiness.evaluate_candle_readiness( candle_count=100, first_ts_open=datetime(2026, 4, 29, 8, 0, tzinfo=timezone.utc), last_ts_open=datetime(2026, 4, 29, 8, 30, tzinfo=timezone.utc), min_span_minutes=60, max_age_minutes=10, now=datetime(2026, 4, 29, 9, 0, tzinfo=timezone.utc), ) self.assertFalse(result["ready"]) self.assertIn("not_enough_candle_span", result["reasons"]) self.assertIn("candles_too_stale", result["reasons"]) self.assertEqual(result["span_minutes"], 30.0) self.assertEqual(result["age_minutes"], 30.0) self.assertEqual(result["first_ts_open"], "2026-04-29T08:00:00+00:00") def test_run_database_readiness_passes_when_all_gates_are_ready(self) -> None: captured = {} def fake_candle_probe(**kwargs): captured.update(kwargs) return {"ready": True, "candle_count": 3, "reasons": []} result = run_database_readiness.run_database_readiness( env={"READINESS_MIN_CANDLES": "2"}, db_probe=lambda: {"available": True, "value": 1}, schema_probe=lambda: {"ready": True, "missing_tables": [], "missing_columns": {}}, seed_probe=lambda **kwargs: {"ready": True, "reasons": []}, candle_probe=fake_candle_probe, ) self.assertTrue(result["passed"]) self.assertEqual(result["reasons"], []) self.assertEqual(result["action_plan"][0]["action"], "run_guarded_pipeline") self.assertEqual(captured["min_candles"], 2) self.assertEqual(captured["min_span_minutes"], run_database_readiness.DEFAULT_MIN_CANDLE_SPAN_MINUTES) self.assertEqual(captured["max_age_minutes"], run_database_readiness.DEFAULT_MAX_CANDLE_AGE_MINUTES) def test_run_database_readiness_collects_reasons(self) -> None: result = run_database_readiness.run_database_readiness( env={}, db_probe=lambda: {"available": True, "value": 1}, schema_probe=lambda: {"ready": False, "missing_tables": ["candles"], "missing_columns": {}}, seed_probe=lambda **kwargs: {"ready": False, "reasons": ["missing_required_instruments"]}, candle_probe=lambda **kwargs: {"ready": False, "reasons": ["not_enough_candles"]}, ) self.assertFalse(result["passed"]) self.assertEqual(result["reasons"], ["schema_not_ready", "missing_required_instruments", "not_enough_candles"]) self.assertEqual( result["next_actions"], [ "run_migrations_before_pipeline", "verify_seed_migrations_or_seed_data", "run_okx_backfill_or_load_replay_sample", ], ) self.assertEqual(result["action_plan"][0]["command"][-1], "scripts/run_migrations.py") self.assertTrue(result["action_plan"][0]["requires_database"]) def test_next_actions_default_to_guarded_pipeline_when_ready(self) -> None: self.assertEqual(run_database_readiness.build_next_actions([]), ["run_guarded_pipeline"]) def test_next_actions_backfill_for_span_or_staleness(self) -> None: self.assertEqual( run_database_readiness.build_next_actions(["not_enough_candle_span", "candles_too_stale"]), ["run_okx_backfill_or_load_replay_sample"], ) def test_main_can_emit_json(self) -> None: output = io.StringIO() original = run_database_readiness.run_database_readiness try: run_database_readiness.run_database_readiness = lambda env=None: { "passed": True, "database": {"available": True}, "schema": {"ready": True}, "seed_data": {"ready": True}, "candles": {"candle_count": 1}, "reasons": [], "next_actions": ["run_guarded_pipeline"], "action_plan": run_database_readiness.build_action_plan(["run_guarded_pipeline"]), } with contextlib.redirect_stdout(output): exit_code = run_database_readiness.main(env={"READINESS_OUTPUT_JSON": "1"}) finally: run_database_readiness.run_database_readiness = original summary = json.loads(output.getvalue()) self.assertEqual(exit_code, 0) self.assertTrue(summary["passed"]) self.assertEqual(summary["action_plan"][0]["action"], "run_guarded_pipeline") if __name__ == "__main__": unittest.main()