fix: surface the fundamentals cache result on every SEC job outcome
The runtime message only appended the cache summary when the import itself completed. On a deferred, failed or source-locked run Admin → Jobs showed just the import outcome, so an operator had no signal that `fundamental_data` had advanced — contradicting the claim now made in the docstring, the schedule hint and the deployment doc. The import status still varies and stays the headline; the cache summary is appended to all of them. Adds a source-locked regression test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+10
-11
@@ -905,21 +905,20 @@ async def run_sec_fundamentals_import() -> None:
|
|||||||
f"cache {summary['refreshed']} · "
|
f"cache {summary['refreshed']} · "
|
||||||
f"{summary['score_inputs_changed']} score inputs changed"
|
f"{summary['score_inputs_changed']} score inputs changed"
|
||||||
)
|
)
|
||||||
|
# Every outcome carries the cache summary — including deferred, failed and
|
||||||
|
# source-locked ones. The import status is what varies; the refresh always
|
||||||
|
# happened, and Admin → Jobs is the only place an operator sees that.
|
||||||
runtime = get_job_runtime_snapshot(job_name)
|
runtime = get_job_runtime_snapshot(job_name)
|
||||||
if not import_ran:
|
if import_ran:
|
||||||
_runtime_finish(
|
status = str(runtime.get("status") or "completed")
|
||||||
job_name,
|
|
||||||
"completed",
|
|
||||||
processed=1,
|
|
||||||
total=1,
|
|
||||||
message=f"Import disabled · {cache_message}",
|
|
||||||
)
|
|
||||||
elif runtime.get("status") == "completed":
|
|
||||||
import_message = runtime.get("message") or "import completed"
|
import_message = runtime.get("message") or "import completed"
|
||||||
|
processed = 1 if status == "completed" else 0
|
||||||
|
else:
|
||||||
|
status, import_message, processed = "completed", "Import disabled", 1
|
||||||
_runtime_finish(
|
_runtime_finish(
|
||||||
job_name,
|
job_name,
|
||||||
"completed",
|
status,
|
||||||
processed=1,
|
processed=processed,
|
||||||
total=1,
|
total=1,
|
||||||
message=f"{import_message} · {cache_message}",
|
message=f"{import_message} · {cache_message}",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ class TestShadowImportJobs:
|
|||||||
assert runtime["status"] == "skipped"
|
assert runtime["status"] == "skipped"
|
||||||
assert runtime["message"] == "Disabled"
|
assert runtime["message"] == "Disabled"
|
||||||
|
|
||||||
async def test_sec_failure_still_runs_activated_local_refresh(self, monkeypatch):
|
async def test_sec_failure_still_runs_local_cache_refresh(self, monkeypatch):
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
async def enabled(db, job_name):
|
async def enabled(db, job_name):
|
||||||
@@ -307,7 +307,10 @@ class TestShadowImportJobs:
|
|||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
runtime = get_job_runtime_snapshot("sec_fundamentals_import")
|
runtime = get_job_runtime_snapshot("sec_fundamentals_import")
|
||||||
assert runtime["status"] == "error"
|
assert runtime["status"] == "error"
|
||||||
assert runtime["message"] == "SEC unavailable"
|
# the failure stays the headline, but the cache result is still visible
|
||||||
|
assert runtime["message"] == (
|
||||||
|
"SEC unavailable · cache 511 · 2 score inputs changed"
|
||||||
|
)
|
||||||
|
|
||||||
async def test_sec_success_surfaces_cache_refresh_summary(self, monkeypatch):
|
async def test_sec_success_surfaces_cache_refresh_summary(self, monkeypatch):
|
||||||
async def enabled(db, job_name):
|
async def enabled(db, job_name):
|
||||||
@@ -342,6 +345,42 @@ class TestShadowImportJobs:
|
|||||||
"no_op · abcdef123456 · cache 511 · 2 score inputs changed"
|
"no_op · abcdef123456 · cache 511 · 2 score inputs changed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def test_source_locked_sec_run_still_reports_the_cache_refresh(
|
||||||
|
self, monkeypatch
|
||||||
|
):
|
||||||
|
"""A skipped import keeps its skip status but shows the cache advanced."""
|
||||||
|
|
||||||
|
async def enabled(db, job_name):
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def locked(importer):
|
||||||
|
return None # another import owns the source lock
|
||||||
|
|
||||||
|
async def refreshed(db):
|
||||||
|
return {
|
||||||
|
"refreshed": 511,
|
||||||
|
"score_inputs_changed": 0,
|
||||||
|
"dimension_scores_staled": 0,
|
||||||
|
"composite_scores_staled": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.scheduler.async_session_factory", self._session_factory)
|
||||||
|
monkeypatch.setattr("app.scheduler._is_job_enabled", enabled)
|
||||||
|
monkeypatch.setattr("app.scheduler.run_import", locked)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"app.scheduler.fundamental_data_refresh_service.refresh",
|
||||||
|
refreshed,
|
||||||
|
)
|
||||||
|
|
||||||
|
await run_sec_fundamentals_import()
|
||||||
|
|
||||||
|
runtime = get_job_runtime_snapshot("sec_fundamentals_import")
|
||||||
|
assert runtime["status"] == "skipped"
|
||||||
|
assert runtime["message"] == (
|
||||||
|
"Another import for this source is already running · "
|
||||||
|
"cache 511 · 0 score inputs changed"
|
||||||
|
)
|
||||||
|
|
||||||
async def test_disabled_sec_job_still_refreshes_local_cache(self, monkeypatch):
|
async def test_disabled_sec_job_still_refreshes_local_cache(self, monkeypatch):
|
||||||
"""Disabling the job stops the SEC fetch, not the local cache.
|
"""Disabling the job stops the SEC fetch, not the local cache.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user