diff --git a/app/scheduler.py b/app/scheduler.py index d6ad8b4..da5b11c 100644 --- a/app/scheduler.py +++ b/app/scheduler.py @@ -905,24 +905,23 @@ async def run_sec_fundamentals_import() -> None: f"cache {summary['refreshed']} · " 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) - if not import_ran: - _runtime_finish( - job_name, - "completed", - processed=1, - total=1, - message=f"Import disabled · {cache_message}", - ) - elif runtime.get("status") == "completed": + if import_ran: + status = str(runtime.get("status") or "completed") import_message = runtime.get("message") or "import completed" - _runtime_finish( - job_name, - "completed", - processed=1, - total=1, - message=f"{import_message} · {cache_message}", - ) + processed = 1 if status == "completed" else 0 + else: + status, import_message, processed = "completed", "Import disabled", 1 + _runtime_finish( + job_name, + status, + processed=processed, + total=1, + message=f"{import_message} · {cache_message}", + ) # --------------------------------------------------------------------------- diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py index 853789e..c31715a 100644 --- a/tests/unit/test_scheduler.py +++ b/tests/unit/test_scheduler.py @@ -276,7 +276,7 @@ class TestShadowImportJobs: assert runtime["status"] == "skipped" 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 = [] async def enabled(db, job_name): @@ -307,7 +307,10 @@ class TestShadowImportJobs: assert len(calls) == 1 runtime = get_job_runtime_snapshot("sec_fundamentals_import") 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 enabled(db, job_name): @@ -342,6 +345,42 @@ class TestShadowImportJobs: "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): """Disabling the job stops the SEC fetch, not the local cache.