Freeze date-range previews and model settings, track per-news progress and downstream targets, and share completion-driven scheduling with realtime analysis. Prevent overlapping retries for the same news, retain batch queue associations, and refresh selected details with session-safe pagination.
95 lines
4.9 KiB
Python
95 lines
4.9 KiB
Python
"""Reprocessing checkpoints and retries isolate work by the failed stage."""
|
|
import asyncio
|
|
from types import SimpleNamespace as N
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
import pytest
|
|
|
|
|
|
def session_mock():
|
|
session = MagicMock()
|
|
session.__aenter__ = AsyncMock(return_value=session)
|
|
session.__aexit__ = AsyncMock(return_value=None)
|
|
for name in ('get', 'scalar', 'scalars', 'commit', 'rollback', 'flush', 'execute'):
|
|
setattr(session, name, AsyncMock())
|
|
return session
|
|
|
|
|
|
def test_pause_after_claim_does_not_count_old_success(trading, monkeypatch):
|
|
from alphapulse.news_risk import reprocess, worker
|
|
from alphapulse.models import NewsItem
|
|
from alphapulse.models.risk import RiskSettings, ReprocessJob, ArticleAnalysis
|
|
job = N(id=20, state='pending', position=0, items=[{'id':21,'hash':'abc'}], failures=[], settings={})
|
|
old = N(id=25, state='done', error=None, result={'old_success':True})
|
|
session = session_mock()
|
|
session.scalar.side_effect = [None, None, job, None, old]
|
|
session.get.side_effect = lambda model, *a, **kw: {
|
|
RiskSettings:N(enabled=True), NewsItem:N(id=21,content_hash='abc'), ArticleAnalysis:old, ReprocessJob:job}[model]
|
|
paused = session_mock()
|
|
paused.get.return_value = N(enabled=False)
|
|
model = AsyncMock(side_effect=AssertionError('Paused work must not call a model'))
|
|
monkeypatch.setattr(reprocess, 'SessionLocal', lambda:session)
|
|
monkeypatch.setattr(reprocess, 'frozen_configuration', AsyncMock(return_value=(N(),'rules',{})))
|
|
monkeypatch.setattr(worker, 'SessionLocal', lambda:paused)
|
|
monkeypatch.setattr(worker, 'complete', model)
|
|
asyncio.run(reprocess.process_job())
|
|
assert job.position == 0 and job.failures == []
|
|
assert old.result == {'old_success':True}
|
|
model.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.parametrize('stage', ['analysis', 'events', 'outlook'])
|
|
def test_downstream_failure_does_not_mark_unrelated_news_failed(trading, monkeypatch, stage):
|
|
from alphapulse.news_risk import reprocess, events
|
|
job = N(id=10,state='running',position=2,items=[{'id':11},{'id':12}],failures=[],
|
|
settings={'configurations':{'outlook':{'rules':{}}}})
|
|
failed = N(id=31,error='Synthetic failure',state='failed',quotes={})
|
|
session = session_mock()
|
|
session.get.return_value = N(enabled=True)
|
|
session.scalar.side_effect = [None,None,job,None,None,failed]
|
|
session.scalars.side_effect = [N(all=lambda:[failed] if stage == 'analysis' else []),
|
|
N(all=lambda:[failed] if stage == 'events' else [])]
|
|
monkeypatch.setattr(reprocess,'SessionLocal',lambda:session)
|
|
monkeypatch.setattr(events,'reading_inputs',AsyncMock(return_value=[]))
|
|
asyncio.run(reprocess.process_job())
|
|
assert job.state == 'failed'
|
|
assert job.failures == [{'stage':stage,'id':31,'error':'Synthetic failure'}]
|
|
assert job.position == 2 and len(job.items) == 2
|
|
|
|
|
|
@pytest.mark.parametrize('stage', ['grouping', 'analysis', 'events', 'outlook'])
|
|
def test_retry_resumes_only_failed_stage(trading, stage):
|
|
from alphapulse.api.risk import reprocess_retry
|
|
job = N(id=10,state='failed',position=2,settings={},items=[{'id':11},{'id':12}],
|
|
failures=[{'stage':stage,'id':11,'error':'Synthetic failure'}])
|
|
failed = N(id=11,state='failed',quotes={})
|
|
session = session_mock()
|
|
session.get.return_value = job
|
|
session.scalar.return_value = None
|
|
session.scalars.side_effect = [N(all=lambda:[failed] if stage == 'outlook' else []),
|
|
N(all=lambda:[failed] if stage == 'analysis' else []), N(all=lambda:[failed] if stage == 'events' else [])]
|
|
asyncio.run(reprocess_retry(job.id,session))
|
|
assert job.state == 'running' and job.failures == []
|
|
if stage == 'grouping':
|
|
assert job.items == [{'id':11, 'state':'pending'}] and job.position == 0
|
|
else:
|
|
assert job.items == [{'id':11},{'id':12}] and job.position == 2
|
|
if stage == 'outlook': assert failed.quotes['retry_requested'] is True
|
|
else: assert failed.state == 'pending' and failed.error is None
|
|
|
|
|
|
def test_job_outlook_retry_waits_for_its_grouping_retry(trading, monkeypatch):
|
|
from alphapulse.news_risk import reprocess, worker, events
|
|
session = session_mock()
|
|
session.get.return_value = N(enabled=True)
|
|
session.scalar.side_effect = [None, N(quotes={'job_id':10}), N(id=10,position=0,settings={},items=[{'id':11}])]
|
|
monkeypatch.setattr(worker,'SessionLocal',lambda:session)
|
|
monkeypatch.setattr(events,'reading_inputs',AsyncMock(return_value=[]))
|
|
current = AsyncMock(side_effect=AssertionError('Must not use current settings for a job retry'))
|
|
frozen = AsyncMock(side_effect=AssertionError('Must wait for grouping'))
|
|
monkeypatch.setattr(worker,'configuration',current)
|
|
monkeypatch.setattr(reprocess,'frozen_configuration',frozen)
|
|
asyncio.run(worker.outlook())
|
|
current.assert_not_awaited()
|
|
frozen.assert_not_awaited()
|
|
session.add.assert_not_called()
|