fix: make SEC quality gating terminal-safe
This commit is contained in:
@@ -481,6 +481,209 @@ async def test_unresolved_filing_stops_blocking_after_retry_window(engine):
|
||||
assert await _count(factory, SecFilingGap) == 0
|
||||
|
||||
|
||||
async def test_queued_gap_without_index_date_retries_without_wedging_and_escalates_once(
|
||||
engine,
|
||||
):
|
||||
factory = _factory(engine)
|
||||
await _seed(factory, ["AAPL"])
|
||||
backfill = FakeSecClient(
|
||||
tickers={"AAPL": 320193},
|
||||
companyfacts={320193: _companyfacts([CF_K, CF_Q1], [SH_K, SH_Q1])},
|
||||
submissions={320193: _submissions(SUB_FILINGS)},
|
||||
latest_index=date(2026, 1, 31),
|
||||
)
|
||||
await run_import(_importer(backfill), engine=engine)
|
||||
|
||||
old = datetime(2026, 4, 1, tzinfo=timezone.utc)
|
||||
async with factory() as db:
|
||||
db.add(SecFilingGap(
|
||||
cik="0000320193",
|
||||
accession="DATELESS",
|
||||
form="10-Q",
|
||||
index_date=None,
|
||||
reason="not_in_companyfacts",
|
||||
first_seen_at=old,
|
||||
last_attempted_at=old,
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
missing = FakeSecClient(
|
||||
tickers={"AAPL": 320193},
|
||||
companyfacts={320193: _companyfacts([CF_K, CF_Q1], [SH_K, SH_Q1])},
|
||||
submissions={320193: _submissions(SUB_FILINGS + [
|
||||
_filing(
|
||||
"DATELESS",
|
||||
"10-Q",
|
||||
"2026-03-28",
|
||||
"2026-05-01",
|
||||
"2026-05-01T10:01:00.000Z",
|
||||
)
|
||||
])},
|
||||
latest_index=date(2026, 1, 31),
|
||||
)
|
||||
first = await run_import(
|
||||
_importer(missing, today=date(2026, 5, 20)), engine=engine
|
||||
)
|
||||
second = await run_import(
|
||||
_importer(missing, today=date(2026, 5, 21)), engine=engine
|
||||
)
|
||||
|
||||
assert first.status == STATUS_PROMOTED
|
||||
assert second.status == STATUS_PROMOTED
|
||||
async with factory() as db:
|
||||
gap = (await db.execute(select(SecFilingGap))).scalar_one()
|
||||
events = (
|
||||
await db.execute(
|
||||
select(SystemEvent).where(SystemEvent.code == "filing_gap_aged")
|
||||
)
|
||||
).scalars().all()
|
||||
assert gap.escalated_at is not None
|
||||
assert len(events) == 1
|
||||
|
||||
|
||||
async def test_queued_filing_reclassified_non_xbrl_is_removed(engine):
|
||||
factory = _factory(engine)
|
||||
await _seed(factory, ["AAPL"])
|
||||
backfill = FakeSecClient(
|
||||
tickers={"AAPL": 320193},
|
||||
companyfacts={320193: _companyfacts([CF_K, CF_Q1], [SH_K, SH_Q1])},
|
||||
submissions={320193: _submissions(SUB_FILINGS)},
|
||||
latest_index=date(2026, 1, 31),
|
||||
)
|
||||
await run_import(_importer(backfill), engine=engine)
|
||||
now = datetime.now(timezone.utc)
|
||||
async with factory() as db:
|
||||
db.add(SecFilingGap(
|
||||
cik="0000320193",
|
||||
accession="NONX",
|
||||
form="10-Q/A",
|
||||
index_date=date(2026, 5, 1),
|
||||
reason="not_in_companyfacts",
|
||||
first_seen_at=now,
|
||||
last_attempted_at=now,
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
client = FakeSecClient(
|
||||
tickers={"AAPL": 320193},
|
||||
companyfacts={320193: _companyfacts([CF_K, CF_Q1], [SH_K, SH_Q1])},
|
||||
submissions={320193: _submissions(SUB_FILINGS + [
|
||||
_filing(
|
||||
"NONX",
|
||||
"10-Q/A",
|
||||
"2026-03-28",
|
||||
"2026-05-01",
|
||||
"2026-05-01T10:01:00.000Z",
|
||||
is_xbrl=False,
|
||||
)
|
||||
])},
|
||||
latest_index=date(2026, 1, 31),
|
||||
)
|
||||
run = await run_import(_importer(client, today=date(2026, 5, 20)), engine=engine)
|
||||
|
||||
assert run.status == STATUS_PROMOTED
|
||||
assert await _count(factory, SecFilingGap) == 0
|
||||
|
||||
|
||||
async def test_queued_parser_skip_stays_blocked_with_actionable_reason(
|
||||
engine, monkeypatch
|
||||
):
|
||||
from app.services.sec_facts_parser import ParseResult
|
||||
|
||||
factory = _factory(engine)
|
||||
await _seed(factory, ["AAPL"])
|
||||
backfill = FakeSecClient(
|
||||
tickers={"AAPL": 320193},
|
||||
companyfacts={320193: _companyfacts([CF_K, CF_Q1], [SH_K, SH_Q1])},
|
||||
submissions={320193: _submissions(SUB_FILINGS)},
|
||||
latest_index=date(2026, 1, 31),
|
||||
)
|
||||
await run_import(_importer(backfill), engine=engine)
|
||||
now = datetime.now(timezone.utc)
|
||||
async with factory() as db:
|
||||
db.add(SecFilingGap(
|
||||
cik="0000320193",
|
||||
accession="BADPARSE",
|
||||
form="10-Q",
|
||||
index_date=date(2026, 5, 1),
|
||||
reason="not_in_companyfacts",
|
||||
first_seen_at=now,
|
||||
last_attempted_at=now,
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
bad_fact = _rev(
|
||||
"2025-09-28", "2026-03-28", 254940, 2026, "Q2", "BADPARSE"
|
||||
)
|
||||
bad_share = _shares("2026-04-17", 14687, "BADPARSE", 2026, "Q2")
|
||||
client = FakeSecClient(
|
||||
tickers={"AAPL": 320193},
|
||||
companyfacts={
|
||||
320193: _companyfacts(
|
||||
[CF_K, CF_Q1, bad_fact], [SH_K, SH_Q1, bad_share]
|
||||
)
|
||||
},
|
||||
submissions={320193: _submissions(SUB_FILINGS + [
|
||||
_filing(
|
||||
"BADPARSE",
|
||||
"10-Q",
|
||||
"2026-03-28",
|
||||
"2026-05-01",
|
||||
"2026-05-01T10:01:00.000Z",
|
||||
)
|
||||
])},
|
||||
latest_index=date(2026, 1, 31),
|
||||
)
|
||||
|
||||
def skip_parse(*args, **kwargs):
|
||||
return ParseResult(skipped_filings=[{
|
||||
"accession": "BADPARSE",
|
||||
"reason": "unparseable",
|
||||
}])
|
||||
|
||||
monkeypatch.setattr("app.services.sec_facts_parser.parse_snapshots", skip_parse)
|
||||
run = await run_import(_importer(client, today=date(2026, 5, 20)), engine=engine)
|
||||
|
||||
assert run.status == STATUS_PROMOTED
|
||||
async with factory() as db:
|
||||
gap = (await db.execute(select(SecFilingGap))).scalar_one()
|
||||
assert gap.reason == "parser_unusable"
|
||||
|
||||
|
||||
async def test_validation_caps_details_but_keeps_complete_blocked_cik_set():
|
||||
importer = SecFundamentalsImporter(today=date(2026, 5, 20))
|
||||
importer._latest_index_date = date(2026, 5, 19)
|
||||
staged = StagedFundamentals(
|
||||
resolved=ResolvedUniverse(),
|
||||
missing_xbrl=[
|
||||
{
|
||||
"cik": f"{i:010d}",
|
||||
"accession": f"MISS-{i}",
|
||||
"form": "10-Q",
|
||||
"index_date": date(2026, 5, 1),
|
||||
"age_days": 19,
|
||||
"reason": "not_in_companyfacts",
|
||||
}
|
||||
for i in range(60)
|
||||
],
|
||||
no_xbrl_filings=[
|
||||
{"cik": f"{i + 100:010d}", "name": f"New {i}"}
|
||||
for i in range(60)
|
||||
],
|
||||
recovered=[
|
||||
{"cik": f"{i:010d}", "accession": f"REC-{i}", "source_cik": "1"}
|
||||
for i in range(60)
|
||||
],
|
||||
)
|
||||
|
||||
result = await importer.validate(None, staged)
|
||||
|
||||
assert len(result.summary["missing_xbrl"]) == 50
|
||||
assert len(result.summary["no_xbrl_filings"]) == 50
|
||||
assert len(result.summary["recovered_from_coregistrant"]) == 50
|
||||
assert len(result.summary["setup_blocked_ciks"]) == 120
|
||||
|
||||
|
||||
async def test_non_xbrl_amendment_skipped_not_failed(engine):
|
||||
factory = _factory(engine)
|
||||
await _seed(factory, ["AAPL"])
|
||||
|
||||
Reference in New Issue
Block a user