2026-03-10 13:42:38 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import inspect
|
|
|
|
|
from types import SimpleNamespace
|
2026-03-14 01:31:39 +08:00
|
|
|
from unittest.mock import MagicMock
|
2026-03-10 13:42:38 +08:00
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
import src.services.system.maintenance_scheduler as maintenance_scheduler_module
|
|
|
|
|
from src.config.settings import config
|
|
|
|
|
from src.services.system.maintenance_scheduler import MaintenanceScheduler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_maintenance_scheduler_start_skips_startup_task_when_disabled(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
monkeypatch.setattr(config, "maintenance_startup_tasks_enabled", False)
|
|
|
|
|
|
|
|
|
|
scheduler = MaintenanceScheduler()
|
|
|
|
|
|
|
|
|
|
created = False
|
|
|
|
|
|
|
|
|
|
def fake_create_task(coro): # type: ignore[no-untyped-def]
|
|
|
|
|
nonlocal created
|
|
|
|
|
created = True
|
|
|
|
|
if inspect.iscoroutine(coro):
|
|
|
|
|
coro.close()
|
|
|
|
|
return object()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(maintenance_scheduler_module.asyncio, "create_task", fake_create_task)
|
|
|
|
|
monkeypatch.setattr(scheduler, "_get_checkin_time", lambda: (1, 5))
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
maintenance_scheduler_module,
|
|
|
|
|
"get_scheduler",
|
|
|
|
|
lambda: SimpleNamespace(
|
|
|
|
|
add_cron_job=lambda *args, **kwargs: None,
|
|
|
|
|
add_interval_job=lambda *args, **kwargs: None,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await scheduler.start()
|
|
|
|
|
|
|
|
|
|
assert created is False
|
2026-03-14 01:31:39 +08:00
|
|
|
|
|
|
|
|
|
2026-03-14 11:59:07 +08:00
|
|
|
def test_http_client_idle_cleanup_interval_env_invalid(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
monkeypatch.setenv("HTTP_CLIENT_IDLE_CLEANUP_INTERVAL_MINUTES", "bad")
|
|
|
|
|
assert MaintenanceScheduler._get_http_client_idle_cleanup_interval_minutes() == 5
|
|
|
|
|
|
|
|
|
|
|
2026-03-14 01:31:39 +08:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_candidate_cleanup_uses_dedicated_retention_and_batch_settings(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
scheduler = MaintenanceScheduler()
|
|
|
|
|
|
|
|
|
|
class _FakeLoop:
|
|
|
|
|
async def run_in_executor(self, _executor, func): # type: ignore[no-untyped-def]
|
|
|
|
|
return func()
|
|
|
|
|
|
|
|
|
|
class _ConfigSession:
|
|
|
|
|
def close(self) -> None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
class _BatchSession:
|
|
|
|
|
def __init__(self, ids: list[str]) -> None:
|
|
|
|
|
self.ids = ids
|
|
|
|
|
self.closed = False
|
|
|
|
|
self.committed = False
|
|
|
|
|
self.query_obj = MagicMock()
|
|
|
|
|
filtered = self.query_obj.filter.return_value
|
|
|
|
|
filtered.order_by.return_value.limit.return_value.all.return_value = [
|
|
|
|
|
SimpleNamespace(id=value) for value in ids
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def query(self, _model): # type: ignore[no-untyped-def]
|
|
|
|
|
return self.query_obj
|
|
|
|
|
|
|
|
|
|
def execute(self, _statement): # type: ignore[no-untyped-def]
|
|
|
|
|
return SimpleNamespace(rowcount=len(self.ids))
|
|
|
|
|
|
|
|
|
|
def commit(self) -> None:
|
|
|
|
|
self.committed = True
|
|
|
|
|
|
|
|
|
|
def rollback(self) -> None:
|
|
|
|
|
raise AssertionError("rollback should not be called")
|
|
|
|
|
|
|
|
|
|
def close(self) -> None:
|
|
|
|
|
self.closed = True
|
|
|
|
|
|
|
|
|
|
config_session = _ConfigSession()
|
|
|
|
|
batch_one = _BatchSession(["candidate-1", "candidate-2"])
|
|
|
|
|
batch_two = _BatchSession([])
|
|
|
|
|
sessions = iter([config_session, batch_one, batch_two])
|
|
|
|
|
|
|
|
|
|
def fake_create_session(): # type: ignore[no-untyped-def]
|
|
|
|
|
return next(sessions)
|
|
|
|
|
|
|
|
|
|
config_values = {
|
|
|
|
|
"enable_auto_cleanup": True,
|
|
|
|
|
"request_candidates_retention_days": 21,
|
|
|
|
|
"request_candidates_cleanup_batch_size": 2,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(maintenance_scheduler_module, "create_session", fake_create_session)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
maintenance_scheduler_module.SystemConfigService,
|
|
|
|
|
"get_config",
|
|
|
|
|
lambda _db, key, default=None: config_values.get(key, default),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
maintenance_scheduler_module.asyncio, "get_running_loop", lambda: _FakeLoop()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await scheduler._perform_candidate_cleanup()
|
|
|
|
|
|
|
|
|
|
batch_one.query_obj.filter.return_value.order_by.return_value.limit.return_value.all.assert_called_once()
|
|
|
|
|
batch_one.query_obj.filter.return_value.order_by.return_value.limit.assert_called_once_with(2)
|
|
|
|
|
assert batch_one.committed is True
|
|
|
|
|
assert batch_one.closed is True
|
|
|
|
|
assert batch_two.closed is True
|
2026-03-15 23:46:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cleanup_body_fields_loads_ids_then_processes_records_individually(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
scheduler = MaintenanceScheduler()
|
|
|
|
|
|
|
|
|
|
class _IdBatchSession:
|
|
|
|
|
def __init__(self, ids: list[str]) -> None:
|
|
|
|
|
self.ids = ids
|
|
|
|
|
self.closed = False
|
|
|
|
|
self.query_obj = MagicMock()
|
|
|
|
|
filtered = self.query_obj.filter.return_value
|
|
|
|
|
filtered.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [
|
|
|
|
|
SimpleNamespace(id=value) for value in ids
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def query(self, *args): # type: ignore[no-untyped-def]
|
|
|
|
|
self.query_args = args
|
|
|
|
|
return self.query_obj
|
|
|
|
|
|
|
|
|
|
def rollback(self) -> None:
|
|
|
|
|
raise AssertionError("rollback should not be called")
|
|
|
|
|
|
|
|
|
|
def close(self) -> None:
|
|
|
|
|
self.closed = True
|
|
|
|
|
|
|
|
|
|
class _RecordSession:
|
|
|
|
|
def __init__(self, record: SimpleNamespace) -> None:
|
|
|
|
|
self.record = record
|
|
|
|
|
self.closed = False
|
|
|
|
|
self.committed = False
|
|
|
|
|
self.query_obj = MagicMock()
|
|
|
|
|
self.query_obj.filter.return_value.first.return_value = record
|
|
|
|
|
|
|
|
|
|
def query(self, *args): # type: ignore[no-untyped-def]
|
|
|
|
|
self.query_args = args
|
|
|
|
|
return self.query_obj
|
|
|
|
|
|
|
|
|
|
def execute(self, _statement): # type: ignore[no-untyped-def]
|
|
|
|
|
return SimpleNamespace(rowcount=1)
|
|
|
|
|
|
|
|
|
|
def commit(self) -> None:
|
|
|
|
|
self.committed = True
|
|
|
|
|
|
|
|
|
|
def rollback(self) -> None:
|
|
|
|
|
raise AssertionError("rollback should not be called")
|
|
|
|
|
|
|
|
|
|
def close(self) -> None:
|
|
|
|
|
self.closed = True
|
|
|
|
|
|
|
|
|
|
batch_one = _IdBatchSession(["usage-1", "usage-2"])
|
|
|
|
|
record_one = _RecordSession(
|
|
|
|
|
SimpleNamespace(
|
|
|
|
|
id="usage-1",
|
|
|
|
|
request_body={"hello": "world"},
|
|
|
|
|
response_body=None,
|
|
|
|
|
provider_request_body=None,
|
|
|
|
|
client_response_body=None,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
record_two = _RecordSession(
|
|
|
|
|
SimpleNamespace(
|
|
|
|
|
id="usage-2",
|
|
|
|
|
request_body=None,
|
|
|
|
|
response_body={"ok": True},
|
|
|
|
|
provider_request_body=None,
|
|
|
|
|
client_response_body=None,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
batch_two = _IdBatchSession([])
|
|
|
|
|
sessions = iter([batch_one, record_one, record_two, batch_two])
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
maintenance_scheduler_module,
|
|
|
|
|
"create_session",
|
|
|
|
|
lambda: next(sessions),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
maintenance_scheduler_module,
|
|
|
|
|
"compress_json",
|
|
|
|
|
lambda payload: f"compressed:{payload}".encode(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
compressed = scheduler._cleanup_body_fields(
|
|
|
|
|
cutoff_time=SimpleNamespace(), # type: ignore[arg-type]
|
|
|
|
|
batch_size=1000,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert compressed == 2
|
|
|
|
|
assert batch_one.query_args == (maintenance_scheduler_module.Usage.id,)
|
|
|
|
|
assert len(record_one.query_args) == 5
|
|
|
|
|
assert len(record_two.query_args) == 5
|
|
|
|
|
assert batch_one.closed is True
|
|
|
|
|
assert batch_two.closed is True
|
|
|
|
|
assert record_one.committed is True
|
|
|
|
|
assert record_two.committed is True
|
|
|
|
|
assert record_one.closed is True
|
|
|
|
|
assert record_two.closed is True
|