aps-agent/tests/golden/test_constraint_catalog.py

192 lines
8.3 KiB
Python
Raw Normal View History

# ============================================================
# 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