Add optional events, evidence-backed timelines, versioned package corrections and bounded historical reprocessing. Keep independent packages readable and isolate retries by failed stage. Configure reasoning effort per model stage and refresh default news Skill drafts without publishing.
95 lines
4.8 KiB
Python
95 lines
4.8 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, 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,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,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}] 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,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()
|