mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-15 01:55:48 -07:00
Fix agent listing and add scheduler timing tests
This commit is contained in:
@@ -16,6 +16,33 @@ from typing import Iterator
|
|||||||
import pytest
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
import importlib.machinery
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_eventlet_stub() -> None:
|
||||||
|
if "eventlet" in sys.modules:
|
||||||
|
return
|
||||||
|
eventlet_module = types.ModuleType("eventlet")
|
||||||
|
eventlet_module.monkey_patch = lambda **_kwargs: None # type: ignore[attr-defined]
|
||||||
|
eventlet_module.sleep = lambda _seconds: None # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
wsgi_module = types.ModuleType("eventlet.wsgi")
|
||||||
|
wsgi_module.HttpProtocol = object() # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
eventlet_module.wsgi = wsgi_module # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
eventlet_module.__spec__ = importlib.machinery.ModuleSpec("eventlet", loader=None)
|
||||||
|
wsgi_module.__spec__ = importlib.machinery.ModuleSpec("eventlet.wsgi", loader=None)
|
||||||
|
|
||||||
|
sys.modules["eventlet"] = eventlet_module
|
||||||
|
sys.modules["eventlet.wsgi"] = wsgi_module
|
||||||
|
|
||||||
|
|
||||||
|
_ensure_eventlet_stub()
|
||||||
|
|
||||||
from Data.Engine.server import create_app
|
from Data.Engine.server import create_app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ def test_list_devices(engine_harness: EngineTestHarness) -> None:
|
|||||||
assert "summary" in device and isinstance(device["summary"], dict)
|
assert "summary" in device and isinstance(device["summary"], dict)
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_agents(engine_harness: EngineTestHarness) -> None:
|
||||||
|
client = engine_harness.app.test_client()
|
||||||
|
response = client.get("/api/agents")
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.get_json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert payload, "expected at least one agent in the response"
|
||||||
|
first_agent = next(iter(payload.values()))
|
||||||
|
assert first_agent["hostname"] == "test-device"
|
||||||
|
assert first_agent["agent_id"] == "test-device-agent"
|
||||||
|
|
||||||
|
|
||||||
def test_device_details(engine_harness: EngineTestHarness) -> None:
|
def test_device_details(engine_harness: EngineTestHarness) -> None:
|
||||||
client = engine_harness.app.test_client()
|
client = engine_harness.app.test_client()
|
||||||
response = client.get("/api/device/details/test-device")
|
response = client.get("/api/device/details/test-device")
|
||||||
|
|||||||
83
Data/Engine/Unit_Tests/test_scheduler_timing.py
Normal file
83
Data/Engine/Unit_Tests/test_scheduler_timing.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# ======================================================
|
||||||
|
# Data\Engine\Unit_Tests\test_scheduler_timing.py
|
||||||
|
# Description: Validates the Engine job scheduler's interval calculations to
|
||||||
|
# ensure jobs are queued on the expected cadence.
|
||||||
|
#
|
||||||
|
# API Endpoints (if applicable): None
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, List, Tuple
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
from Data.Engine.services.API.scheduled_jobs import job_scheduler
|
||||||
|
|
||||||
|
|
||||||
|
class _DummySocketIO:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.started_tasks: List[Tuple[Callable, tuple, dict]] = []
|
||||||
|
|
||||||
|
def start_background_task(self, target: Callable, *args, **kwargs):
|
||||||
|
# The scheduler calls into Socket.IO to spawn the background loop.
|
||||||
|
# Tests only verify the scheduling math, so we capture the request
|
||||||
|
# without launching a greenlet/thread.
|
||||||
|
self.started_tasks.append((target, args, kwargs))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def emit(self, *_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _make_scheduler(tmp_path: Path) -> job_scheduler.JobScheduler:
|
||||||
|
app = Flask(__name__)
|
||||||
|
db_path = tmp_path / "scheduler.sqlite3"
|
||||||
|
return job_scheduler.JobScheduler(app, _DummySocketIO(), str(db_path))
|
||||||
|
|
||||||
|
|
||||||
|
def _ts(year: int, month: int, day: int, hour: int = 0, minute: int = 0) -> int:
|
||||||
|
return int(dt.datetime(year, month, day, hour, minute, tzinfo=dt.timezone.utc).timestamp())
|
||||||
|
|
||||||
|
|
||||||
|
def test_immediate_schedule_runs_once(tmp_path):
|
||||||
|
scheduler = _make_scheduler(tmp_path)
|
||||||
|
now = _ts(2024, 3, 1, 12, 0)
|
||||||
|
assert scheduler._compute_next_run("immediately", None, None, now) == now
|
||||||
|
assert scheduler._compute_next_run("immediately", None, now, now) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_hourly_schedule_advances_increments(tmp_path):
|
||||||
|
scheduler = _make_scheduler(tmp_path)
|
||||||
|
start = _ts(2024, 3, 1, 9, 0)
|
||||||
|
now = _ts(2024, 3, 1, 9, 30)
|
||||||
|
assert scheduler._compute_next_run("every_hour", start, None, now) == start
|
||||||
|
next_candidate = scheduler._compute_next_run("every_hour", start, start, now)
|
||||||
|
assert next_candidate == _ts(2024, 3, 1, 10, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_daily_schedule_rolls_forward(tmp_path):
|
||||||
|
scheduler = _make_scheduler(tmp_path)
|
||||||
|
start = _ts(2024, 3, 1, 6, 15)
|
||||||
|
after_two_days = _ts(2024, 3, 3, 5, 0)
|
||||||
|
next_run = scheduler._compute_next_run("daily", start, start, after_two_days)
|
||||||
|
assert next_run == _ts(2024, 3, 2, 6, 15)
|
||||||
|
|
||||||
|
|
||||||
|
def test_monthly_schedule_handles_late_month(tmp_path):
|
||||||
|
scheduler = _make_scheduler(tmp_path)
|
||||||
|
start = _ts(2024, 1, 31, 8, 0)
|
||||||
|
after_two_months = _ts(2024, 3, 5, 8, 0)
|
||||||
|
next_run = scheduler._compute_next_run("monthly", start, start, after_two_months)
|
||||||
|
# February 2024 has 29 days, so the scheduler should clamp to Feb 29th.
|
||||||
|
assert next_run == _ts(2024, 2, 29, 8, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_yearly_schedule_rolls_forward(tmp_path):
|
||||||
|
scheduler = _make_scheduler(tmp_path)
|
||||||
|
start = _ts(2023, 2, 28, 0, 0)
|
||||||
|
after_two_years = _ts(2025, 3, 1, 0, 0)
|
||||||
|
next_run = scheduler._compute_next_run("yearly", start, start, after_two_years)
|
||||||
|
assert next_run == _ts(2024, 2, 28, 0, 0)
|
||||||
@@ -615,7 +615,11 @@ class DeviceManagementService:
|
|||||||
payload["agent_id"] = agent_key
|
payload["agent_id"] = agent_key
|
||||||
agents[agent_key] = payload
|
agents[agent_key] = payload
|
||||||
|
|
||||||
return {"agents": agents}, 200
|
# The legacy server exposed /api/agents as a mapping keyed by
|
||||||
|
# agent identifier. The Engine WebUI expects the same structure,
|
||||||
|
# so we return the flattened dictionary directly instead of
|
||||||
|
# wrapping it in another object.
|
||||||
|
return agents, 200
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.logger.debug("Failed to list agents", exc_info=True)
|
self.logger.debug("Failed to list agents", exc_info=True)
|
||||||
return {"error": str(exc)}, 500
|
return {"error": str(exc)}, 500
|
||||||
|
|||||||
Reference in New Issue
Block a user