130 lines
5.6 KiB
Python
130 lines
5.6 KiB
Python
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()
|