aps-agent/tests/golden/test_constraint_catalog.py

192 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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