Skip to content

Commit c98fb3e

Browse files
Pigbibicodex
andauthored
test: add reconciliation recovery fault injection (#426)
Co-authored-by: Codex <noreply@openai.com>
1 parent f6d8bb0 commit c98fb3e

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

docs/ibkr_reconciliation_baseline_enrollment.zh-CN.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,11 @@ AIAuditBridge 的完整 `reconciliation_baseline` 输出。它会同时核验:
4545
`execution_authority_granted=false``state_write_attempted=false`;它没有 Cloud Run、
4646
券商、执行标记或订单写入代码。下一层单独的最小权限控制器才可消费该计划,并且仍要
4747
在同一目标上比较五项摘要后执行一次精确 CAS。
48+
49+
## 故障注入回归
50+
51+
恢复链路的回归测试会主动注入:控制台把不可执行策略篡改为可执行、五项摘要之一
52+
漂移、人工确认过期、确认与新回执同秒、运行目标不再是 `RECONCILE_ONLY`、以及控制器
53+
令牌被试图发送到普通管理站路径。每一种情况都必须抛出拒绝或返回没有
54+
`transition_plan` 的结果;测试同时断言 `state_write_attempted=false`。这让后续接入
55+
最小权限 CAS 时能持续证明“异常只能保持冻结,不能意外恢复实盘”。
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Fault injection for the non-executable IBKR legacy recovery path.
2+
3+
Each case simulates an untrusted or stale input at a trust boundary. A test
4+
passes only when the verifier rejects it or returns no transition plan; none of
5+
these paths has a state-write or broker-order implementation.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from datetime import datetime, timedelta, timezone
11+
12+
import pytest
13+
14+
from quant_platform_kit.common.broker_reconciliation import (
15+
calculate_broker_reconciliation_evidence_sha256,
16+
)
17+
18+
from tests.test_reconciliation_recovery_verify_only import (
19+
_candidate_payload,
20+
_console_response,
21+
_dual_review,
22+
_evidence,
23+
_runtime_target,
24+
)
25+
from scripts.verify_reconciliation_recovery import (
26+
read_console_confirmation,
27+
verify_reconciliation_recovery,
28+
)
29+
30+
31+
def _inputs(start: datetime) -> tuple[dict[str, object], str]:
32+
candidate = _candidate_payload(start)
33+
candidate_value = candidate["candidate"]
34+
assert isinstance(candidate_value, dict)
35+
return candidate, str(candidate_value["candidate_sha256"])
36+
37+
38+
def test_fault_injection_tampered_console_policy_is_rejected_before_evidence_evaluation() -> None:
39+
start = datetime(2026, 8, 31, 1, 0, tzinfo=timezone.utc)
40+
candidate, candidate_sha256 = _inputs(start)
41+
confirmation = _console_response(candidate_sha256, confirmed_at=start + timedelta(minutes=3))
42+
confirmation["policy"] = {
43+
"no_order": True,
44+
"execution_authority_granted": True,
45+
"controller_must_reverify": True,
46+
}
47+
48+
with pytest.raises(ValueError, match="non-execution policy"):
49+
verify_reconciliation_recovery(
50+
candidate_payload=candidate,
51+
dual_review_payload=_dual_review(candidate_sha256),
52+
confirmation_payload=confirmation,
53+
current_receipt_payload={"evidence": _evidence(observed_at=start + timedelta(minutes=4)).to_dict()},
54+
runtime_target_payload=_runtime_target(),
55+
recovery_id="ibkr-soxl-live-recovery",
56+
now=start + timedelta(minutes=5),
57+
)
58+
59+
60+
def test_fault_injection_current_cash_digest_drift_closes_transition_plan() -> None:
61+
start = datetime(2026, 8, 31, 1, 0, tzinfo=timezone.utc)
62+
candidate, candidate_sha256 = _inputs(start)
63+
current = _evidence(observed_at=start + timedelta(minutes=4)).to_dict()
64+
current["cash_sha256"] = "9" * 64
65+
current["evidence_sha256"] = calculate_broker_reconciliation_evidence_sha256(current)
66+
67+
result = verify_reconciliation_recovery(
68+
candidate_payload=candidate,
69+
dual_review_payload=_dual_review(candidate_sha256),
70+
confirmation_payload=_console_response(candidate_sha256, confirmed_at=start + timedelta(minutes=3)),
71+
current_receipt_payload={"evidence": current},
72+
runtime_target_payload=_runtime_target(),
73+
recovery_id="ibkr-soxl-live-recovery",
74+
now=start + timedelta(minutes=5),
75+
)
76+
77+
assert result["ready_for_atomic_state_transition"] is False
78+
assert result["transition_plan"] is None
79+
assert "broker_reconciliation_cash_mismatch" in result["findings"]
80+
assert result["policy"]["state_write_attempted"] is False # type: ignore[index]
81+
82+
83+
def test_fault_injection_stale_human_confirmation_closes_transition_plan() -> None:
84+
start = datetime(2026, 8, 31, 1, 0, tzinfo=timezone.utc)
85+
candidate, candidate_sha256 = _inputs(start)
86+
87+
result = verify_reconciliation_recovery(
88+
candidate_payload=candidate,
89+
dual_review_payload=_dual_review(candidate_sha256),
90+
confirmation_payload=_console_response(candidate_sha256, confirmed_at=start + timedelta(minutes=3)),
91+
current_receipt_payload={"evidence": _evidence(observed_at=start + timedelta(minutes=40)).to_dict()},
92+
runtime_target_payload=_runtime_target(),
93+
recovery_id="ibkr-soxl-live-recovery",
94+
now=start + timedelta(minutes=40),
95+
)
96+
97+
assert result["ready_for_atomic_state_transition"] is False
98+
assert result["transition_plan"] is None
99+
assert "reconciliation_recovery_confirmation_stale" in result["findings"]
100+
101+
102+
def test_fault_injection_controller_read_url_cannot_be_a_general_console_endpoint() -> None:
103+
with pytest.raises(ValueError, match="controller read endpoint"):
104+
read_console_confirmation(
105+
confirmation_url="https://console.example/api/reconciliation-recovery",
106+
recovery_id="ibkr-soxl-live-recovery",
107+
token="controller-token",
108+
)

0 commit comments

Comments
 (0)