Extract reusable literal keywords once per source revision, assign complete news packages to long-lived events, and research all event members with immutable evidence and references. Add additive research storage, vocabulary snapshots, the two-stage workspace, isolated acceptance coverage, and a paused rollout with an initial fixed 50-news production trial.
64 lines
3.2 KiB
Python
64 lines
3.2 KiB
Python
"""Backfill selection and task progress use isolated model and storage boundaries."""
|
|
import asyncio
|
|
from types import SimpleNamespace as N
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
import pytest
|
|
|
|
|
|
def test_progress_is_scoped_and_removed_on_failure():
|
|
from alphapulse.news_risk.progress import execute, step, active, current
|
|
async def check():
|
|
async def task():
|
|
step('查询资料',tool='read_news')
|
|
assert len(active)==1 and current.get()['tool']=='read_news'
|
|
raise ValueError('Synthetic')
|
|
with pytest.raises(ValueError): await execute(('analysis',7),task)
|
|
assert active=={} and current.get() is None
|
|
asyncio.run(check())
|
|
|
|
|
|
@pytest.mark.parametrize('published,revisions,expected', [(True,(2,2),True),(True,(1,2),False),(False,(2,2),False)])
|
|
def test_all_member_events_require_published_results(trading,published,revisions,expected):
|
|
from alphapulse.news_risk.backfill import classification
|
|
package=N(id=1,state='done',result={'summary':'ok'} if published else None,published_revision=revisions[0],revision=revisions[1])
|
|
session=N(scalar=AsyncMock(return_value=N(id=3,result={'packages':[]})),scalars=AsyncMock(return_value=N(all=lambda:[package])))
|
|
actual,_,_=asyncio.run(classification(session,N(id=1,content_hash='current')))
|
|
assert actual is expected
|
|
|
|
|
|
def test_empty_active_memberships_are_incomplete(trading):
|
|
from alphapulse.news_risk.backfill import classification
|
|
session=N(scalar=AsyncMock(return_value=N(id=3,result={'old':True})),scalars=AsyncMock(return_value=N(all=lambda:[])))
|
|
assert asyncio.run(classification(session,N(id=1,content_hash='current')))[0] is False
|
|
|
|
|
|
def test_expired_preview_does_not_create_work(trading,monkeypatch):
|
|
from alphapulse.news_risk import backfill
|
|
monkeypatch.setattr(backfill.redis,'get',AsyncMock(return_value=None))
|
|
session=MagicMock()
|
|
with pytest.raises(ValueError,match='预览已过期'):asyncio.run(backfill.start(session,'expired'))
|
|
session.add.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize('modes',[[],['missing','missing'],['review','other'],['other']])
|
|
def test_range_rejects_empty_duplicate_or_unknown_modes(trading,modes):
|
|
from pydantic import ValidationError
|
|
from alphapulse.api.risk import ReprocessIn
|
|
with pytest.raises(ValidationError):
|
|
ReprocessIn(start='2026-10-01T00:00:00Z',end='2026-11-01T00:00:00Z',modes=modes)
|
|
|
|
|
|
@pytest.mark.parametrize('failure',[False,True])
|
|
def test_empty_or_invalid_outlook_poll_does_not_admit_a_task(trading,monkeypatch,failure):
|
|
from alphapulse.news_risk import scheduler, worker
|
|
session=MagicMock()
|
|
session.__aenter__=AsyncMock(return_value=session); session.__aexit__=AsyncMock()
|
|
session.get=AsyncMock(return_value=N(enabled=True,start_at='configured'))
|
|
session.scalar=AsyncMock(return_value=None)
|
|
monkeypatch.setattr(scheduler,'SessionLocal',lambda:session)
|
|
monkeypatch.setattr(worker,'enqueue',AsyncMock())
|
|
check=AsyncMock(side_effect=ValueError('Synthetic configuration failure') if failure else None,return_value=False)
|
|
monkeypatch.setattr(worker,'outlook',check)
|
|
assert asyncio.run(scheduler.candidate(set(),0)) is None
|
|
check.assert_awaited_once_with(prepare_only=True)
|