feat(MAT-174): Add cron job scheduler and executors

Cron package that syncs jobs from matrixhost portal API, schedules execution
with timezone-aware timing, and posts results to Matrix rooms. Includes
Brave Search, reminder, and browser scrape (placeholder) executors with
formatter. 31 pytest tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-03-16 09:31:19 +02:00
parent 21b8a4efb1
commit 4d8ea44b3d
15 changed files with 1009 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
"""Tests for the Brave Search cron executor."""
import os
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from cron.brave_search import execute_brave_search
@pytest.fixture
def job():
return {
"id": "j1",
"name": "BMW Search",
"jobType": "brave_search",
"config": {"query": "BMW X3 damaged Cyprus", "maxResults": 5},
"targetRoom": "!room:cars",
"dedupKeys": ["https://old-result.com"],
}
class TestBraveSearchExecutor:
@pytest.mark.asyncio
async def test_returns_error_without_api_key(self, job):
with patch.dict(os.environ, {"BRAVE_API_KEY": ""}, clear=False):
# Need to reload module to pick up empty env
import importlib
import cron.brave_search as bs
original_key = bs.BRAVE_API_KEY
bs.BRAVE_API_KEY = ""
try:
result = await execute_brave_search(job=job, send_text=AsyncMock())
assert result["status"] == "error"
assert "BRAVE_API_KEY" in result["error"]
finally:
bs.BRAVE_API_KEY = original_key
@pytest.mark.asyncio
async def test_returns_error_without_query(self):
job = {
"id": "j1",
"name": "Empty",
"jobType": "brave_search",
"config": {},
"targetRoom": "!room:test",
"dedupKeys": [],
}
import cron.brave_search as bs
original_key = bs.BRAVE_API_KEY
bs.BRAVE_API_KEY = "test-key"
try:
result = await execute_brave_search(job=job, send_text=AsyncMock())
assert result["status"] == "error"
assert "query" in result["error"].lower()
finally:
bs.BRAVE_API_KEY = original_key
@pytest.mark.asyncio
async def test_deduplicates_results(self, job):
"""Results with URLs already in dedupKeys should be filtered out."""
import cron.brave_search as bs
original_key = bs.BRAVE_API_KEY
bs.BRAVE_API_KEY = "test-key"
mock_response = MagicMock()
mock_response.json.return_value = {
"web": {
"results": [
{"title": "Old Result", "url": "https://old-result.com", "description": "Already seen"},
{"title": "New BMW", "url": "https://new-result.com", "description": "Fresh listing"},
]
}
}
mock_response.raise_for_status = MagicMock()
send_text = AsyncMock()
with patch("httpx.AsyncClient") as mock_client_cls:
mock_client = AsyncMock()
mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
mock_client.get = AsyncMock(return_value=mock_response)
try:
result = await execute_brave_search(job=job, send_text=send_text)
finally:
bs.BRAVE_API_KEY = original_key
assert result["status"] == "success"
assert result["newDedupKeys"] == ["https://new-result.com"]
send_text.assert_called_once()
# Message should contain only the new result
msg = send_text.call_args[0][1]
assert "New BMW" in msg
assert "Old Result" not in msg
@pytest.mark.asyncio
async def test_no_results_status(self, job):
"""When API returns empty results, status should be no_results."""
import cron.brave_search as bs
original_key = bs.BRAVE_API_KEY
bs.BRAVE_API_KEY = "test-key"
mock_response = MagicMock()
mock_response.json.return_value = {"web": {"results": []}}
mock_response.raise_for_status = MagicMock()
with patch("httpx.AsyncClient") as mock_client_cls:
mock_client = AsyncMock()
mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
mock_client.get = AsyncMock(return_value=mock_response)
try:
result = await execute_brave_search(job=job, send_text=AsyncMock())
finally:
bs.BRAVE_API_KEY = original_key
assert result["status"] == "no_results"
@pytest.mark.asyncio
async def test_all_results_already_seen(self, job):
"""When all results are already in dedupKeys, status should be no_results."""
import cron.brave_search as bs
original_key = bs.BRAVE_API_KEY
bs.BRAVE_API_KEY = "test-key"
mock_response = MagicMock()
mock_response.json.return_value = {
"web": {
"results": [
{"title": "Old", "url": "https://old-result.com", "description": "Seen"},
]
}
}
mock_response.raise_for_status = MagicMock()
with patch("httpx.AsyncClient") as mock_client_cls:
mock_client = AsyncMock()
mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
mock_client.get = AsyncMock(return_value=mock_response)
try:
result = await execute_brave_search(job=job, send_text=AsyncMock())
finally:
bs.BRAVE_API_KEY = original_key
assert result["status"] == "no_results"