Files
set50-system/backend/tests/test_research.py
2026-08-23 14:58:15 +07:00

163 lines
8.5 KiB
Python

import copy
import json
import tempfile
import unittest
from pathlib import Path
from app.prices import PriceSnapshotStore, PriceSourceError
from app.research import ResearchRunError, ResearchRunStore, run_tourism_research
from app.vintages import VintageStore
def tourism_snapshot(vintage_id: str, published_at: str) -> dict:
return {
"as_of": "2026-01-01",
"data_quality": "provisional",
"theme": "tourism",
"strategy_version": "tourism-v0.2-bot",
"observations": [
{"metric_key": "foreign_arrivals_yoy", "value": 1.0, "expected": 0.0, "scale": 1.0, "period": "2025-12", "history_points": 12, "unit": "percent", "provisional": True}
],
"exposures": [
{"symbol": "AOT", "coefficient": 1.0, "confidence": 1.0, "evidence": "airport"},
{"symbol": "PTT", "coefficient": -0.5, "confidence": 1.0, "evidence": "control"},
],
"source": {
"source_id": "test.bot",
"source_url": "https://example.test/bot",
"vintage_id": vintage_id,
"published_at": published_at,
"retrieved_at": "2026-02-01T00:00:00+00:00",
"release_status": "provisional",
"parser_version": "test-v1",
},
}
def price_snapshot(point_in_time: bool) -> tuple[dict, dict[str, bytes]]:
return (
{
"schema_version": 1,
"source": {
"source_id": "test.prices",
"snapshot_id": "prices-test",
"retrieved_at": "2026-02-01T00:00:00+00:00",
"period_start": "2026-01-01",
"period_end": "2026-01-06",
"quality": "forward_market_archive" if point_in_time else "revised_vendor_history",
"point_in_time": point_in_time,
"adjusted_prices": False,
},
"series": {
"AOT": {"bars": [{"date": "2026-01-01", "close": 100.0}, {"date": "2026-01-02", "close": 102.0}, {"date": "2026-01-05", "close": 104.0}, {"date": "2026-01-06", "close": 106.0}]},
"PTT": {"bars": [{"date": "2026-01-01", "close": 100.0}, {"date": "2026-01-02", "close": 99.0}, {"date": "2026-01-05", "close": 98.0}, {"date": "2026-01-06", "close": 97.0}]},
"SET50": {"bars": [{"date": "2026-01-01", "close": 100.0}, {"date": "2026-01-02", "close": 101.0}, {"date": "2026-01-05", "close": 102.0}, {"date": "2026-01-06", "close": 103.0}]},
},
"benchmark_symbol": "SET50",
},
{"AOT.BK": b"aot", "PTT.BK": b"ptt", "^SET.BK": b"set"},
)
class ResearchRunTests(unittest.TestCase):
def test_price_store_loads_snapshot_and_verifies_raw_payload(self):
with tempfile.TemporaryDirectory() as temp_dir:
store = PriceSnapshotStore(Path(temp_dir))
snapshot, raw = price_snapshot(point_in_time=False)
store.persist(snapshot, raw)
loaded = store.load_snapshot("prices-test")
self.assertEqual(loaded["source"]["snapshot_id"], "prices-test")
self.assertEqual(loaded["source"]["raw_payload_hash"], store.load_manifest()["snapshots"]["prices-test"]["raw_payload_hash"])
def test_price_store_rejects_manifest_metadata_tampering(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
store = PriceSnapshotStore(root)
snapshot, raw = price_snapshot(point_in_time=False)
store.persist(snapshot, raw)
manifest_path = root / "manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["snapshots"]["prices-test"]["point_in_time"] = True
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
with self.assertRaisesRegex(PriceSourceError, "metadata"):
store.load_snapshot("prices-test")
def test_runner_persists_blocked_report_when_vintage_count_is_insufficient(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
vintage_store = VintageStore(root / "tourism")
vintage_store.persist(b"tourism", tourism_snapshot("v1", "2026-01-01T08:00:00+07:00"))
price_store = PriceSnapshotStore(root / "prices")
snapshot, raw = price_snapshot(point_in_time=False)
price_store.persist(snapshot, raw)
run_store = ResearchRunStore(root / "runs")
report = run_tourism_research(vintage_store, price_store, run_store, min_events=2, windows=(1,))
self.assertEqual(report["status"], "blocked")
self.assertEqual(report["reason"], "insufficient_vintages")
self.assertEqual(report["gates"]["vintages"]["available_events"], 1)
self.assertNotIn("result", report)
self.assertEqual(run_store.latest()["run_id"], report["run_id"])
def test_runner_computes_replayable_result_when_both_gates_pass(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
vintage_store = VintageStore(root / "tourism")
for index, published_at in ((1, "2026-01-01T08:00:00+07:00"), (2, "2026-01-02T08:00:00+07:00")):
snapshot = tourism_snapshot(f"v{index}", published_at)
vintage_store.persist(f"tourism-{index}".encode(), snapshot)
price_store = PriceSnapshotStore(root / "prices")
snapshot, raw = price_snapshot(point_in_time=True)
price_store.persist(snapshot, raw)
run_store = ResearchRunStore(root / "runs")
report = run_tourism_research(vintage_store, price_store, run_store, min_events=2, windows=(1,), cost_bps=20)
replay = run_tourism_research(vintage_store, price_store, run_store, min_events=2, windows=(1,), cost_bps=20)
self.assertEqual(report["status"], "ready")
self.assertEqual(report["result"]["event_count"], 2)
self.assertEqual(report["result"]["windows"]["1"]["cost_bps"], 20.0)
self.assertEqual(replay, report)
self.assertEqual(report["inputs"]["price_snapshot"]["point_in_time"], True)
def test_runner_uses_latest_revision_once(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
vintage_store = VintageStore(root / "tourism")
vintage_store.persist(b"initial", tourism_snapshot("v1", "2026-01-01T08:00:00+07:00"))
revised = tourism_snapshot("v1-revised", "2026-01-01T01:00:00Z")
revised["source"]["source_id"] = "test.bot"
vintage_store.persist(b"revised", revised)
vintage_store.persist(b"next", tourism_snapshot("v2", "2026-01-02T08:00:00+07:00"))
price_store = PriceSnapshotStore(root / "prices")
snapshot, raw = price_snapshot(point_in_time=True)
price_store.persist(snapshot, raw)
report = run_tourism_research(vintage_store, price_store, ResearchRunStore(root / "runs"), min_events=2, windows=(1,))
self.assertEqual(report["status"], "ready")
self.assertEqual(len(report["inputs"]["vintages"]), 2)
self.assertIn("v1-revised", {entry["vintage_id"] for entry in report["inputs"]["vintages"]})
def test_runner_does_not_replay_cached_ready_report_after_price_tampering(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
vintage_store = VintageStore(root / "tourism")
for index, published_at in ((1, "2026-01-01T08:00:00+07:00"), (2, "2026-01-02T08:00:00+07:00")):
vintage_store.persist(f"tourism-{index}".encode(), tourism_snapshot(f"v{index}", published_at))
price_store = PriceSnapshotStore(root / "prices")
snapshot, raw = price_snapshot(point_in_time=True)
price_store.persist(snapshot, raw)
run_store = ResearchRunStore(root / "runs")
report = run_tourism_research(vintage_store, price_store, run_store, min_events=2, windows=(1,))
self.assertEqual(report["status"], "ready")
manifest_path = root / "prices" / "manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["snapshots"]["prices-test"]["point_in_time"] = False
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
with self.assertRaisesRegex(ResearchRunError, "integrity"):
run_tourism_research(vintage_store, price_store, run_store, min_events=2, windows=(1,))
if __name__ == "__main__":
unittest.main()