import asyncio from datetime import datetime, timedelta from pathlib import Path from fastapi import FastAPI from webapp import app as app_module def test_startup_recovery_runs_overdue_scheduled_posts_without_waiting_for_retry() -> None: now = datetime.now().astimezone() item = { "status": "scheduled", "scheduled_for": (now - timedelta(minutes=5)).isoformat(), "scheduled_next_attempt_at": (now + timedelta(minutes=30)).isoformat(), } assert app_module.is_scheduled_publish_due(item, recover_missed_jobs=True) is True assert app_module.is_scheduled_publish_due(item, recover_missed_jobs=False) is False def test_manual_publish_retries_once_after_a_failed_attempt(monkeypatch) -> None: app = FastAPI() directory_name = "retry-once" app.state.operations = { directory_name: { "running": True, "completed": False, "output": "", "result": None, "error": None, } } app.state.operation_tasks = {directory_name: object()} app.state.scheduler_lock = asyncio.Lock() attempts = [] async def fake_run_publish_action(post_dir: Path, config, progress): attempts.append(post_dir) return {"status": "failed", "output": "still unavailable"} monkeypatch.setattr(app_module, "run_publish_action_with_progress", fake_run_publish_action) asyncio.run(app_module.run_publish_operation(app, directory_name, Path("/tmp/retry-once"), None)) assert attempts == [Path("/tmp/retry-once"), Path("/tmp/retry-once")] operation = app.state.operations[directory_name] assert operation["completed"] is True assert operation["result"] == "failed" assert directory_name not in app.state.operation_tasks