fix(sec): defer expected Company Facts lag without alerting
This commit is contained in:
@@ -29,6 +29,7 @@ from app.models.data_import_run import DataImportRun
|
||||
from app.models.fundamental_snapshot import FundamentalSnapshot
|
||||
from app.models.system_event import SystemEvent
|
||||
from app.services.data_import import (
|
||||
STATUS_DEFERRED,
|
||||
STATUS_FAILED,
|
||||
STATUS_NO_OP,
|
||||
STATUS_PROMOTED,
|
||||
@@ -67,9 +68,10 @@ class FakeImporter:
|
||||
|
||||
source = "sec_facts"
|
||||
|
||||
def __init__(self, revision, *, ok=True, n_rows=3, raise_in="none"):
|
||||
def __init__(self, revision, *, ok=True, retryable=False, n_rows=3, raise_in="none"):
|
||||
self.revision = revision
|
||||
self.ok = ok
|
||||
self.retryable = retryable
|
||||
self.n_rows = n_rows
|
||||
self.raise_in = raise_in
|
||||
self.staged_called = False
|
||||
@@ -94,6 +96,7 @@ class FakeImporter:
|
||||
summary={"staged_rows": len(staged)},
|
||||
source_max_date=date(2026, 7, 21),
|
||||
messages=[] if self.ok else ["coverage below threshold"],
|
||||
retryable=self.retryable,
|
||||
)
|
||||
|
||||
async def promote(self, db, staged, run_id):
|
||||
@@ -200,6 +203,20 @@ async def test_failed_validation_leaves_data_untouched(engine):
|
||||
assert await _count(factory, SystemEvent) == 1 # alerted
|
||||
|
||||
|
||||
async def test_retryable_validation_defers_without_alerting(engine):
|
||||
factory = _factory(engine)
|
||||
await run_import(FakeImporter("rev1", n_rows=3), engine=engine)
|
||||
|
||||
run = await run_import(
|
||||
FakeImporter("rev2", ok=False, retryable=True, n_rows=5), engine=engine
|
||||
)
|
||||
|
||||
assert run is not None and run.status == STATUS_DEFERRED
|
||||
assert "coverage" in (run.error_details or "")
|
||||
assert await _count(factory, FundamentalSnapshot) == 3 # untouched
|
||||
assert await _count(factory, SystemEvent) == 0 # expected retry does not alert
|
||||
|
||||
|
||||
async def test_exception_in_promote_rolls_back(engine):
|
||||
factory = _factory(engine)
|
||||
await run_import(FakeImporter("rev1", n_rows=3), engine=engine) # baseline
|
||||
|
||||
@@ -250,6 +250,28 @@ class TestShadowImportJobs:
|
||||
assert runtime["processed"] == 0
|
||||
assert runtime["message"] == "validation failed"
|
||||
|
||||
async def test_deferred_run_is_visible_without_error_status(self, monkeypatch):
|
||||
async def enabled(db, job_name):
|
||||
return True
|
||||
|
||||
async def imported(importer):
|
||||
return SimpleNamespace(
|
||||
status="deferred",
|
||||
revision="abcdef1234567890",
|
||||
error_details="Company Facts publication lag; retrying",
|
||||
)
|
||||
|
||||
monkeypatch.setattr("app.scheduler.async_session_factory", self._session_factory)
|
||||
monkeypatch.setattr("app.scheduler._is_job_enabled", enabled)
|
||||
monkeypatch.setattr("app.scheduler.run_import", imported)
|
||||
|
||||
await _run_shadow_import("sec_fundamentals_import", object())
|
||||
|
||||
runtime = get_job_runtime_snapshot("sec_fundamentals_import")
|
||||
assert runtime["status"] == "deferred"
|
||||
assert runtime["processed"] == 0
|
||||
assert runtime["message"] == "Company Facts publication lag; retrying"
|
||||
|
||||
async def test_source_lock_surfaces_skipped(self, monkeypatch):
|
||||
async def enabled(db, job_name):
|
||||
return True
|
||||
|
||||
@@ -17,7 +17,12 @@ import app.models # noqa: F401
|
||||
from app.models.fundamental_snapshot import FundamentalSnapshot
|
||||
from app.models.system_event import SystemEvent
|
||||
from app.models.ticker import Ticker
|
||||
from app.services.data_import import STATUS_FAILED, STATUS_PROMOTED, run_import
|
||||
from app.services.data_import import (
|
||||
STATUS_DEFERRED,
|
||||
STATUS_FAILED,
|
||||
STATUS_PROMOTED,
|
||||
run_import,
|
||||
)
|
||||
from app.services.sec_fundamentals_importer import SecFundamentalsImporter
|
||||
|
||||
|
||||
@@ -191,7 +196,7 @@ async def test_incremental_adds_only_new_filing(engine):
|
||||
assert q2.fiscal_period == "Q2" and q2.revenue == 254940
|
||||
|
||||
|
||||
async def test_consistency_gate_fails_when_facts_lag_index(engine):
|
||||
async def test_consistency_gate_defers_without_alert_when_facts_lag_index(engine):
|
||||
factory = _factory(engine)
|
||||
await _seed(factory, ["AAPL"])
|
||||
backfill = FakeSecClient(
|
||||
@@ -213,9 +218,9 @@ async def test_consistency_gate_fails_when_facts_lag_index(engine):
|
||||
)
|
||||
run = await run_import(_importer(incr, today=date(2026, 5, 3)), engine=engine)
|
||||
|
||||
assert run.status == STATUS_FAILED
|
||||
# The gate blocks every later run until it clears, so the alert itself has to
|
||||
# name the filing and say why it could not be resolved.
|
||||
assert run.status == STATUS_DEFERRED
|
||||
# The gate blocks every later run until it clears, so run history still has
|
||||
# to name the filing and say why it could not be resolved.
|
||||
details = run.error_details or ""
|
||||
assert "GHOST" in details and "not_in_companyfacts" in details
|
||||
assert "2026-05-01" in details # index date the filing was seen on
|
||||
@@ -224,6 +229,7 @@ async def test_consistency_gate_fails_when_facts_lag_index(engine):
|
||||
assert summary["missing_xbrl"][0]["accession"] == "GHOST"
|
||||
assert summary["missing_xbrl"][0]["form"] == "10-Q"
|
||||
assert await _count(factory, FundamentalSnapshot) == 2 # nothing new written
|
||||
assert await _count(factory, SystemEvent) == 0 # expected SEC lag does not alert
|
||||
|
||||
|
||||
async def test_gate_separates_missing_submissions_from_missing_facts(engine):
|
||||
|
||||
Reference in New Issue
Block a user