fix: don't double-report a failed SEC run, and correct the rollback doc
Deploy / lint (push) Successful in 11s
Deploy / test (push) Successful in 1m24s
Deploy / deploy (push) Successful in 41s

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:
2026-08-07 11:46:34 +02:00
co-authored by Claude Opus 5
parent 13f3636b6a
commit e1607ddbff
4 changed files with 63 additions and 36 deletions
+18 -9
View File
@@ -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"
)