192 lines
8.3 KiB
Python
192 lines
8.3 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 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
|