|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import datetime as dt |
| 4 | +import hashlib |
4 | 5 | import json |
| 6 | +import subprocess |
5 | 7 | from pathlib import Path |
6 | 8 |
|
7 | 9 | import pytest |
|
25 | 27 | ROOT = Path(__file__).resolve().parents[1] |
26 | 28 |
|
27 | 29 |
|
| 30 | +def write_trusted_example_ai_signal(tmp_path: Path) -> Path: |
| 31 | + repo = tmp_path / "ResearchSignalContextPipelines" |
| 32 | + signal = repo / "data/output/latest_signal.json" |
| 33 | + signal.parent.mkdir(parents=True) |
| 34 | + signal.write_bytes((ROOT / "examples/research_signal_context.example.json").read_bytes()) |
| 35 | + payload = json.loads(signal.read_text(encoding="utf-8")) |
| 36 | + manifest = { |
| 37 | + "manifest_type": "research_signal_context", |
| 38 | + "schema_version": 2, |
| 39 | + "artifact": { |
| 40 | + "path": "data/output/latest_signal.json", |
| 41 | + "sha256": hashlib.sha256(signal.read_bytes()).hexdigest(), |
| 42 | + }, |
| 43 | + "as_of": payload["as_of"], |
| 44 | + "generated_at": payload["generated_at"], |
| 45 | + "expires_at": payload["expires_at"], |
| 46 | + "mode": payload["mode"], |
| 47 | + "producer": { |
| 48 | + "repository": "QuantStrategyLab/ResearchSignalContextPipelines", |
| 49 | + "commit_sha": "a" * 40, |
| 50 | + }, |
| 51 | + "input_digest": f"sha256:{'b' * 64}", |
| 52 | + "policy": {"execution_allowed": False}, |
| 53 | + } |
| 54 | + signal.with_name("latest_signal.manifest.json").write_text(json.dumps(manifest) + "\n", encoding="utf-8") |
| 55 | + subprocess.run(["git", "init", "-q", repo], check=True) |
| 56 | + subprocess.run( |
| 57 | + ["git", "-C", repo, "remote", "add", "origin", "https://github.com/QuantStrategyLab/ResearchSignalContextPipelines.git"], |
| 58 | + check=True, |
| 59 | + ) |
| 60 | + subprocess.run(["git", "-C", repo, "add", "data/output"], check=True) |
| 61 | + subprocess.run( |
| 62 | + [ |
| 63 | + "git", |
| 64 | + "-C", |
| 65 | + repo, |
| 66 | + "-c", |
| 67 | + "user.name=Test", |
| 68 | + "-c", |
| 69 | + "user.email=test@example.invalid", |
| 70 | + "commit", |
| 71 | + "-qm", |
| 72 | + "trusted fixture", |
| 73 | + ], |
| 74 | + check=True, |
| 75 | + ) |
| 76 | + return signal |
| 77 | + |
| 78 | + |
28 | 79 | def test_build_advisory_report_blocks_execution_and_allocation() -> None: |
29 | 80 | report = build_advisory_report( |
30 | 81 | as_of="2026-05-30", |
@@ -61,13 +112,13 @@ def test_low_confidence_events_remain_verify_source_until_verified() -> None: |
61 | 112 | assert by_symbol["EVT1"]["evidence_score"] > by_symbol["EVT4"]["evidence_score"] |
62 | 113 |
|
63 | 114 |
|
64 | | -def test_ai_avoid_bias_defers_research_item() -> None: |
| 115 | +def test_ai_avoid_bias_defers_research_item(tmp_path: Path) -> None: |
65 | 116 | report = build_advisory_report( |
66 | 117 | as_of="2026-05-30", |
67 | 118 | cadence="monthly", |
68 | 119 | political_events_path=ROOT / "examples/political_events.example.csv", |
69 | 120 | political_watchlist_path=ROOT / "examples/political_watchlist.example.csv", |
70 | | - ai_signal_path=ROOT / "examples/research_signal_context.example.json", |
| 121 | + ai_signal_path=write_trusted_example_ai_signal(tmp_path), |
71 | 122 | ) |
72 | 123 |
|
73 | 124 | by_symbol = {item["symbol"]: item for item in report["recommendations"]} |
@@ -138,13 +189,13 @@ def test_mixed_confidence_recommendation_is_not_tier_one(tmp_path: Path) -> None |
138 | 189 | assert rec["recommendation_tier"] == "tier_2" |
139 | 190 |
|
140 | 191 |
|
141 | | -def test_long_horizon_window_is_measured_in_years() -> None: |
| 192 | +def test_long_horizon_window_is_measured_in_years(tmp_path: Path) -> None: |
142 | 193 | report = build_advisory_report( |
143 | 194 | as_of="2026-05-30", |
144 | 195 | cadence="weekly", |
145 | 196 | political_events_path=ROOT / "examples/political_events.example.csv", |
146 | 197 | political_watchlist_path=ROOT / "examples/political_watchlist.example.csv", |
147 | | - ai_signal_path=ROOT / "examples/research_signal_context.example.json", |
| 198 | + ai_signal_path=write_trusted_example_ai_signal(tmp_path), |
148 | 199 | ) |
149 | 200 |
|
150 | 201 | by_symbol = {item["symbol"]: item for item in report["recommendations"]} |
@@ -280,7 +331,7 @@ def test_report_manifest_records_contract_version_and_hashes(tmp_path: Path) -> |
280 | 331 | assert manifest["artifacts"]["markdown"]["sha256"] |
281 | 332 |
|
282 | 333 |
|
283 | | -def test_theme_bias_can_lift_static_watchlist_item_without_direct_symbol_bias(tmp_path: Path) -> None: |
| 334 | +def test_legacy_v1_theme_bias_is_untrusted_no_op(tmp_path: Path) -> None: |
284 | 335 | events_path = tmp_path / "events.csv" |
285 | 336 | events_path.write_text( |
286 | 337 | "event_id,event_date,symbol,event_type,direction,confidence,source_url,notes\n", |
@@ -339,11 +390,12 @@ def test_theme_bias_can_lift_static_watchlist_item_without_direct_symbol_bias(tm |
339 | 390 |
|
340 | 391 | rec = report["recommendations"][0] |
341 | 392 | assert rec["symbol"] == "MU" |
342 | | - assert rec["rating"] == "watch" |
343 | | - assert rec["evidence_score"] > 4 |
344 | | - assert any("主题=hbm_memory" in reason for reason in rec["reasons"]) |
| 393 | + assert rec["rating"] == "monitor" |
| 394 | + assert rec["evidence_score"] == 4 |
| 395 | + assert rec["ai_context"]["source"] == "" |
| 396 | + assert report["summary"]["data_quality_warnings"] == ["ai_signal_provenance_untrusted"] |
345 | 397 | assert report["summary"]["long_context_available"] is False |
346 | | - assert report["summary"]["long_context_missing_reason"] == "current_candidates_do_not_meet_long_context_gate" |
| 398 | + assert report["summary"]["long_context_missing_reason"] == "ai_signal_not_available" |
347 | 399 | assert "MU" not in report["summary"]["long_context_symbols"] |
348 | 400 | assert report["final_decisions"]["horizon_action_buckets"]["long"]["watch"] == [] |
349 | 401 |
|
|
0 commit comments