feat: add historical news AI backfill and visible task queue
Development CI / check-and-build (push) Successful in 2m44s
GitHub Git mirror / mirror (push) Successful in 6s
Development CI / deploy-development (push) Failing after 7s

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.
This commit is contained in:
eezd committed 2026-11-01 14:47:54 +08:00
1 parent cbd2618a20
commit b45981ec30
25 files changed
+1339 -39

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
"""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_current_packages_require_published_results(trading,published,revisions,expected):
from alphapulse.news_risk.backfill import classification
package=N(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)