"""Admin → Jobs listing: categories, ordering, and next-run coherence. The panel used to render 19 jobs as one alphabetical list in which a pipeline step, a cron job and a manual job were indistinguishable, and a triggered job could advertise a next run ten years out. """ from datetime import datetime, timedelta, timezone import pytest from app import job_catalog from app.scheduler import configure_scheduler, scheduler from app.services.admin_service import _visible_next_run, list_jobs @pytest.fixture(autouse=True) def _configured_scheduler(): scheduler.remove_all_jobs() configure_scheduler() yield scheduler.remove_all_jobs() def _by_name(jobs: list[dict]) -> dict[str, dict]: return {job["name"]: job for job in jobs} class TestVisibleNextRun: def test_parked_backstop_is_not_a_schedule(self): """Paused jobs carry a 520-week interval; triggering one re-arms it.""" backstop = datetime.now(timezone.utc) + timedelta(weeks=520) assert _visible_next_run(backstop) is None def test_a_real_upcoming_run_passes_through(self): soon = datetime.now(timezone.utc) + timedelta(hours=6) assert _visible_next_run(soon) == soon def test_none_stays_none(self): assert _visible_next_run(None) is None class TestListJobs: async def test_hidden_jobs_are_not_listed_but_stay_valid(self, db_session): jobs = _by_name(await list_jobs(db_session)) assert "data_backfill" not in jobs # Still triggerable through the API, and still registered. assert "data_backfill" in job_catalog.VALID_JOB_NAMES assert scheduler.get_job("data_backfill") is not None async def test_every_visible_job_has_a_category(self, db_session): jobs = await list_jobs(db_session) assert {j["name"] for j in jobs} == set( job_catalog.VALID_JOB_NAMES - job_catalog.HIDDEN_JOBS ) assert all(j["category"] in job_catalog.CATEGORY_ORDER for j in jobs) async def test_jobs_arrive_grouped_by_category(self, db_session): """The frontend renders sections in payload order, so ordering is the API's job — not something each client re-derives.""" categories = [j["category"] for j in await list_jobs(db_session)] ranks = [job_catalog.CATEGORY_ORDER.index(c) for c in categories] assert ranks == sorted(ranks) async def test_pipeline_steps_defer_their_schedule_to_the_parent(self, db_session): jobs = _by_name(await list_jobs(db_session)) step = jobs["rr_scanner"] assert step["category"] == job_catalog.CATEGORY_STEP assert step["next_run_at"] is None assert step["next_run_source"] == "via_pipeline" assert step["pipelines"] == ["near_close_pipeline"] async def test_step_reports_the_soonest_enabled_parent(self, db_session): due = datetime.now(timezone.utc) + timedelta(hours=3) scheduler.modify_job("daily_pipeline", next_run_time=due) collector = _by_name(await list_jobs(db_session))["data_collector"] assert collector["via_next_run_job"] == "daily_pipeline" assert collector["via_next_run_at"] == due.isoformat() # Runs in all four pipelines — the reason steps are not nested under one. assert set(collector["pipelines"]) == set(job_catalog.PIPELINE_JOBS) async def test_manual_jobs_say_so_instead_of_showing_a_date(self, db_session): study = _by_name(await list_jobs(db_session))["event_study"] assert study["category"] == job_catalog.CATEGORY_MANUAL assert study["next_run_source"] == "manual_only" assert study["next_run_at"] is None async def test_a_triggered_manual_job_still_shows_no_next_run(self, db_session): """Regression: triggering re-armed the 520-week backstop, which the panel rendered as a real 'next run in ~87600h'.""" scheduler.modify_job("event_study", next_run_time=datetime.now(timezone.utc)) scheduler.modify_job("event_study", next_run_time=None) study = _by_name(await list_jobs(db_session))["event_study"] assert study["next_run_at"] is None async def test_pipelines_report_their_own_schedule_and_steps(self, db_session): pipeline = _by_name(await list_jobs(db_session))["daily_pipeline"] assert pipeline["category"] == job_catalog.CATEGORY_PIPELINE assert pipeline["next_run_source"] == "own_schedule" assert pipeline["steps"] == [ step for step, _ in job_catalog.PIPELINE_STEPS["daily_pipeline"] ] async def test_standalone_jobs_keep_their_own_schedule(self, db_session): backtest = _by_name(await list_jobs(db_session))["backtest"] assert backtest["category"] == job_catalog.CATEGORY_SCHEDULED assert backtest["next_run_source"] == "own_schedule" assert backtest["pipelines"] == []