11from __future__ import annotations
22
33import hashlib
4+ import json
5+ from datetime import datetime , timezone
46from pathlib import Path
57
68import pytest
79
8- from runtime_preflight import run_preflight
10+ from runtime_preflight import E3_RECEIPT_SCHEMA_VERSION , run_preflight , validate_e3_receipt
911from scripts .preflight_qmt_runtime import main
1012
1113
@@ -17,6 +19,24 @@ def _sha256(path: str) -> str:
1719 return hashlib .sha256 (Path (path ).read_bytes ()).hexdigest ()
1820
1921
22+ def _valid_e3_receipt () -> dict [str , object ]:
23+ return {
24+ "schema_version" : E3_RECEIPT_SCHEMA_VERSION ,
25+ "as_of" : "2026-09-05T00:00:00+00:00" ,
26+ "freshness_seconds" : 60 ,
27+ "no_order" : True ,
28+ "verify_only" : True ,
29+ "summary_counts" : {
30+ "accounts" : 1 ,
31+ "positions" : 0 ,
32+ "orders" : 0 ,
33+ "fills" : 0 ,
34+ "cash" : 1 ,
35+ "ledger" : 1 ,
36+ },
37+ }
38+
39+
2040def test_preflight_accepts_primary_dry_run_config (monkeypatch , market_history_csv : str ):
2141 monkeypatch .setenv ("STRATEGY_PROFILE" , "cn_industry_etf_rotation" )
2242 monkeypatch .setenv ("QMT_DRY_RUN_ONLY" , "true" )
@@ -136,3 +156,79 @@ def test_preflight_rejects_research_only_dividend_profile(monkeypatch):
136156
137157 assert report .status == "error"
138158 assert _issue_codes (report ) == {"runtime_config_error" }
159+
160+
161+ def test_e3_receipt_validator_accepts_complete_sanitized_receipt ():
162+ report = validate_e3_receipt (
163+ _valid_e3_receipt (),
164+ now = datetime (2026 , 9 , 5 , 0 , 0 , 30 , tzinfo = timezone .utc ),
165+ )
166+
167+ assert report .status == "ok"
168+ assert report .schema_version == E3_RECEIPT_SCHEMA_VERSION
169+ assert report .issues == ()
170+ assert report .to_payload () == {
171+ "status" : "ok" ,
172+ "schema_version" : E3_RECEIPT_SCHEMA_VERSION ,
173+ "required_summary_counts" : ["accounts" , "positions" , "orders" , "fills" , "cash" , "ledger" ],
174+ "issues" : [],
175+ }
176+
177+
178+ @pytest .mark .parametrize (
179+ ("field" , "value" , "expected_code" ),
180+ [
181+ ("summary_counts" , {"accounts" : 1 }, "invalid_summary_counts" ),
182+ ("as_of" , "not-a-timestamp" , "invalid_as_of" ),
183+ ("freshness_seconds" , - 1 , "invalid_freshness" ),
184+ ("no_order" , False , "no_order_required" ),
185+ ("verify_only" , False , "verify_only_required" ),
186+ ],
187+ )
188+ def test_e3_receipt_validator_fails_closed_for_missing_or_unsafe_fields (field , value , expected_code ):
189+ receipt = _valid_e3_receipt ()
190+ receipt [field ] = value
191+
192+ report = validate_e3_receipt (receipt , now = datetime (2026 , 9 , 5 , tzinfo = timezone .utc ))
193+
194+ assert report .status == "error"
195+ assert expected_code in _issue_codes (report )
196+
197+
198+ def test_e3_receipt_validator_fails_closed_for_stale_or_detail_bearing_receipts ():
199+ stale_report = validate_e3_receipt (
200+ _valid_e3_receipt (),
201+ now = datetime (2026 , 9 , 5 , 0 , 6 , tzinfo = timezone .utc ),
202+ )
203+ detail_receipt = _valid_e3_receipt ()
204+ detail_receipt ["account" ] = {"id" : "must-not-be-accepted" }
205+ detail_report = validate_e3_receipt (detail_receipt , now = datetime (2026 , 9 , 5 , tzinfo = timezone .utc ))
206+
207+ assert _issue_codes (stale_report ) == {"stale_receipt" }
208+ assert _issue_codes (detail_report ) == {"unexpected_receipt_fields" }
209+ assert "must-not-be-accepted" not in str (detail_report .to_payload ())
210+
211+
212+ def test_e3_receipt_validator_fails_closed_for_missing_required_fields ():
213+ receipt = _valid_e3_receipt ()
214+ receipt .pop ("verify_only" )
215+
216+ report = validate_e3_receipt (receipt , now = datetime (2026 , 9 , 5 , tzinfo = timezone .utc ))
217+
218+ assert _issue_codes (report ) == {"missing_receipt_fields" , "verify_only_required" }
219+
220+
221+ def test_preflight_cli_validates_e3_receipt_offline (monkeypatch , market_history_csv : str , tmp_path , capsys ):
222+ monkeypatch .setenv ("STRATEGY_PROFILE" , "cn_industry_etf_rotation" )
223+ monkeypatch .setenv ("QMT_DRY_RUN_ONLY" , "true" )
224+ monkeypatch .setenv ("QMT_MARKET_HISTORY_PATH" , market_history_csv )
225+ monkeypatch .delenv ("RUNTIME_TARGET_JSON" , raising = False )
226+ receipt_path = tmp_path / "e3-receipt.json"
227+ receipt = _valid_e3_receipt ()
228+ receipt ["as_of" ] = datetime .now (timezone .utc ).isoformat ()
229+ receipt_path .write_text (json .dumps (receipt ), encoding = "utf-8" )
230+
231+ assert main (["--e3-receipt" , str (receipt_path )]) == 0
232+
233+ payload = json .loads (capsys .readouterr ().out )
234+ assert payload ["e3_receipt" ]["status" ] == "ok"
0 commit comments