|
|
@@ -0,0 +1,220 @@
|
|
|
+#!/usr/bin/env bash
|
|
|
+# S8-EDGE-FIXTURE-OOR-SHORTAGE-1:OUT_OF_RANGE / SHORTAGE 边缘路径回归(dev/test only, aidopdev)。
|
|
|
+#
|
|
|
+# 与 r6-detection-log-edge-regression.sh(TIMEOUT NO_HIT/EVALUATE_FAILED)同形,覆盖:
|
|
|
+# - TEMP_SCHED_OOR_NO_HIT : OUT_OF_RANGE evaluator OK + 表达式 measured ∈ [lower, upper] → 0 hits → NO_HIT
|
|
|
+# - TEMP_SCHED_OOR_EVALUATE_FAILED : OUT_OF_RANGE params 缺 measuredValueField → params_schema_invalid → EVALUATE_FAILED
|
|
|
+# - TEMP_SCHED_SHORTAGE_NO_HIT : SHORTAGE evaluator OK + 表达式 actual >= target → 0 hits → NO_HIT
|
|
|
+# - TEMP_SCHED_SHORTAGE_EVALUATE_FAILED : SHORTAGE params 缺 targetQtyField → params_schema_invalid → EVALUATE_FAILED
|
|
|
+#
|
|
|
+# 触发:watch_rule.next_run_at=NOW → Job tick 路径(RunDispatchTickAsync→ProcessSingleRuleAsync),
|
|
|
+# 与 r6 一致;Job 路径写 detection_log + 更新 last_status / last_error / consecutive_failure_count。
|
|
|
+#
|
|
|
+# 严守:
|
|
|
+# - 不影响 demo rule 10/11/12
|
|
|
+# - TEMP rule 测后 enabled=0;TEMP exception soft delete
|
|
|
+# - 不清 detection_log / rule_detection_state / 演示数据
|
|
|
+# - 不动 lock 三件套(任何残留 lock 由本脚本 setup 时清空)
|
|
|
+
|
|
|
+set -uo pipefail
|
|
|
+
|
|
|
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
+# shellcheck source=./s8-regression-common.sh
|
|
|
+source "${SCRIPT_DIR}/s8-regression-common.sh"
|
|
|
+
|
|
|
+auth_load
|
|
|
+
|
|
|
+# OUT_OF_RANGE
|
|
|
+OOR_NH_RULE='TEMP_SCHED_OOR_NO_HIT'
|
|
|
+OOR_FL_RULE='TEMP_SCHED_OOR_EVALUATE_FAILED'
|
|
|
+# SHORTAGE
|
|
|
+SH_NH_RULE='TEMP_SCHED_SHORTAGE_NO_HIT'
|
|
|
+SH_FL_RULE='TEMP_SCHED_SHORTAGE_EVALUATE_FAILED'
|
|
|
+
|
|
|
+# OOR NO_HIT:合法 params + 表达式 measured=50 ∈ [0,100] → evaluator 0 hits
|
|
|
+OOR_NH_PARAMS='{"measuredValueField":"measured","lowerBound":0,"upperBound":100,"toleranceAbs":0,"toleranceRatio":0,"objectCodeField":"related_object_code","objectIdField":"source_object_id","exceptionTypeCode":"DELIVERY_DELAY"}'
|
|
|
+OOR_NH_EXPR="SELECT 'TEMP-OOR-NOHIT-001' AS related_object_code, 'TEMP-OOR-NOHIT-001' AS source_object_id, 50 AS measured"
|
|
|
+
|
|
|
+# OOR EVALUATE_FAILED:缺 measuredValueField → params_schema_invalid
|
|
|
+OOR_FL_PARAMS='{"lowerBound":0,"upperBound":100,"objectCodeField":"related_object_code","exceptionTypeCode":"DELIVERY_DELAY"}'
|
|
|
+OOR_FL_EXPR="SELECT 'TEMP-OOR-FAILED-001' AS related_object_code, 'TEMP-OOR-FAILED-001' AS source_object_id, 999 AS measured"
|
|
|
+
|
|
|
+# SHORTAGE NO_HIT:合法 params + actual=100 >= target=100 → 0 hits
|
|
|
+SH_NH_PARAMS='{"targetQtyField":"target","actualQtyField":"actual","toleranceAbs":0,"toleranceRatio":0,"objectCodeField":"related_object_code","objectIdField":"source_object_id","exceptionTypeCode":"DELIVERY_DELAY"}'
|
|
|
+SH_NH_EXPR="SELECT 'TEMP-SHORTAGE-NOHIT-001' AS related_object_code, 'TEMP-SHORTAGE-NOHIT-001' AS source_object_id, 100 AS target, 100 AS actual"
|
|
|
+
|
|
|
+# SHORTAGE EVALUATE_FAILED:缺 targetQtyField → params_schema_invalid
|
|
|
+SH_FL_PARAMS='{"actualQtyField":"actual","objectCodeField":"related_object_code","objectIdField":"source_object_id","exceptionTypeCode":"DELIVERY_DELAY"}'
|
|
|
+SH_FL_EXPR="SELECT 'TEMP-SHORTAGE-FAILED-001' AS related_object_code, 'TEMP-SHORTAGE-FAILED-001' AS source_object_id, 50 AS actual"
|
|
|
+
|
|
|
+baseline_before=$(read_baseline)
|
|
|
+echo "==== oor-shortage-edge-regression baseline_before=${baseline_before} ===="
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# Upsert TEMP rule(直接 DB 写,绕过 service-level params 校验,让非法 params 进入 evaluator)
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+upsert_temp_rule() {
|
|
|
+ local code="$1" rtype="$2" expr="$3" params="$4"
|
|
|
+ local exists
|
|
|
+ exists=$(mysql_run "SELECT COUNT(*) FROM ado_s8_watch_rule WHERE rule_code='${code}' AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID};")
|
|
|
+ if [[ "${exists}" == "0" ]]; then
|
|
|
+ mysql_run_strict "INSERT INTO ado_s8_watch_rule
|
|
|
+ (tenant_id, factory_id, rule_code, scene_code, data_source_id, watch_object_type, expression, severity,
|
|
|
+ poll_interval_seconds, enabled, created_at, rule_type, source_object_type, params_json,
|
|
|
+ consecutive_failure_count, trigger_count_required, recover_count_required, next_run_at)
|
|
|
+ VALUES (${TENANT_ID}, ${FACTORY_ID}, '${code}', 'S1S7_DELIVERY', 1, 'ORDER',
|
|
|
+ \"${expr}\", 'HIGH', 60, 1, NOW(), '${rtype}', 'ORDER', '${params}',
|
|
|
+ 0, 1, 1, NOW());" >/dev/null
|
|
|
+ echo " inserted ${code} (rule_type=${rtype})"
|
|
|
+ else
|
|
|
+ mysql_run_strict "UPDATE ado_s8_watch_rule SET enabled=1, rule_type='${rtype}', expression=\"${expr}\", params_json='${params}',
|
|
|
+ trigger_count_required=1, recover_count_required=1,
|
|
|
+ paused_until=NULL, pause_reason=NULL,
|
|
|
+ lock_token=NULL, locked_by=NULL, lock_until=NULL, running_started_at=NULL,
|
|
|
+ consecutive_failure_count=0, last_status=NULL, last_error=NULL,
|
|
|
+ next_run_at=NOW(), updated_at=NOW()
|
|
|
+ WHERE rule_code='${code}' AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID};" >/dev/null
|
|
|
+ echo " re-armed ${code} (rule_type=${rtype})"
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+echo "---- Setup TEMP rules ----"
|
|
|
+upsert_temp_rule "${OOR_NH_RULE}" "OUT_OF_RANGE" "${OOR_NH_EXPR}" "${OOR_NH_PARAMS}"
|
|
|
+upsert_temp_rule "${OOR_FL_RULE}" "OUT_OF_RANGE" "${OOR_FL_EXPR}" "${OOR_FL_PARAMS}"
|
|
|
+upsert_temp_rule "${SH_NH_RULE}" "SHORTAGE" "${SH_NH_EXPR}" "${SH_NH_PARAMS}"
|
|
|
+upsert_temp_rule "${SH_FL_RULE}" "SHORTAGE" "${SH_FL_EXPR}" "${SH_FL_PARAMS}"
|
|
|
+
|
|
|
+OOR_NH_ID=$(get_rule_id_by_code "${OOR_NH_RULE}")
|
|
|
+OOR_FL_ID=$(get_rule_id_by_code "${OOR_FL_RULE}")
|
|
|
+SH_NH_ID=$(get_rule_id_by_code "${SH_NH_RULE}")
|
|
|
+SH_FL_ID=$(get_rule_id_by_code "${SH_FL_RULE}")
|
|
|
+echo " OOR_NH_ID=${OOR_NH_ID} OOR_FL_ID=${OOR_FL_ID} SH_NH_ID=${SH_NH_ID} SH_FL_ID=${SH_FL_ID}"
|
|
|
+
|
|
|
+# 清理可能残留的 detection_state / TEMP exception
|
|
|
+mysql_run_strict "DELETE FROM ado_s8_rule_detection_state WHERE rule_code IN ('${OOR_NH_RULE}','${OOR_FL_RULE}','${SH_NH_RULE}','${SH_FL_RULE}') AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID};" >/dev/null
|
|
|
+mysql_run_strict "UPDATE ado_s8_exception SET is_deleted=1, updated_at=NOW() WHERE source_rule_code IN ('${OOR_NH_RULE}','${OOR_FL_RULE}','${SH_NH_RULE}','${SH_FL_RULE}') AND is_deleted=0;" >/dev/null
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# Wait Job tick:4 条 TEMP rule 的 last_run_at 全部跨过 marker
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+echo "---- Wait Job tick (poll up to 90s for all 4 TEMP rules) ----"
|
|
|
+marker=$(mysql_run "SELECT NOW();")
|
|
|
+sleep 1
|
|
|
+echo " marker=${marker}"
|
|
|
+
|
|
|
+# 重新触发:next_run_at=NOW(防止 Job 已抢锁释放后未及时再排程)
|
|
|
+mysql_run_strict "UPDATE ado_s8_watch_rule SET next_run_at=NOW(), lock_token=NULL, locked_by=NULL, lock_until=NULL WHERE id IN (${OOR_NH_ID}, ${OOR_FL_ID}, ${SH_NH_ID}, ${SH_FL_ID});" >/dev/null
|
|
|
+
|
|
|
+picked_up=0
|
|
|
+for ((i=1; i<=18; i++)); do
|
|
|
+ sleep 5
|
|
|
+ oor_nh_ts=$(get_rule_field "${OOR_NH_ID}" last_run_at)
|
|
|
+ oor_fl_ts=$(get_rule_field "${OOR_FL_ID}" last_run_at)
|
|
|
+ sh_nh_ts=$(get_rule_field "${SH_NH_ID}" last_run_at)
|
|
|
+ sh_fl_ts=$(get_rule_field "${SH_FL_ID}" last_run_at)
|
|
|
+ if [[ "${oor_nh_ts}" != "NULL" && "${oor_fl_ts}" != "NULL" && "${sh_nh_ts}" != "NULL" && "${sh_fl_ts}" != "NULL" \
|
|
|
+ && "${oor_nh_ts}" > "${marker}" && "${oor_fl_ts}" > "${marker}" \
|
|
|
+ && "${sh_nh_ts}" > "${marker}" && "${sh_fl_ts}" > "${marker}" ]]; then
|
|
|
+ picked_up=1
|
|
|
+ echo " iter ${i}: all 4 TEMP rules picked up"
|
|
|
+ echo " OOR_NH last_run_at=${oor_nh_ts} OOR_FL last_run_at=${oor_fl_ts}"
|
|
|
+ echo " SH_NH last_run_at=${sh_nh_ts} SH_FL last_run_at=${sh_fl_ts}"
|
|
|
+ break
|
|
|
+ fi
|
|
|
+ echo " iter ${i}: waiting…"
|
|
|
+done
|
|
|
+
|
|
|
+if (( picked_up == 0 )); then
|
|
|
+ record_skip "Job did not pick up all 4 TEMP rules within 90s — backend cron likely paused or saturated"
|
|
|
+fi
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# Verify NO_HIT (OOR + SHORTAGE)
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+verify_no_hit() {
|
|
|
+ local label="$1" rule="$2" rid="$3"
|
|
|
+ local nh_logs other_logs excs status
|
|
|
+ if (( picked_up == 0 )); then
|
|
|
+ record_skip "${label}: NO_HIT — Job tick timeout"
|
|
|
+ return
|
|
|
+ fi
|
|
|
+ nh_logs=$(mysql_run "SELECT COUNT(*) FROM ado_s8_detection_log WHERE rule_code='${rule}' AND detect_result='NO_HIT' AND detected_at >= '${marker}';")
|
|
|
+ other_logs=$(mysql_run "SELECT COUNT(*) FROM ado_s8_detection_log WHERE rule_code='${rule}' AND detect_result IN ('CREATED','REFRESHED','EVALUATE_FAILED') AND detected_at >= '${marker}';")
|
|
|
+ excs=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code='${rule}' AND is_deleted=0;")
|
|
|
+ status=$(get_rule_field "${rid}" last_status)
|
|
|
+ echo " ${label}: NO_HIT_logs=${nh_logs} other_logs=${other_logs} active_excs=${excs} last_status=${status}"
|
|
|
+
|
|
|
+ [[ "${nh_logs}" -ge 1 ]] && record_pass "${label}: NO_HIT detection_log present (${nh_logs})" || record_fail "${label}: expected NO_HIT log >=1, got ${nh_logs}"
|
|
|
+ [[ "${other_logs}" == "0" ]] && record_pass "${label}: no CREATED/REFRESHED/FAILED" || record_fail "${label}: unexpected ${other_logs} non-NO_HIT log"
|
|
|
+ [[ "${excs}" == "0" ]] && record_pass "${label}: 0 active exception" || record_fail "${label}: created ${excs} exception"
|
|
|
+ [[ "${status}" == "SUCCESS" ]] && record_pass "${label}: last_status=SUCCESS" || record_fail "${label}: last_status expected SUCCESS, got ${status}"
|
|
|
+}
|
|
|
+
|
|
|
+verify_evaluate_failed() {
|
|
|
+ local label="$1" rule="$2" rid="$3"
|
|
|
+ local fl_logs reason msg_len excs status err failcount
|
|
|
+ if (( picked_up == 0 )); then
|
|
|
+ record_skip "${label}: EVALUATE_FAILED — Job tick timeout"
|
|
|
+ return
|
|
|
+ fi
|
|
|
+ fl_logs=$(mysql_run "SELECT COUNT(*) FROM ado_s8_detection_log WHERE rule_code='${rule}' AND detect_result='EVALUATE_FAILED' AND detected_at >= '${marker}';")
|
|
|
+ reason=$(mysql_run "SELECT failure_reason FROM ado_s8_detection_log WHERE rule_code='${rule}' AND detect_result='EVALUATE_FAILED' AND detected_at >= '${marker}' ORDER BY id DESC LIMIT 1;")
|
|
|
+ msg_len=$(mysql_run "SELECT CHAR_LENGTH(IFNULL(failure_message,'')) FROM ado_s8_detection_log WHERE rule_code='${rule}' AND detect_result='EVALUATE_FAILED' AND detected_at >= '${marker}' ORDER BY id DESC LIMIT 1;")
|
|
|
+ excs=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code='${rule}' AND is_deleted=0;")
|
|
|
+ status=$(get_rule_field "${rid}" last_status)
|
|
|
+ err=$(get_rule_field "${rid}" last_error)
|
|
|
+ failcount=$(get_rule_field "${rid}" consecutive_failure_count)
|
|
|
+ echo " ${label}: EVALUATE_FAILED_logs=${fl_logs} reason='${reason}' msg_len=${msg_len} active_excs=${excs}"
|
|
|
+ echo " last_status=${status} consecutive_failure_count=${failcount}"
|
|
|
+
|
|
|
+ [[ "${fl_logs}" -ge 1 ]] && record_pass "${label}: EVALUATE_FAILED detection_log present (${fl_logs})" || record_fail "${label}: expected EVALUATE_FAILED log >=1, got ${fl_logs}"
|
|
|
+ [[ -n "${reason}" && "${reason}" != "NULL" ]] && record_pass "${label}: failure_reason='${reason}'" || record_fail "${label}: failure_reason missing"
|
|
|
+ [[ "${msg_len}" -gt 0 ]] && record_pass "${label}: failure_message non-empty (${msg_len} chars)" || record_fail "${label}: failure_message empty"
|
|
|
+ [[ "${excs}" == "0" ]] && record_pass "${label}: 0 active exception" || record_fail "${label}: created ${excs} exception"
|
|
|
+ [[ "${status}" == "FAILED" ]] && record_pass "${label}: last_status=FAILED" || record_fail "${label}: last_status expected FAILED, got ${status}"
|
|
|
+ [[ "${err}" != "NULL" && -n "${err}" ]] && record_pass "${label}: last_error populated" || record_fail "${label}: last_error missing"
|
|
|
+ [[ "${failcount}" -ge 1 ]] && record_pass "${label}: consecutive_failure_count=${failcount}" || record_fail "${label}: consecutive_failure_count not bumped (${failcount})"
|
|
|
+}
|
|
|
+
|
|
|
+echo "---- A. OUT_OF_RANGE NO_HIT ----"
|
|
|
+verify_no_hit "A.OOR_NH" "${OOR_NH_RULE}" "${OOR_NH_ID}"
|
|
|
+
|
|
|
+echo "---- B. OUT_OF_RANGE EVALUATE_FAILED ----"
|
|
|
+verify_evaluate_failed "B.OOR_FL" "${OOR_FL_RULE}" "${OOR_FL_ID}"
|
|
|
+
|
|
|
+echo "---- C. SHORTAGE NO_HIT ----"
|
|
|
+verify_no_hit "C.SH_NH" "${SH_NH_RULE}" "${SH_NH_ID}"
|
|
|
+
|
|
|
+echo "---- D. SHORTAGE EVALUATE_FAILED ----"
|
|
|
+verify_evaluate_failed "D.SH_FL" "${SH_FL_RULE}" "${SH_FL_ID}"
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# Cleanup
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+echo "---- Cleanup TEMP rules ----"
|
|
|
+mysql_run_strict "UPDATE ado_s8_watch_rule SET enabled=0, paused_until=NULL, pause_reason=NULL,
|
|
|
+ lock_token=NULL, locked_by=NULL, lock_until=NULL, running_started_at=NULL, updated_at=NOW()
|
|
|
+ WHERE rule_code IN ('${OOR_NH_RULE}','${OOR_FL_RULE}','${SH_NH_RULE}','${SH_FL_RULE}')
|
|
|
+ AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID};" >/dev/null
|
|
|
+TEMP_EXC_IDS=$(mysql_run "SELECT IFNULL(GROUP_CONCAT(id),'none') FROM ado_s8_exception WHERE source_rule_code IN ('${OOR_NH_RULE}','${OOR_FL_RULE}','${SH_NH_RULE}','${SH_FL_RULE}') AND is_deleted=0;")
|
|
|
+mysql_run_strict "UPDATE ado_s8_exception SET is_deleted=1, updated_at=NOW() WHERE source_rule_code IN ('${OOR_NH_RULE}','${OOR_FL_RULE}','${SH_NH_RULE}','${SH_FL_RULE}') AND is_deleted=0;" >/dev/null
|
|
|
+echo " disabled 4 TEMP rules; soft-deleted exception ids=[${TEMP_EXC_IDS}]"
|
|
|
+
|
|
|
+for rid in "${OOR_NH_ID}" "${OOR_FL_ID}" "${SH_NH_ID}" "${SH_FL_ID}"; do
|
|
|
+ e=$(get_rule_field "${rid}" enabled)
|
|
|
+ [[ "${e}" == "0" ]] && record_pass "cleanup: rule_id=${rid} enabled=0" || record_fail "cleanup: rule_id=${rid} still enabled=${e}"
|
|
|
+done
|
|
|
+
|
|
|
+temp_visible=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code IN ('${OOR_NH_RULE}','${OOR_FL_RULE}','${SH_NH_RULE}','${SH_FL_RULE}') AND is_deleted=0;")
|
|
|
+[[ "${temp_visible}" == "0" ]] && record_pass "cleanup: TEMP exceptions not in default list" || record_fail "cleanup: ${temp_visible} TEMP exception(s) still visible"
|
|
|
+
|
|
|
+# demo rule + baseline 守恒
|
|
|
+demo_state=$(mysql_run "SELECT GROUP_CONCAT(CONCAT(id,':',enabled,':',IFNULL(paused_until,'NULL'),':',trigger_count_required,':',recover_count_required) ORDER BY id) FROM ado_s8_watch_rule WHERE id IN (10,11,12);")
|
|
|
+expected="10:1:NULL:1:1,11:1:NULL:1:1,12:1:NULL:1:1"
|
|
|
+[[ "${demo_state}" == "${expected}" ]] && record_pass "demo rule 10/11/12 conserved" || record_fail "demo rule drift: ${demo_state}"
|
|
|
+
|
|
|
+assert_baseline_unchanged "${baseline_before}"
|
|
|
+
|
|
|
+print_summary
|
|
|
+echo "TEMP rules: OOR_NH(id=${OOR_NH_ID}) OOR_FL(id=${OOR_FL_ID}) SH_NH(id=${SH_NH_ID}) SH_FL(id=${SH_FL_ID}); TEMP exception ids=[${TEMP_EXC_IDS}]"
|
|
|
+exit_by_summary
|