"""T07-a 预审结果约束测试:AC-05/06/07/09/12/45。 每条规则都配反例——只测"正常给出 advance"证明不了约束存在。 本模块纯确定性,不调用模型、不算概率。 """ import unittest from services import prescreen_policy as policy def finding(code, result, basis_version=None, **over): item = {"code": code, "title": f"检查项 {code}", "result": result} if basis_version: item["basis_version"] = basis_version item.update(over) return item class HardBlockTests(unittest.TestCase): def test_hard_block_forces_stop(self): result = policy.decide([finding("lease.coverage.below", "issue")], hard_rule_codes=["lease.coverage.below"], critical_check_ids=["lease.coverage.below"]) self.assertEqual(result["recommendation"], "stop") self.assertEqual(result["stage"], "hard_block") def test_hard_block_cites_the_rule_version(self): result = policy.decide([finding("r.hard", "issue", basis_version="rule.credit@v3")], hard_rule_codes=["r.hard"]) self.assertIn("rule.credit@v3", result["recommendation_reason"]) def test_positive_single_judgment_cannot_override_a_hard_block(self): # AC-05 / BR-05:单题正面判断不能覆盖综合意见 judgments = [{"question_id": "q1", "status": "completed", "answer": {"kind": "choice", "value": "supported"}}] result = policy.decide([finding("r.hard", "issue")], hard_rule_codes=["r.hard"], judgments=judgments) self.assertEqual(result["recommendation"], "stop") self.assertFalse(result["judgment_can_override"]) self.assertEqual(result["judgments"], judgments, "判断仍应入档供审计") def test_hard_block_beats_insufficient_information(self): result = policy.decide( [finding("r.hard", "issue"), finding("k1", "unknown")], hard_rule_codes=["r.hard"], critical_check_ids=["k1"]) self.assertEqual(result["recommendation"], "stop") def test_block_does_not_touch_formal_approval(self): # AC-45:stop 只表示暂不建议推进,系统不得自动关闭项目或审批 result = policy.decide([finding("r.hard", "issue")], hard_rule_codes=["r.hard"]) self.assertFalse(result["formal_approval_touched"]) self.assertIn("暂不建议推进", result["recommendation_reason"]) def test_advisory_code_is_not_a_hard_block(self): result = policy.decide([finding("r.advisory", "issue")], hard_rule_codes=["r.hard"]) self.assertNotEqual(result["recommendation"], "stop") class InsufficientInformationTests(unittest.TestCase): def test_incomplete_critical_check_never_becomes_high_risk(self): # AC-07 / BR-07:全部关键资料缺失时给信息不足,**不判高风险** result = policy.decide([finding("k1", "unknown"), finding("k2", "unknown")], critical_check_ids=["k1", "k2"]) self.assertEqual(result["recommendation"], "insufficient_information") # 要求不是"别出现高风险三个字"(免责说明本身就会写到它),而是: # 结论不能变成风险判定,且不得凭空生成"高风险"这类 finding self.assertIn("不得据此判定企业高风险", result["recommendation_reason"]) self.assertNotEqual(result["recommendation"], "stop") # 缺证可以(也应该)出现在待处理列表里,但**不得被改写成风险问题**: # findings 数量与 result 取值必须原样保留,不能凭空多出 issue self.assertEqual(len(result["findings"]), 2) self.assertTrue(all(item["result"] == "unknown" for item in result["findings"])) self.assertFalse(any(item["result"] == "issue" for item in result["open_findings"])) def test_missing_inputs_include_the_incomplete_critical_checks(self): result = policy.decide([finding("k1", "unknown")], critical_check_ids=["k1"], missing_inputs=["现金流材料"]) self.assertEqual(result["missing_inputs"], ["k1", "现金流材料"]) def test_execution_failure_counts_as_incomplete(self): result = policy.decide([finding("k1", "error")], critical_check_ids=["k1"]) self.assertEqual(result["recommendation"], "insufficient_information") def test_non_critical_unknown_does_not_force_insufficient(self): # 非关键项的未知只要求补件,不阻断成信息不足 result = policy.decide([finding("other", "unknown")], critical_check_ids=["k1"]) self.assertEqual(result["recommendation"], "supplement") class SupplementAndAdvanceTests(unittest.TestCase): def test_open_issue_leads_to_supplement(self): result = policy.decide([finding("c1", "issue")]) self.assertEqual(result["recommendation"], "supplement") def test_clean_run_advances_but_does_not_approve(self): result = policy.decide([finding("c1", "pass")]) self.assertEqual(result["recommendation"], "advance") self.assertIn("正式审批仍由业务流程决定", result["recommendation_reason"]) def test_no_findings_at_all_is_treated_as_clean(self): result = policy.decide([]) self.assertEqual(result["stage"], "clean") self.assertEqual(result["open_findings"], []) class DegradationTests(unittest.TestCase): def test_degradations_are_preserved_not_swallowed(self): result = policy.decide([finding("c1", "pass")], degradations=[{"code": "JUDGMENT_UNAVAILABLE", "message": "专项核查未完成"}]) self.assertEqual(len(result["degradations"]), 1) def test_budget_exhaustion_reports_unfinished_not_success(self): # E-08:耗尽即停止并列未完成,不伪造完成 item = policy.budget_exhausted() self.assertEqual(item["code"], "RUN_BUDGET_EXCEEDED") self.assertFalse(item["retryable"]) self.assertIn("未完成项", item["message"]) class ProbabilityGateTests(unittest.TestCase): def test_probability_is_always_null_with_a_reason(self): for findings in ([], [finding("c1", "pass")], [finding("c1", "issue")]): with self.subTest(findings=len(findings)): result = policy.decide(findings) self.assertIsNone(result["probability"]["value"]) self.assertEqual(result["probability"]["reason"], "not_calibrated") def test_probability_takes_no_input_so_a_model_score_cannot_leak_in(self): with self.assertRaises(TypeError): policy.probability(0.93) def test_native_metric_named_probability_is_rejected(self): # BR-12 / AC-18:厂商原生指标不得被当作审批概率 result = policy.decide( [finding("c1", "pass")], judgments=[{"question_id": "q1", "status": "completed", "native_metrics": [{"name": "probability", "value": 0.91}]}]) with self.assertRaises(ValueError) as ctx: policy.assert_no_probability_leak(result) self.assertIn("不得被当作审批概率", str(ctx.exception)) def test_self_reported_confidence_does_not_become_probability(self): result = policy.decide( [finding("c1", "pass")], judgments=[{"question_id": "q1", "status": "completed", "native_metrics": [{"name": "confidence", "value": 0.99, "source": "self_reported"}]}]) policy.assert_no_probability_leak(result) # 可以入档,但不得变概率 self.assertIsNone(result["probability"]["value"]) if __name__ == "__main__": unittest.main()