feat(news): restrict research to local sources and process frozen batches
Development CI / check-and-build (push) Successful in 3m23s
GitHub Git mirror / mirror (push) Successful in 6s
Development CI / deploy-development (push) Successful in 5s

This commit is contained in:
eezd committed 2026-11-05 20:43:31 +08:00
1 parent d79a10c2e6
commit 89da4df785
28 files changed
+360 -409

No files matched your search

+12 -49
View File
@@ -18,23 +18,6 @@ def test_private_and_mixed_dns_are_rejected(trading, monkeypatch):
asyncio.run(check())
@pytest.mark.parametrize('discovered', [False, True])
def test_read_web_supports_direct_urls_and_budget(trading, monkeypatch, discovered):
from alphapulse.news_research import tools as module
reader = module.Research()
url = 'https://example.invalid/official'
if discovered:
reader.urls.add(url)
fetch = AsyncMock(return_value={'url': url, 'content': 'Official original'})
monkeypatch.setattr(module, 'fetch_public', fetch)
result = asyncio.run(reader.tool('read_web', {'url': url}))
assert result['discovery_method'] == ('discovered' if discovered else 'direct')
assert result['article_id'] < 0
fetch.assert_awaited_once_with(url)
reader.counts['read_web'] = 4
with pytest.raises(ValueError, match='预算'):
asyncio.run(reader.tool('read_web', {'url': url + '/other'}))
assert fetch.await_count == 1
def test_high_importance_positive_package_is_valid(trading):
@@ -63,17 +46,10 @@ def test_public_fetch_pins_address_and_rechecks_redirect(trading, monkeypatch):
monkeypatch.setattr(research, 'public_address', resolve)
monkeypatch.setattr(httpx, 'AsyncClient', lambda **kwargs: original(transport=httpx.MockTransport(handle)))
with pytest.raises(ValueError, match='非公开'):
asyncio.run(research.fetch_public('https://public.invalid'))
asyncio.run(research.fetch_public_response('https://public.invalid'))
assert len(calls) == 1
def test_failed_review_can_reuse_saved_evidence_without_new_io(trading):
from alphapulse.news_research.tools import Research
research = Research()
record = {'tool': 'read_web', 'arguments': {'url': 'https://example.invalid'}, 'result': {'article_id': -1, 'content': 'Original'}}
research.restore([record])
assert asyncio.run(research.tool('read_web', record['arguments']))['content'] == 'Original'
assert not research.records and research.counts['read_web'] == 0
def test_restored_news_is_sent_before_reusing_its_reference(trading, monkeypatch):
@@ -170,29 +146,16 @@ def test_provided_original_is_reused_without_io(trading, monkeypatch):
assert review.counts['read'] == 0 and not review.records
def test_web_unavailable_short_circuits_only_this_review(trading, monkeypatch):
from alphapulse.news_research import tools as research
session = AsyncMock()
session.__aenter__.return_value = session
monkeypatch.setattr(research, 'SessionLocal', lambda: session)
client = AsyncMock()
client.__aenter__.return_value = client
response = SimpleNamespace(raise_for_status=lambda: None,
json=lambda: {'results': [], 'unresponsive_engines': [['engine', 'CAPTCHA']]})
client.get.return_value = response
monkeypatch.setattr(research.httpx, 'AsyncClient', lambda **kwargs: client)
review = research.Research()
assert asyncio.run(review.tool('search_web', {'query': 'first query'}))['status'] == 'unavailable'
assert asyncio.run(review.tool('search_web', {'query': 'different query'}))['status'] == 'unavailable'
assert client.get.await_count == 1 and review.counts['search_web'] == 1
assert research.Research().web_unavailable is None
def test_direct_web_read_preserves_public_reader_rejection(trading, monkeypatch):
from alphapulse.news_research import tools as module
reader = module.Research()
fetch = AsyncMock(side_effect=ValueError('不允许访问非公开地址'))
monkeypatch.setattr(module, 'fetch_public', fetch)
with pytest.raises(ValueError, match='非公开'):
asyncio.run(reader.tool('read_web', {'url': 'http://127.0.0.1'}))
assert not reader.sources
@pytest.mark.parametrize('tool,args', [('search_web', {'query': 'test'}), ('read_web', {'url': 'https://example.invalid'})])
def test_external_tools_are_removed_and_cannot_restore_sources(trading, tool, args):
from alphapulse.news_research.service import Research
reader = Research(event={'id': 1})
assert tool not in [spec.name for spec in reader.tool_specs()]
reader.restore([{'tool': tool, 'arguments': args, 'result': {'article_id': -1, 'content': 'Old external body'}}])
assert not reader.records and not reader.sources and not reader.cache
with pytest.raises(ValueError, match='未知'):
asyncio.run(reader.tool(tool, args))
@@ -239,43 +239,6 @@ def test_retry_refreshes_local_search_with_current_vocabulary(
assert len(reader.records) == 1 and not reader.records[0].get("reused")
@pytest.mark.parametrize("status", ["unavailable", "no_matches", "matched"])
def test_retry_refreshes_unavailable_search_but_reuses_successful_results(
trading, monkeypatch, status
):
import asyncio
from unittest.mock import AsyncMock
from alphapulse.news_research.service import Research
from alphapulse.news_research import tools as tools
session = AsyncMock()
session.__aenter__.return_value = session
monkeypatch.setattr(tools, "SessionLocal", lambda: session)
client = AsyncMock()
client.__aenter__.return_value = client
client.get.return_value = N(
raise_for_status=lambda: None,
json=lambda: {"results": [], "unresponsive_engines": []},
)
monkeypatch.setattr(tools.httpx, "AsyncClient", lambda **kwargs: client)
cached = {
"status": status,
"results": [{"url": "https://example.invalid/retry"}]
if status == "matched"
else [],
"errors": [["engine", "CAPTCHA"]] if status == "unavailable" else [],
}
reader = Research(event={"id": 8})
reader.restore(
[{"tool": "search_web", "arguments": {"query": "same query"}, "result": cached}]
)
result = asyncio.run(reader.tool("search_web", {"query": "same query"}))
if status == "unavailable":
assert client.get.await_count == 1 and result["status"] == "no_matches"
assert len(reader.records) == 1 and not reader.records[0].get("reused")
else:
assert client.get.await_count == 0 and result == cached
assert reader.records[0]["reused"] is True
def test_settings_contract_rejects_third_stage_and_unknown_efforts(trading):
-25
View File
@@ -131,33 +131,8 @@ def test_checkpoint_is_atomic_private_and_can_be_replaced(scenario, tmp_path):
assert not path.with_suffix('.pending').exists()
def test_official_evidence_requires_success_correct_event_and_cited_claim(scenario):
tables = {m.__tablename__: [] for m in scenario.MODELS}
value = {'external_news_ids': [1], 'official_sources': [{'url': 'https://official.invalid', 'anchors': ['Official fact']}]}
tables['news_research_packages'] = [{'id': 10, 'news_id': 1}]
tables['news_research_memberships'] = [{'package_id': 10, 'event_id': 2}]
version = {'id': 3, 'event_id': 2, 'state': 'done', 'evidence': [
{'tool': 'read_web', 'result': {'url': 'https://official.invalid/article', 'content': 'Official fact was confirmed.', 'article_id': -1}}],
'output': {'claims': [{'verdict': 'supported', 'evidence_refs': [{'article_id': -1, 'quote': 'Official fact was confirmed.'}]}]}}
tables['news_research_versions'] = [version]
assert scenario.official_assertions(value, tables, [2])[0]['passed']
for field, invalid in [('state', 'failed'), ('event_id', 99), ('output', {'claims': []})]:
old = version[field]
version[field] = invalid
assert not scenario.official_assertions(value, tables, [2])[0]['passed']
version[field] = old
def test_official_hints_are_scoped_to_event_members(scenario):
value = {'external_news_ids': [11, 22], 'official_sources': [
{'url': 'https://one.invalid/incident'}, {'url': 'https://two.invalid/licence'}]}
hints = scenario.official_hints(value, {11, 33})
assert 'https://one.invalid/incident' in hints and 'https://two.invalid/licence' not in hints
hints = scenario.official_hints(value, {22})
assert 'https://two.invalid/licence' in hints and 'https://one.invalid/incident' not in hints
value['official_sources'].pop()
with pytest.raises(ValueError):
scenario.official_hints(value, {11})
def test_reference_api_assertion_checks_changed_and_original_version(scenario, monkeypatch):