fix: don't double-report a failed SEC run, and correct the rollback doc
Three follow-ups from review of the A6 commits. The cache-summary re-finalize raised a second durable system event on a failed run: _runtime_finish emits for `error`/`rate_limited`, and the dedup key includes the message, so "SEC unavailable" and "SEC unavailable · cache 511 · 2 score inputs changed" landed as two unacknowledged Admin events. Adds `emit_event` so a re-finalize that only rewords an outcome stays silent, with a regression test asserting exactly one event. The rollback section still claimed disabling the SEC job freezes the cache — the opposite of what the same page says two lines earlier, and of what the code now does. Rewritten: there is no Admin cache-off switch, restoring `fundamental_data` alone is temporary because the next run rebuilds it from the same snapshots and code, and a real freeze means stopping the service. Remaining "shadow" wording: the two import jobs have never been shadow since activation, so `_run_shadow_import` -> `_run_source_import`, its section heading, the deployment doc's job label, and the plan doc's "production switch remains" handoff paragraph are all brought up to date. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Unit tests for app.scheduler module."""
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
@@ -12,7 +13,7 @@ from app.scheduler import (
|
||||
_parse_frequency,
|
||||
_resume_tickers,
|
||||
_last_successful,
|
||||
_run_shadow_import,
|
||||
_run_source_import,
|
||||
run_sec_fundamentals_import,
|
||||
configure_scheduler,
|
||||
get_job_runtime_snapshot,
|
||||
@@ -175,7 +176,7 @@ class _SessionContext:
|
||||
return None
|
||||
|
||||
|
||||
class TestShadowImportJobs:
|
||||
class TestSourceImportJobs:
|
||||
@staticmethod
|
||||
def _session_factory():
|
||||
return _SessionContext()
|
||||
@@ -193,7 +194,7 @@ class TestShadowImportJobs:
|
||||
monkeypatch.setattr("app.scheduler._is_job_enabled", enabled)
|
||||
monkeypatch.setattr("app.scheduler.run_import", imported)
|
||||
|
||||
await _run_shadow_import("dolt_earnings_import", object())
|
||||
await _run_source_import("dolt_earnings_import", object())
|
||||
|
||||
runtime = get_job_runtime_snapshot("dolt_earnings_import")
|
||||
assert runtime["status"] == "completed"
|
||||
@@ -213,7 +214,7 @@ class TestShadowImportJobs:
|
||||
monkeypatch.setattr("app.scheduler._is_job_enabled", enabled)
|
||||
monkeypatch.setattr("app.scheduler.run_import", imported)
|
||||
|
||||
await _run_shadow_import("sec_fundamentals_import", object())
|
||||
await _run_source_import("sec_fundamentals_import", object())
|
||||
|
||||
runtime = get_job_runtime_snapshot("sec_fundamentals_import")
|
||||
assert runtime["status"] == "error"
|
||||
@@ -235,7 +236,7 @@ class TestShadowImportJobs:
|
||||
monkeypatch.setattr("app.scheduler._is_job_enabled", enabled)
|
||||
monkeypatch.setattr("app.scheduler.run_import", imported)
|
||||
|
||||
await _run_shadow_import("sec_fundamentals_import", object())
|
||||
await _run_source_import("sec_fundamentals_import", object())
|
||||
|
||||
runtime = get_job_runtime_snapshot("sec_fundamentals_import")
|
||||
assert runtime["status"] == STATUS_DEFERRED
|
||||
@@ -253,7 +254,7 @@ class TestShadowImportJobs:
|
||||
monkeypatch.setattr("app.scheduler._is_job_enabled", enabled)
|
||||
monkeypatch.setattr("app.scheduler.run_import", imported)
|
||||
|
||||
await _run_shadow_import("dolt_earnings_import", object())
|
||||
await _run_source_import("dolt_earnings_import", object())
|
||||
|
||||
runtime = get_job_runtime_snapshot("dolt_earnings_import")
|
||||
assert runtime["status"] == "skipped"
|
||||
@@ -270,7 +271,7 @@ class TestShadowImportJobs:
|
||||
monkeypatch.setattr("app.scheduler._is_job_enabled", disabled)
|
||||
monkeypatch.setattr("app.scheduler.run_import", should_not_run)
|
||||
|
||||
await _run_shadow_import("sec_fundamentals_import", object())
|
||||
await _run_source_import("sec_fundamentals_import", object())
|
||||
|
||||
runtime = get_job_runtime_snapshot("sec_fundamentals_import")
|
||||
assert runtime["status"] == "skipped"
|
||||
@@ -278,6 +279,7 @@ class TestShadowImportJobs:
|
||||
|
||||
async def test_sec_failure_still_runs_local_cache_refresh(self, monkeypatch):
|
||||
calls = []
|
||||
events = []
|
||||
|
||||
async def enabled(db, job_name):
|
||||
return True
|
||||
@@ -294,15 +296,20 @@ class TestShadowImportJobs:
|
||||
"composite_scores_staled": 2,
|
||||
}
|
||||
|
||||
async def record(**kwargs):
|
||||
events.append(kwargs)
|
||||
|
||||
monkeypatch.setattr("app.scheduler.async_session_factory", self._session_factory)
|
||||
monkeypatch.setattr("app.scheduler._is_job_enabled", enabled)
|
||||
monkeypatch.setattr("app.scheduler.run_import", unavailable)
|
||||
monkeypatch.setattr("app.scheduler._record_system_event", record)
|
||||
monkeypatch.setattr(
|
||||
"app.scheduler.fundamental_data_refresh_service.refresh",
|
||||
refreshed,
|
||||
)
|
||||
|
||||
await run_sec_fundamentals_import()
|
||||
await asyncio.sleep(0) # let the fire-and-forget event task run
|
||||
|
||||
assert len(calls) == 1
|
||||
runtime = get_job_runtime_snapshot("sec_fundamentals_import")
|
||||
@@ -311,6 +318,10 @@ class TestShadowImportJobs:
|
||||
assert runtime["message"] == (
|
||||
"SEC unavailable · cache 511 · 2 score inputs changed"
|
||||
)
|
||||
# Rewording the outcome must not duplicate the durable event: the dedup
|
||||
# key includes the message, so a second finish would show up twice in
|
||||
# Admin → System Events.
|
||||
assert len(events) == 1, events
|
||||
|
||||
async def test_sec_success_surfaces_cache_refresh_summary(self, monkeypatch):
|
||||
async def enabled(db, job_name):
|
||||
@@ -421,5 +432,3 @@ class TestShadowImportJobs:
|
||||
assert runtime["message"] == (
|
||||
"Import disabled · cache 511 · 2 score inputs changed"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user