157 lines
5.2 KiB
Python
157 lines
5.2 KiB
Python
"""Vocabulary and fixed-version behavior, isolated from production I/O."""
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace as N
|
|
from unittest.mock import AsyncMock
|
|
import pytest
|
|
|
|
|
|
def test_equivalence_preserves_literal_aliases_and_quotes(trading):
|
|
from alphapulse.news_research.vocabulary import normalize, expand, VocabularyInput
|
|
|
|
data = VocabularyInput(
|
|
base_revision=0, entries=[{"canonical": "币安", "aliases": ["Binance"]}]
|
|
)
|
|
snapshot = {"revision": 1, "entries": [e.model_dump() for e in data.entries]}
|
|
original = [
|
|
{
|
|
"canonical": "Binance交易所",
|
|
"aliases": ["Binance"],
|
|
"quote": "Binance reported",
|
|
}
|
|
]
|
|
assert normalize(original, snapshot) == [{**original[0], "canonical": "币安"}]
|
|
assert original[0]["canonical"] == "Binance交易所"
|
|
assert expand([" BINANCE "], snapshot) == ["币安", "binance"]
|
|
assert expand(["Bank of Canada"], snapshot) == ["bank of canada"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"entries",
|
|
[
|
|
[{"canonical": " ", "aliases": []}],
|
|
[{"canonical": "币安", "aliases": ["Binance", "BINANCE"]}],
|
|
[
|
|
{"canonical": "币安", "aliases": ["Binance"]},
|
|
{"canonical": "别家", "aliases": ["binance"]},
|
|
],
|
|
[{"canonical": "币安", "aliases": []}, {"canonical": "币安", "aliases": []}],
|
|
],
|
|
)
|
|
def test_reject_ambiguous_or_empty_vocabulary(trading, entries):
|
|
from alphapulse.news_research.vocabulary import VocabularyInput
|
|
|
|
with pytest.raises(ValueError):
|
|
VocabularyInput(base_revision=0, entries=entries)
|
|
|
|
|
|
def test_merging_tags_never_invents_literal_alias(trading):
|
|
from alphapulse.news_research.vocabulary import normalize
|
|
|
|
snapshot = {"entries": [{"canonical": "币安", "aliases": ["Binance"]}]}
|
|
result = normalize(
|
|
[
|
|
{"canonical": c, "aliases": [a], "quote": a}
|
|
for c, a in [("币安", "币安"), ("Binance", "Binance")]
|
|
],
|
|
snapshot,
|
|
)
|
|
assert len(result) == 1 and result[0]["aliases"] == ["币安", "Binance"]
|
|
|
|
|
|
def test_stale_vocabulary_save_conflicts_without_writing(trading, monkeypatch):
|
|
from fastapi import HTTPException
|
|
from alphapulse.news_research import api
|
|
|
|
monkeypatch.setattr(
|
|
api.vocab, "snapshot", AsyncMock(return_value={"revision": 2, "entries": []})
|
|
)
|
|
session = N(execute=AsyncMock(), add=AsyncMock(), commit=AsyncMock())
|
|
with pytest.raises(HTTPException) as error:
|
|
asyncio.run(
|
|
api.save_vocabulary(
|
|
api.vocab.VocabularyInput(base_revision=1, entries=[]), session
|
|
)
|
|
)
|
|
assert error.value.status_code == 409
|
|
session.add.assert_not_called()
|
|
session.commit.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_id,state", [(2, "done"), (1, "failed"), (1, "superseded")]
|
|
)
|
|
def test_fixed_version_rejects_wrong_event_and_unsuccessful_versions(
|
|
trading, event_id, state
|
|
):
|
|
from fastapi import HTTPException
|
|
from alphapulse.news_research.api import event_version
|
|
|
|
session = N(get=AsyncMock(return_value=N(event_id=event_id, state=state)))
|
|
with pytest.raises(HTTPException) as error:
|
|
asyncio.run(event_version(1, 9, session))
|
|
assert error.value.status_code == 404
|
|
|
|
|
|
def test_acceptance_budget_is_shared_and_counts_failed_streams(trading, tmp_path):
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
spec = importlib.util.spec_from_file_location(
|
|
"acceptance_budget",
|
|
Path(__file__).resolve().parents[2] / "scripts/accept_news_research.py",
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
counter = tmp_path / "requests"
|
|
first = module.Budget(counter, limit=2)
|
|
second = module.Budget(counter, limit=2)
|
|
first.take()
|
|
second.take()
|
|
with pytest.raises(module.BudgetExceeded):
|
|
first.take()
|
|
assert counter.read_text() == "2"
|
|
|
|
|
|
def test_budget_counts_transport_failures_before_retry(trading, tmp_path):
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
spec = importlib.util.spec_from_file_location(
|
|
"failed_stream_budget",
|
|
Path(__file__).resolve().parents[2] / "scripts/accept_news_research.py",
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
budget = module.Budget(tmp_path / "counter", limit=2)
|
|
sent = []
|
|
|
|
async def failing():
|
|
sent.append(True)
|
|
raise ConnectionError("isolated failure")
|
|
yield
|
|
|
|
async def attempt():
|
|
async for _ in budget.wrap(failing)():
|
|
pass
|
|
|
|
async def run():
|
|
return await asyncio.gather(
|
|
*(attempt() for _ in range(3)), return_exceptions=True
|
|
)
|
|
|
|
errors = asyncio.run(run())
|
|
assert len(sent) == 2
|
|
assert sum(isinstance(e, module.BudgetExceeded) for e in errors) == 1
|
|
|
|
|
|
def test_supplemental_citation_must_use_returned_article_id(trading):
|
|
from alphapulse.news_research.pipeline import validate_evidence, Evidence
|
|
|
|
sources = {-1: {"news_id": 1004, "content": "Original correction"}}
|
|
validate_evidence([Evidence(article_id=-1, quote="Original correction")], sources)
|
|
with pytest.raises(ValueError):
|
|
validate_evidence(
|
|
[Evidence(article_id=1004, quote="Original correction")], sources
|
|
)
|