Browse Source

test(s8): cover oor and shortage edge fixtures

YY968XX 1 month ago
parent
commit
dcd79c5989
2 changed files with 221 additions and 0 deletions
  1. 220 0
      scripts/s8/oor-shortage-edge-regression.sh
  2. 1 0
      scripts/s8/rule-evaluator-regression.sh

+ 220 - 0
scripts/s8/oor-shortage-edge-regression.sh

@@ -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

+ 1 - 0
scripts/s8/rule-evaluator-regression.sh

@@ -77,6 +77,7 @@ run_child "r3-oor"       "${SCRIPT_DIR}/r3-out-of-range-regression.sh"
 run_child "r6-edge"      "${SCRIPT_DIR}/r6-detection-log-edge-regression.sh"
 run_child "r6-edge"      "${SCRIPT_DIR}/r6-detection-log-edge-regression.sh"
 run_child "recovered"    "${SCRIPT_DIR}/recovered-edge-regression.sh"
 run_child "recovered"    "${SCRIPT_DIR}/recovered-edge-regression.sh"
 run_child "trigger1-active" "${SCRIPT_DIR}/active-exception-id-trigger1-regression.sh"
 run_child "trigger1-active" "${SCRIPT_DIR}/active-exception-id-trigger1-regression.sh"
+run_child "oor-shortage-edge" "${SCRIPT_DIR}/oor-shortage-edge-regression.sh"
 
 
 baseline_after=$(read_baseline)
 baseline_after=$(read_baseline)