Merge PR #115 (feat #52 报警看板配置化:阈值/规则外置 + 布局/渲染整合)

This commit is contained in:
2026-08-05 08:26:40 +08:00
parent 8a407fc0ad
commit 52d986e6f0
6 changed files with 1219 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
"""报警看板配置化验收脚本(issue #52,PRD 5.5 验收口径)。
验证四个能力点(覆盖 PRD 5.5「切换模板零改码」对告警面板的覆盖):
1. **Ti 行业模板配置资产合法**:``templates/ti-cl4/dashboard/alarm_panel.ti.yaml``
所描述的配置(等价 dict)通过 ``iAOP-cockpit-alarm-panel-v1`` 校验,
并能渲染成 ``alarm_panel`` 组件 props(证明告警面板已配置化);
2. **树脂模板同样可配置**:换一份配置资产(不同配色 / 规则源)即得到不同 props,
前端代码零改动(PRD 5.5 验收口径「切换模板零改码」);
3. **配置点真实生效**:``mute_lower`` 改变 → ``filter_alarms_by_mute`` 过滤结果
改变;``require_ack`` 关闭 → props 里不再出现 ``ackTimeoutS``;
4. **负例**:一份故意破坏的配置(缺 P0 配色 / 非法 severity / 缺 rulesSource)
被正确拒绝并聚合多条错误(对齐 #50 校验器风格)。
用法(在 core/cockpit 目录下):
python scripts/verify_alarm_config.py
退出码:0 = 全部通过;1 = 存在未达标项。
说明:Ti 模板资产本身是 YAML;本脚本不依赖 PyYAML(避免引入运行时依赖),
而是用与该 YAML 文件内容等价的 dict 进行校验——字段语义完全一致,
若安装了 PyYAML,可直接解析原文件复现(见文末注释)。
"""
from __future__ import annotations
import os
import sys
# 本脚本位于 core/cockpit/scripts/,需要把 core/ 加入 sys.path,
# 才能 `from cockpit import ...`(cockpit 包位于 core/cockpit)
sys.path.insert(
0,
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
)
from cockpit import ( # noqa: E402
ALARM_CONFIG_SCHEMA_ID,
filter_alarms_by_mute,
load_alarm_config,
render_alarm_panel_props,
validate_alarm_config,
)
def _ti_config() -> dict:
"""与 templates/ti-cl4/dashboard/alarm_panel.ti.yaml 等价的 dict。"""
return {
"$schema": ALARM_CONFIG_SCHEMA_ID,
"severityColors": [
{"severity": "P0", "fg": "#ff3b30", "bg": "rgba(255,59,48,0.12)",
"icon": "alert-octagon", "border": "#ff3b30"},
{"severity": "P1", "fg": "#f5a623", "bg": "rgba(245,166,35,0.12)",
"icon": "alert-triangle", "border": "#f5a623"},
{"severity": "P2", "fg": "#3aa0ff", "bg": "rgba(58,160,255,0.10)",
"icon": "info"},
],
"rulesSource": {
"kind": "asset",
"ref": "ti-cl4/impurity-forecast/config/alert_rules.template.yaml",
},
"thresholdsSource": {
"kind": "asset",
"ref": "ti-cl4/impurity-forecast/config/alert_rules.template.yaml",
},
"showSop": True,
"requireAck": True,
"ackTimeoutS": 300,
"muteLower": "P2",
"groupBy": "severity",
"sortBy": "severity",
"maxItems": 50,
}
def _resin_config() -> dict:
"""树脂行业模板的等价配置(不同配色 + 内联规则,证明换模板零改码)。"""
return {
"severityColors": [
{"severity": "P0", "fg": "#e63329", "bg": "rgba(230,51,41,0.12)"},
{"severity": "P1", "fg": "#ffb000", "bg": "rgba(255,176,0,0.12)"},
{"severity": "P2", "fg": "#2bb673", "bg": "rgba(43,182,115,0.10)"},
],
"rulesSource": {
"kind": "inline",
"data": [
{"id": "resin-temp-high", "severity": "P0"},
{"id": "resin-crosslink-low", "severity": "P1"},
],
},
"showSop": False,
"requireAck": False,
"muteLower": "P1", # 树脂模板只看 P0/P1,静默 P2 提示
}
def _check(name: str, fn) -> bool:
try:
ok = fn()
except Exception as exc: # noqa: BLE001
print(f"[FAIL] {name}: 抛异常 {exc!r}")
return False
status = "PASS" if ok else "FAIL"
print(f"[{status}] {name}")
return bool(ok)
def main() -> int:
print("== 报警看板配置化验收(issue #52 / PRD 5.5)==")
all_ok = True
# 1) Ti 模板资产合法 + 可渲染为 props
def check_ti_valid() -> bool:
res = validate_alarm_config(_ti_config())
if not res.ok:
for e in res.errors:
print(f" - {e}")
return False
cfg = load_alarm_config(_ti_config())
props = render_alarm_panel_props(cfg)
# P0 红色告警配色进入 props;requireAck=True 携带 ackTimeoutS
return (
props["severityStyles"]["P0"]["fg"] == "#ff3b30"
and props["requireAck"] is True
and props.get("ackTimeoutS") == 300
and props["rulesSource"]["ref"].endswith("alert_rules.template.yaml")
)
all_ok &= _check("Ti 模板配置合法 + 渲染 props 正确", check_ti_valid)
# 2) 树脂模板换配置 → props 不同,前端代码零改动
def check_resin_differs() -> bool:
cfg = load_alarm_config(_resin_config())
props = render_alarm_panel_props(cfg)
# 树脂 P0 用不同红色;内联规则透传 data;requireAck 关闭不带 ackTimeoutS
return (
props["severityStyles"]["P0"]["fg"] == "#e63329"
and props["rulesSource"]["kind"] == "inline"
and len(props["rulesSource"]["data"]) == 2
and "ackTimeoutS" not in props
and props["showSop"] is False
)
all_ok &= _check("树脂模板换配置 → props 随之变化(切换模板零改码)", check_resin_differs)
# 3) mute_lower 配置点真实驱动展示策略
def check_mute_lower() -> bool:
alarms = [
{"severity": "P2", "id": "a"},
{"severity": "P0", "id": "b"},
{"severity": "P1", "id": "c"},
]
keep_all = filter_alarms_by_mute(alarms, "P2")
only_p0_p1 = filter_alarms_by_mute(alarms, "P1")
# mute=P2 全保留且按严重度降序;mute=P1 过滤掉 P2
return (
[a["id"] for a in keep_all] == ["b", "c", "a"]
and [a["id"] for a in only_p0_p1] == ["b", "c"]
)
all_ok &= _check("muteLower 配置点真实过滤 + 排序告警", check_mute_lower)
# 4) 负例:破坏的配置被拒绝并聚合多条错误
def check_negative() -> bool:
bad = {
"severityColors": [
{"severity": "P0", "fg": "#f00", "bg": "#000"},
# 缺 P1 / P2
{"severity": "P0", "fg": "#f00", "bg": "#000"}, # 重复 P0
{"severity": "P9", "fg": "#f00", "bg": "#000"}, # 非法 severity
],
"rulesSource": {"kind": "asset"}, # 缺 ref
"ackTimeoutS": 0, # 非正整数
"sortBy": "color", # 非法排序键
"maxItems": 0, # 越界
}
res = validate_alarm_config(bad)
# 应收集到多条错误(聚合而非首条即返)
return (not res.ok) and len(res.errors) >= 5
all_ok &= _check("负例:破坏的配置被拒绝并聚合多条错误", check_negative)
print("=" * 48)
print("结果: " + ("全部通过 ✅" if all_ok else "存在未达标项 ❌"))
return 0 if all_ok else 1
if __name__ == "__main__":
raise SystemExit(main())