from __future__ import annotations import argparse import os import sys from datetime import datetime, timedelta, timezone from pathlib import Path from sqlalchemy import create_engine, inspect, select from sqlalchemy.orm import Session ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from scripts.event_bus_smoke import run_smoke_check from services.shared.db import DATABASE_URL, _normalize_database_url from services.shared.sql_models import EventOutboxRow def _engine_for(database_url: str | None): return create_engine(_normalize_database_url(database_url or DATABASE_URL), future=True) def _parse_iso(value: str | None) -> datetime | None: if not value: return None try: return datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError: return None def evaluate_track8( *, database_url: str | None = None, require_bus_enabled: bool = False, base_url: str | None = None, failed_age_seconds: int = 300, ) -> list[str]: issues: list[str] = [] engine = _engine_for(database_url) inspector = inspect(engine) tables = set(inspector.get_table_names()) required_tables = {"event_outbox", "event_inbox", "reporting_event_log"} missing = sorted(required_tables - tables) if missing: issues.append(f"Missing required tables: {', '.join(missing)}") if require_bus_enabled and os.getenv("EVENT_BUS_ENABLED", "0").strip() not in {"1", "true", "yes", "on"}: issues.append("EVENT_BUS_ENABLED is not enabled in the current environment") with Session(engine) as session: failed_rows = session.execute( select(EventOutboxRow).where(EventOutboxRow.status == "failed") ).scalars().all() stale_cutoff = datetime.now(timezone.utc) - timedelta(seconds=failed_age_seconds) stale_failed = [ row for row in failed_rows if (_parse_iso(row.updated_at) or _parse_iso(row.created_at) or stale_cutoff) < stale_cutoff ] if stale_failed: issues.append(f"Found {len(stale_failed)} failed outbox events older than {failed_age_seconds}s") pending_rows = session.execute( select(EventOutboxRow).where(EventOutboxRow.status == "pending") ).scalars().all() if len(pending_rows) > 500: issues.append(f"Outbox backlog too high: {len(pending_rows)} pending events") if not (ROOT / "contracts" / "events" / "ivr.completed.json").exists(): issues.append("Missing contracts/events/ivr.completed.json") if base_url: smoke = run_smoke_check(base_url=base_url, database_url=database_url) if not smoke["passed"]: issues.append("Event bus smoke check failed") return issues def main() -> None: parser = argparse.ArgumentParser(description="Validate Track 8 event-bus readiness.") parser.add_argument("--database-url", default=None) parser.add_argument("--require-bus-enabled", action="store_true") parser.add_argument("--base-url", default=None) parser.add_argument("--failed-age-seconds", type=int, default=300) args = parser.parse_args() issues = evaluate_track8( database_url=args.database_url, require_bus_enabled=args.require_bus_enabled, base_url=args.base_url, failed_age_seconds=args.failed_age_seconds, ) if issues: print("[FAIL] Track 8 validation failed:") for issue in issues: print(f"- {issue}") raise SystemExit(1) print("[PASS] Track 8 validation checks passed") if __name__ == "__main__": main()