- 10 translate plugin test files (100% coverage on 12 modules) - assistant/handler tools: 85+ tests covering dispatch, registry, resolvers, routes, llm_planner, 13 tool handlers - clean release: artifact_catalog_loader, mappers, approval, publication tests - API routes: translate_helpers, validation_service extensions, datasets to 100% - notifications: providers/service tests - services: profile_preference_service - docs/orthogonal-test-report.md — full speckit.tests audit - Fixes: 3 git_base async mock failures, 4 assistant handler permission-check patches - .gitignore: coverage artifacts
137 lines
5.0 KiB
Python
137 lines
5.0 KiB
Python
# #region Test.LLMAnalysisScheduler [C:3] [TYPE Module] [SEMANTICS test, llm, analysis, scheduler, cron]
|
|
# @BRIEF Tests for llm_analysis/scheduler.py — _parse_cron, schedule_dashboard_validation.
|
|
# @RELATION BINDS_TO -> [LLMAnalysisScheduler]
|
|
# @TEST_CONTRACT: _parse_cron -> dict | cron expression parsing
|
|
# @TEST_CONTRACT: schedule_dashboard_validation -> None | schedules a validation job
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
|
|
|
import os
|
|
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
|
|
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
|
|
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.plugins.llm_analysis.scheduler import _parse_cron, schedule_dashboard_validation
|
|
|
|
|
|
class TestParseCron:
|
|
"""_parse_cron — basic cron expression parser."""
|
|
|
|
def test_standard_5_part_cron(self):
|
|
"""Standard 5-part cron expression parsed correctly."""
|
|
result = _parse_cron("0 8 * * 1-5")
|
|
assert result == {
|
|
"minute": "0",
|
|
"hour": "8",
|
|
"day": "*",
|
|
"month": "*",
|
|
"day_of_week": "1-5",
|
|
}
|
|
|
|
def test_daily_at_midnight(self):
|
|
"""Daily at midnight."""
|
|
result = _parse_cron("0 0 * * *")
|
|
assert result["hour"] == "0"
|
|
assert result["minute"] == "0"
|
|
|
|
def test_every_hour(self):
|
|
"""Every hour at minute 0."""
|
|
result = _parse_cron("0 * * * *")
|
|
assert result["minute"] == "0"
|
|
assert result["hour"] == "*"
|
|
|
|
def test_invalid_parts_count(self):
|
|
"""Less or more than 5 parts returns empty dict."""
|
|
assert _parse_cron("0 8 * *") == {}
|
|
assert _parse_cron("0 8 * * 1-5 extra") == {}
|
|
|
|
def test_empty_string(self):
|
|
"""Empty string returns empty dict."""
|
|
assert _parse_cron("") == {}
|
|
|
|
def test_step_values(self):
|
|
"""Step values like */15 parsed correctly."""
|
|
result = _parse_cron("*/15 * * * *")
|
|
assert result["minute"] == "*/15"
|
|
|
|
|
|
class TestScheduleDashboardValidation:
|
|
"""schedule_dashboard_validation — schedules recurring validation."""
|
|
|
|
def test_schedule_with_params(self):
|
|
"""Scheduler and task manager called with correct args."""
|
|
mock_scheduler = MagicMock()
|
|
mock_task_manager = MagicMock()
|
|
|
|
with patch("src.plugins.llm_analysis.scheduler.get_scheduler_service", return_value=mock_scheduler), \
|
|
patch("src.plugins.llm_analysis.scheduler.get_task_manager", return_value=mock_task_manager):
|
|
|
|
schedule_dashboard_validation(
|
|
dashboard_id="dash-1",
|
|
cron_expression="0 6 * * *",
|
|
params={"threshold": 0.8},
|
|
)
|
|
|
|
# Scheduler should have added a cron job
|
|
mock_scheduler.add_job.assert_called_once()
|
|
call_args = mock_scheduler.add_job.call_args
|
|
assert call_args[0][1] == "cron" # trigger type
|
|
assert call_args[1]["id"] == "llm_val_dash-1"
|
|
assert call_args[1]["replace_existing"] is True
|
|
assert call_args[1]["minute"] == "0"
|
|
assert call_args[1]["hour"] == "6"
|
|
|
|
def test_schedule_creates_task_on_execution(self):
|
|
"""The scheduled job function creates a task when executed."""
|
|
mock_scheduler = MagicMock()
|
|
mock_task_manager = AsyncMock()
|
|
|
|
with patch("src.plugins.llm_analysis.scheduler.get_scheduler_service", return_value=mock_scheduler), \
|
|
patch("src.plugins.llm_analysis.scheduler.get_task_manager", return_value=mock_task_manager):
|
|
|
|
schedule_dashboard_validation(
|
|
dashboard_id="dash-1",
|
|
cron_expression="0 6 * * *",
|
|
params={"threshold": 0.8},
|
|
)
|
|
|
|
# Get the job function and call it
|
|
job_func = mock_scheduler.add_job.call_args[0][0]
|
|
import asyncio
|
|
asyncio.run(job_func())
|
|
|
|
mock_task_manager.create_task.assert_called_once_with(
|
|
plugin_id="llm_dashboard_validation",
|
|
params={"dashboard_id": "dash-1", "threshold": 0.8},
|
|
)
|
|
|
|
def test_schedule_without_params(self):
|
|
"""Schedule without extra params still works."""
|
|
mock_scheduler = MagicMock()
|
|
mock_task_manager = AsyncMock()
|
|
|
|
with patch("src.plugins.llm_analysis.scheduler.get_scheduler_service", return_value=mock_scheduler), \
|
|
patch("src.plugins.llm_analysis.scheduler.get_task_manager", return_value=mock_task_manager):
|
|
|
|
schedule_dashboard_validation(
|
|
dashboard_id="dash-2",
|
|
cron_expression="0 0 * * *",
|
|
params={},
|
|
)
|
|
|
|
job_func = mock_scheduler.add_job.call_args[0][0]
|
|
import asyncio
|
|
asyncio.run(job_func())
|
|
|
|
mock_task_manager.create_task.assert_called_once_with(
|
|
plugin_id="llm_dashboard_validation",
|
|
params={"dashboard_id": "dash-2"},
|
|
)
|
|
# #endregion Test.LLMAnalysisScheduler
|