feat(news): restrict research to local sources and process frozen batches
This commit is contained in:
1 parent
d79a10c2e6
commit
89da4df785
28 files changed
+360
-409
No files matched your search
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user