From d66f70f8119008f5843a39686b0eb3426c796d97 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Fri, 26 Jun 2026 14:38:16 +0900 Subject: [PATCH 1/2] Enforce newsletter sync interval floor --- apps/api/src/five08/backend/api.py | 4 ++-- packages/shared/src/five08/settings.py | 2 +- tests/unit/test_backend_api.py | 13 +++++++++++++ tests/unit/test_shared_settings.py | 10 ++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/apps/api/src/five08/backend/api.py b/apps/api/src/five08/backend/api.py index 17ce59e6..8d002f82 100644 --- a/apps/api/src/five08/backend/api.py +++ b/apps/api/src/five08/backend/api.py @@ -586,7 +586,7 @@ def _crm_sync_idempotency_key(*, now: datetime) -> str: def _newsletter_sync_idempotency_key(*, now: datetime) -> str: - interval_seconds = max(1, settings.newsletter_sync_interval_seconds) + interval_seconds = max(60, settings.newsletter_sync_interval_seconds) bucket = int(now.timestamp()) // interval_seconds return f"newsletter-sync:508-members:{bucket}" @@ -718,7 +718,7 @@ async def _crm_sync_scheduler(app: FastAPI) -> None: async def _newsletter_sync_scheduler(app: FastAPI) -> None: queue = app.state.queue - interval_seconds = max(1, settings.newsletter_sync_interval_seconds) + interval_seconds = max(60, settings.newsletter_sync_interval_seconds) while True: try: await _enqueue_newsletter_sync_job(queue, reason="scheduler") diff --git a/packages/shared/src/five08/settings.py b/packages/shared/src/five08/settings.py index ad61bb30..e645035d 100644 --- a/packages/shared/src/five08/settings.py +++ b/packages/shared/src/five08/settings.py @@ -107,7 +107,7 @@ class SharedSettings(BaseSettings): ) keila_api_timeout_seconds: float = 20.0 newsletter_sync_enabled: bool = True - newsletter_sync_interval_seconds: int = 604800 + newsletter_sync_interval_seconds: int = Field(default=604800, ge=60) newsletter_sync_excluded_mailboxes: str = "" onboarding_email_smtp_server: str | None = Field( default=None, diff --git a/tests/unit/test_backend_api.py b/tests/unit/test_backend_api.py index 9fe4fd89..46230ca3 100644 --- a/tests/unit/test_backend_api.py +++ b/tests/unit/test_backend_api.py @@ -7089,6 +7089,19 @@ async def test_manual_newsletter_sync_idempotency_keys_are_unique() -> None: assert all(key.startswith("newsletter-sync:508-members:dashboard:") for key in keys) +def test_newsletter_sync_idempotency_key_clamps_interval_floor( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Scheduler idempotency buckets should keep the same one-minute floor as settings.""" + monkeypatch.setattr(api.settings, "newsletter_sync_interval_seconds", 1) + + key = api._newsletter_sync_idempotency_key( + now=datetime(1970, 1, 1, 0, 1, 30, tzinfo=timezone.utc), + ) + + assert key == "newsletter-sync:508-members:1" + + def test_dashboard_sync_newsletters_workflows_engineer_is_dry_run( client: TestClient, ) -> None: diff --git a/tests/unit/test_shared_settings.py b/tests/unit/test_shared_settings.py index 048d1de1..fd909300 100644 --- a/tests/unit/test_shared_settings.py +++ b/tests/unit/test_shared_settings.py @@ -168,6 +168,16 @@ def test_shared_settings_brevo_members_list_id_accepts_numeric_string() -> None: assert settings.brevo_508_members_newsletter_list_id == 4 +def test_shared_settings_newsletter_sync_interval_requires_one_minute() -> None: + """Newsletter scheduler intervals should match dashboard runtime-config limits.""" + with pytest.raises(ValidationError): + SharedSettings(newsletter_sync_interval_seconds=59) + + settings = SharedSettings(newsletter_sync_interval_seconds=60) + + assert settings.newsletter_sync_interval_seconds == 60 + + def test_local_service_defaults_target_host_runtime( monkeypatch: pytest.MonkeyPatch, ) -> None: From dc422b12f7f237cfc40c49bca98a5b46d905fb87 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Fri, 26 Jun 2026 16:26:20 +0900 Subject: [PATCH 2/2] Stabilize dashboard Playwright status test --- tests/integration/test_dashboard_playwright.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/integration/test_dashboard_playwright.py b/tests/integration/test_dashboard_playwright.py index ffbe9b51..3e007f04 100644 --- a/tests/integration/test_dashboard_playwright.py +++ b/tests/integration/test_dashboard_playwright.py @@ -355,7 +355,6 @@ def test_dashboard_interactivity_with_playwright(dashboard_server: str) -> None: assign_onboarder_requested = threading.Event() update_onboarding_status_requested = threading.Event() detail_requested = threading.Event() - gig_application_requested = threading.Event() gig_application_add_requested = threading.Event() gig_list_requests: list[str] = [] gig_detail_requests: list[str] = [] @@ -540,7 +539,6 @@ def notifications_route(route: Any) -> None: ) def gig_application_status_route(route: Any) -> None: - gig_application_requested.set() body = route.request.post_data_json assert body["status"] == "unavailable" casey_application_id = "22222222-2222-4222-8222-222222222222" @@ -761,19 +759,19 @@ def sync_route(route: Any) -> None: casey_status = page.get_by_label("Candidate status for Casey Candidate") expect(casey_status).to_be_enabled() expect(casey_status).to_have_value("suggested") - with page.expect_request( - lambda request: ( - request.method == "POST" - and request.url.endswith( + with page.expect_response( + lambda response: ( + response.request.method == "POST" + and response.url.endswith( "/dashboard/api/gigs/" "11111111-1111-4111-8111-111111111111" "/applications/" "22222222-2222-4222-8222-222222222222/status" ) + and response.status == 200 ) ): casey_status.select_option("unavailable") - assert gig_application_requested.wait(timeout=5) expect(casey_status).to_have_value("unavailable") assert gig_detail_requests