From 08aa019439ee3a47e51e151b2ef0a393841d7264 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 27 Apr 2026 12:27:16 -0700 Subject: [PATCH] feat: add occurrence check to poster before strategy dispatch Parse the occurrence field from sub_row and skip execution when today is not the nth weekday specified (e.g. first_sunday). Check runs after sub_row fetch but before dupe guard. Two new tests confirm skip and pass paths using patched date.today in app.services.poster. --- app/services/poster.py | 14 ++++++++++ tests/services/test_poster.py | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/app/services/poster.py b/app/services/poster.py index 8d12aa6..136cab0 100644 --- a/app/services/poster.py +++ b/app/services/poster.py @@ -6,11 +6,13 @@ from __future__ import annotations import asyncio import logging +from datetime import date from pathlib import Path from app.core.config import get_settings from app.db.store import Store from app.services.platforms import get_client +from app.services.platforms.reddit_comment import is_nth_weekday, parse_occurrence logger = logging.getLogger(__name__) @@ -36,6 +38,18 @@ def _run_post(db_path: str, campaign_id: int, target: str, all_subs = store.list_campaign_subs(campaign_id) sub_row = next((s for s in all_subs if s["sub"] == target), {}) + # Occurrence check — skip if not the right week of the month + occurrence_str = (sub_row or {}).get("occurrence") + parsed = parse_occurrence(occurrence_str) + if parsed is not None: + weekday, n = parsed + if not is_nth_weekday(date.today(), weekday, n): + logger.info( + "Skipping %s / %s — not occurrence %s today", + campaign_id, target, occurrence_str, + ) + return {"skipped": True, "reason": f"occurrence {occurrence_str} not today"} + # Dupe guard (opt-out allowed per strategy) if strategy.supports_dupe_guard() and store.already_posted_this_week(campaign_id, target): return {"skipped": True, "reason": f"already posted to {target!r} this week"} diff --git a/tests/services/test_poster.py b/tests/services/test_poster.py index c8e498a..1ccf846 100644 --- a/tests/services/test_poster.py +++ b/tests/services/test_poster.py @@ -113,3 +113,52 @@ def test_run_post_unknown_type_skips(tmp_path): assert result["skipped"] is True assert "Unknown campaign type" in result["reason"] + + +def test_occurrence_skip(tmp_path): + """When occurrence is 'first_sunday' and today is NOT the first Sunday, post is skipped.""" + db = str(tmp_path / "test.db") + # 2026-04-19 is a Sunday but the 3rd Sunday of April 2026 + mock_store = _make_store( + campaign_type="reddit_comment", + subs=[{"sub": "selfhosted", "active": 1, "occurrence": "first_sunday"}], + ) + mock_strategy = MagicMock() + mock_strategy.supports_dupe_guard.return_value = False + + with patch("app.services.poster.Store", return_value=mock_store): + with patch("app.services.poster.get_client", return_value=mock_strategy): + with patch("app.services.poster.date") as mock_date: + from datetime import date as real_date + mock_date.today.return_value = real_date(2026, 4, 19) + result = _run_post(db, campaign_id=1, target="selfhosted", triggered_by="scheduler") + + assert result["skipped"] is True + assert "occurrence" in result["reason"] + mock_strategy.execute.assert_not_called() + + +def test_occurrence_passes(tmp_path): + """When occurrence is 'first_sunday' and today IS the first Sunday, post proceeds.""" + db = str(tmp_path / "test.db") + # 2026-05-03 is the first Sunday of May 2026 + mock_store = _make_store( + campaign_type="reddit_comment", + subs=[{"sub": "selfhosted", "active": 1, "occurrence": "first_sunday"}], + ) + mock_result = MagicMock() + mock_result.url = "https://reddit.com/r/selfhosted/comments/abc/" + + mock_strategy = MagicMock() + mock_strategy.supports_dupe_guard.return_value = False + mock_strategy.execute.return_value = mock_result + + with patch("app.services.poster.Store", return_value=mock_store): + with patch("app.services.poster.get_client", return_value=mock_strategy): + with patch("app.services.poster.date") as mock_date: + from datetime import date as real_date + mock_date.today.return_value = real_date(2026, 5, 3) + result = _run_post(db, campaign_id=1, target="selfhosted", triggered_by="scheduler") + + assert result.get("skipped") is not True + mock_strategy.execute.assert_called_once()