@@ -1077,6 +1077,95 @@ def test_unmandated_fallback_rejects_stale_snapshot(self) -> None:
10771077 self .assertIn ("stale_portfolio_snapshot" , result .assessment .reason_codes )
10781078 self .assertEqual (result .decision .positions , ())
10791079
1080+ def test_evidence_assessment_requires_mandate (self ) -> None :
1081+ with patch ("quant_platform_kit.risk.gate._utc_now" , return_value = self ._NOW ):
1082+ result = assess_with_evidence (
1083+ _decision (positions = (PositionTarget (symbol = "BTCUSDT" , target_weight = 0.10 ),)),
1084+ self ._snapshot (),
1085+ scope = "MEMBER" ,
1086+ mandate_provenance = None ,
1087+ market_data = {},
1088+ candidate_identity = None ,
1089+ )
1090+
1091+ self .assertEqual (result .assessment .outcome , "REJECT" )
1092+ self .assertIn ("missing_mandate" , result .assessment .reason_codes )
1093+ self .assertFalse (result .assessment .execution_authorized )
1094+ self .assertEqual (result .decision .positions , ())
1095+
1096+ def test_evidence_assessment_rejects_unbounded_target_and_unknown_symbol (self ) -> None :
1097+ cases = (
1098+ (
1099+ "10x_target" ,
1100+ _decision (positions = (PositionTarget (symbol = "BTCUSDT" , target_weight = 10.0 ),)),
1101+ "effective_exposure_cap" ,
1102+ ),
1103+ (
1104+ "unknown_symbol" ,
1105+ _decision (positions = (PositionTarget (symbol = "UNKNOWN" , target_weight = 0.10 ),)),
1106+ "asset_not_authorized" ,
1107+ ),
1108+ )
1109+ for name , decision , reason_code in cases :
1110+ with (
1111+ self .subTest (name = name ),
1112+ patch ("quant_platform_kit.risk.gate._utc_now" , return_value = self ._NOW ),
1113+ ):
1114+ result = assess_with_evidence (
1115+ decision ,
1116+ self ._snapshot (),
1117+ scope = "MEMBER" ,
1118+ mandate_provenance = self ._mandate (),
1119+ market_data = {},
1120+ candidate_identity = self ._candidate (),
1121+ )
1122+
1123+ self .assertEqual (result .assessment .outcome , "REJECT" )
1124+ self .assertIn (reason_code , result .assessment .reason_codes )
1125+ self .assertFalse (result .assessment .execution_authorized )
1126+ self .assertEqual (result .decision .positions , ())
1127+
1128+ def test_evidence_assessment_rejects_expired_mandate (self ) -> None :
1129+ with patch ("quant_platform_kit.risk.gate._utc_now" , return_value = self ._NOW ):
1130+ result = assess_with_evidence (
1131+ _decision (positions = (PositionTarget (symbol = "BTCUSDT" , target_weight = 0.10 ),)),
1132+ self ._snapshot (),
1133+ scope = "MEMBER" ,
1134+ mandate_provenance = {
1135+ ** self ._mandate (),
1136+ "expires_at" : "2026-08-04T04:27:59Z" ,
1137+ },
1138+ market_data = {},
1139+ candidate_identity = self ._candidate (),
1140+ )
1141+
1142+ self .assertEqual (result .assessment .outcome , "REJECT" )
1143+ self .assertIn ("expired_mandate" , result .assessment .reason_codes )
1144+ self .assertFalse (result .assessment .execution_authorized )
1145+
1146+ def test_evidence_assessment_rejects_stale_capital_snapshot (self ) -> None :
1147+ with patch ("quant_platform_kit.risk.gate._utc_now" , return_value = self ._NOW ):
1148+ result = assess_with_evidence (
1149+ _decision (positions = (PositionTarget (symbol = "BTCUSDT" , target_value = 10_000.0 ),)),
1150+ self ._snapshot (),
1151+ scope = "MEMBER" ,
1152+ mandate_provenance = self ._mandate (),
1153+ market_data = {},
1154+ candidate_identity = self ._candidate (),
1155+ capital_base = _capital_base (
1156+ as_of = self ._NOW - timedelta (seconds = 301 ),
1157+ strategy_scope = "crypto_live_pool_rotation" ,
1158+ ),
1159+ capital_base_binding = _capital_base_binding (
1160+ strategy_scope = "crypto_live_pool_rotation" ,
1161+ ),
1162+ )
1163+
1164+ self .assertEqual (result .assessment .outcome , "REJECT" )
1165+ self .assertIn ("stale_capital_base" , result .assessment .reason_codes )
1166+ self .assertFalse (result .assessment .execution_authorized )
1167+ self .assertEqual (result .decision .positions , ())
1168+
10801169 def test_risk_plugin_exception_rejects_without_exposing_exception (self ) -> None :
10811170 class CrashingPlugin :
10821171 plugin_name = "crashing_plugin"
@@ -1377,8 +1466,16 @@ def test_optional_string_nulls_and_nfc_text_remain_canonical(self) -> None:
13771466 ),
13781467 ),
13791468 )
1380- first , first_engine = self ._assess (decision )
1381- second , second_engine = self ._assess (decision )
1469+ first , first_engine = self ._assess (
1470+ decision ,
1471+ mandate = self ._mandate (),
1472+ candidate = self ._candidate (),
1473+ )
1474+ second , second_engine = self ._assess (
1475+ decision ,
1476+ mandate = self ._mandate (),
1477+ candidate = self ._candidate (),
1478+ )
13821479
13831480 self .assertEqual (first .assessment .outcome , "APPROVE" )
13841481 self .assertEqual (first .assessment .assessment_sha256 , second .assessment .assessment_sha256 )
@@ -1568,14 +1665,20 @@ def test_timezone_and_whole_second_timestamp_compatibility(self) -> None:
15681665 first , _engine = self ._assess (
15691666 StrategyDecision (),
15701667 snapshot = {** self ._SNAPSHOT , "as_of" : utc .replace (microsecond = 1 )},
1668+ mandate = self ._mandate (),
1669+ candidate = self ._candidate (),
15711670 )
15721671 second , _engine = self ._assess (
15731672 StrategyDecision (),
15741673 snapshot = {** self ._SNAPSHOT , "as_of" : offset .replace (microsecond = 999_999 )},
1674+ mandate = self ._mandate (),
1675+ candidate = self ._candidate (),
15751676 )
15761677 rejected , _engine = self ._assess (
15771678 StrategyDecision (),
15781679 snapshot = {** self ._SNAPSHOT , "as_of" : "2026-08-04T12:27:55+08:00" },
1680+ mandate = self ._mandate (),
1681+ candidate = self ._candidate (),
15791682 )
15801683
15811684 self .assertEqual (first .assessment .outcome , "APPROVE" )
0 commit comments