# ============================================================ # matrix-113(Q 方向):13 类约束目录齐全 + 关闭硬约束被拒并审计 # 黄金测试(可重生 ✅) # 1) constraint_catalog() 只读接口:13 类 code C1..C13 齐全 # 2) 每类硬/软 + 可关闭性 + 黄金用例标识 # 3) 关闭硬约束(enabled=False / 降级为软 / 软权重归零)→ 拒绝 + 审计 # 4) LLM(SOP→约束)与确认/应用路径走同一门禁 # ============================================================ from __future__ import annotations import pytest from server.aps_domain.constraints import ( CONFLICT_TO_CONSTRAINT, DEFAULT_CONSTRAINT_DEFS, ConstraintProfileDenied, apply_profile_save, confirmation_for_profile_save, constraint_catalog, engine_constraint_flags, get_constraint, ) from server.aps_domain.sop_rules import apply_sop_pack from server.state.seed import seed_world ALL_CODES = {f"C{i}" for i in range(1, 14)} PLAN_HARD = {"C1", "C2", "C3", "C4", "C5", "C7", "C11", "C12"} # plan §9.2a.1 硬 PLAN_SOFT = {"C6", "C8", "C9", "C10", "C13"} # plan §9.2a.1 软 HARD_CONFIGURABLE_IDS = ["C4_maintenance", "C7_capacity", "C11_freeze", "C12_team", "C12_tooling"] HARD_BUILTIN_IDS = ["C1_precedence", "C2_no_overlap", "C3_calendar", "C5_capability"] def _last_audit(world) -> dict: assert world["auditEvents"], "应有审计事件" return world["auditEvents"][-1] # ---------------- 1) 13 类目录齐全 ---------------- def test_catalog_has_exactly_13_plan_classes(): world = seed_world() cat = constraint_catalog(world) assert cat["catalogVersion"] == "9.2a.1" assert cat["total"] == 13 codes = {c["code"] for c in cat["classes"]} assert codes == ALL_CODES for cls in cat["classes"]: assert cls["name"], "每类必须有名称" assert cls["kind"] in ("hard", "soft") assert isinstance(cls["closable"], bool) assert cls["items"], "每类必须挂到具体约束定义" def test_catalog_kind_and_closability_match_plan_9_2a(): world = seed_world() by_code = {c["code"]: c for c in constraint_catalog(world)["classes"]} for code in PLAN_HARD: assert by_code[code]["kind"] == "hard" assert by_code[code]["closable"] is False, f"{code} 硬约束不可关闭" for code in PLAN_SOFT: assert by_code[code]["kind"] == "soft" assert by_code[code]["closable"] is True, f"{code} 软约束可关闭" # C12 聚合班组/工装两个子资源(plan:班组/工装等累积资源) assert by_code["C12"]["items"] == ["C12_team", "C12_tooling"] summary = constraint_catalog(world)["summary"] assert summary["hardCount"] == 8 and summary["softCount"] == 5 assert summary["closableCount"] == 5 def test_catalog_golden_case_identifier_per_class(): world = seed_world() golden = [c["goldenCase"] for c in constraint_catalog(world)["classes"]] assert all(g and g.startswith("golden-") for g in golden), "每类必须有黄金用例标识" assert len(set(golden)) == len(golden), "黄金用例标识必须唯一" def test_default_defs_cover_all_13_plan_classes(): # 13 类 = 14 条定义(C12 班组/工装拆为两个子资源),code 恰好 C1..C13 ids = [d["id"] for d in DEFAULT_CONSTRAINT_DEFS] assert len(ids) == len(set(ids)) == 14 codes = {d["code"] for d in DEFAULT_CONSTRAINT_DEFS} assert codes == ALL_CODES, "目录外 code(如 C12b)不允许,C1..C13 必须齐全" assert all(d.get("goldenCase") for d in DEFAULT_CONSTRAINT_DEFS) assert any(d["id"] == "C9_customer_priority" for d in DEFAULT_CONSTRAINT_DEFS), "C9 客户优先级/VIP 缺失" # 冲突类型 → 约束映射引用的 id 都必须存在于目录内 for cid in set(CONFLICT_TO_CONSTRAINT.values()): assert cid in ids, f"CONFLICT_TO_CONSTRAINT 引用了目录外约束 {cid}" def test_catalog_reflects_profile_overrides(): world = seed_world() apply_profile_save(world, {"constraints": {"C6_material_kit": {"kind": "hard"}}}) by_code = {c["code"]: c for c in constraint_catalog(world)["classes"]} assert by_code["C6"]["kind"] == "hard" assert by_code["C6"]["closable"] is False # ---------------- 3) 关闭硬约束 → 拒绝 + 审计 ---------------- @pytest.mark.parametrize("cid", HARD_CONFIGURABLE_IDS) def test_close_hard_constraint_rejected_and_audited(cid): world = seed_world() with pytest.raises(ConstraintProfileDenied) as ei: apply_profile_save(world, {"constraints": {cid: {"enabled": False}}}) msg = str(ei.value) assert "硬约束" in msg and "拒绝" in msg, "必须给出中文拒绝说明" ev = _last_audit(world) assert ev["action"] == "constraint.profile.save" assert ev["result"] == "DENIED" assert ev["rationale"]["constraintId"] == cid assert ev["rationale"]["reason"] assert ev["power"] == "P2" # 世界未被篡改:硬约束仍然启用 assert get_constraint(world, cid)["enabled"] is True @pytest.mark.parametrize("cid", HARD_BUILTIN_IDS) def test_close_builtin_hard_constraint_rejected_and_audited(cid): world = seed_world() with pytest.raises(ConstraintProfileDenied): apply_profile_save(world, {"constraints": {cid: {"enabled": False}}}) ev = _last_audit(world) assert ev["action"] == "constraint.profile.save" assert ev["result"] == "DENIED" assert ev["rationale"]["constraintId"] == cid def test_hard_constraint_downgrade_to_soft_rejected(): world = seed_world() with pytest.raises(ConstraintProfileDenied): apply_profile_save(world, {"constraints": {"C4_maintenance": {"kind": "soft"}}}) assert _last_audit(world)["result"] == "DENIED" assert get_constraint(world, "C4_maintenance")["kind"] == "hard" def test_soft_weight_cannot_close_hard_constraint(): world = seed_world() with pytest.raises(ConstraintProfileDenied): apply_profile_save(world, {"constraints": {"C7_capacity": {"weight": 0}}}) ev = _last_audit(world) assert ev["result"] == "DENIED" assert "软权重" in ev["rationale"]["reason"] # 引擎开关不受影响:硬约束仍生效 assert engine_constraint_flags(world)["capacity"] is True assert get_constraint(world, "C7_capacity")["enabled"] is True # 同包组合(kind=soft + enabled=False)同样无法关闭硬约束 with pytest.raises(ConstraintProfileDenied): apply_profile_save(world, {"constraints": {"C7_capacity": {"kind": "soft", "enabled": False}}}) def test_confirmation_for_profile_save_also_denies_and_audits(): world = seed_world() with pytest.raises(ConstraintProfileDenied): confirmation_for_profile_save(world, {"constraints": {"C11_freeze": {"enabled": False}}}) ev = _last_audit(world) assert ev["action"] == "constraint.profile.save" assert ev["result"] == "DENIED" assert ev["rationale"]["constraintId"] == "C11_freeze" def test_llm_sop_apply_goes_through_same_gate(): """LLM 提案(SOP→约束)也走同一门禁:关闭硬约束被拒并审计。""" world = seed_world() pack = { "packId": "pack-hard-close", "title": "关闭硬约束的恶意SOP", "source": {"assetId": "SOP-HARD-CLOSE", "title": "测试SOP"}, "effects": [{"op": "constraint.set", "id": "C4_maintenance", "enabled": False}], } with pytest.raises(ConstraintProfileDenied): apply_sop_pack(world, pack) ev = _last_audit(world) assert ev["action"] == "constraint.profile.save" assert ev["result"] == "DENIED" assert ev["rationale"]["constraintId"] == "C4_maintenance" # ---------------- 软约束仍可关闭(不误伤) ---------------- def test_soft_constraints_remain_closable(): world = seed_world() apply_profile_save(world, {"constraints": {"C6_material_kit": {"enabled": False}}}) assert get_constraint(world, "C6_material_kit")["enabled"] is False apply_profile_save(world, {"constraints": {"C8_due_date": {"weight": 0}}}) assert get_constraint(world, "C8_due_date")["weight"] == 0 def test_reset_defaults_allowed(): world = seed_world() apply_profile_save(world, {"constraints": {"C6_material_kit": {"kind": "hard"}}}) apply_profile_save(world, {"resetDefaults": True}) c6 = get_constraint(world, "C6_material_kit") assert c6["kind"] == "soft" and c6["enabled"] is True