r6-detection-log-edge-regression.sh 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. #!/usr/bin/env bash
  2. # R6-2-DETECTION-LOG-EDGE-CASES-1 edge regression script (dev/test only, aidopdev).
  3. #
  4. # Drives detection_log NO_HIT and EVALUATE_FAILED edge cases that the
  5. # baseline three-class regression does not naturally exercise:
  6. # 1. G01_TEST_TIMEOUT_NO_HIT — TIMEOUT rule that returns rows whose
  7. # due_at is in the future, so evaluator succeeds with hits.Count==0;
  8. # 2. G01_TEST_TIMEOUT_FAILED — TIMEOUT rule with params_json missing
  9. # dueAtField, so evaluator throws S8RuleEvaluatorException
  10. # (reason=params_schema_invalid).
  11. #
  12. # Idempotent: rules are upserted with INSERT ... ON DUPLICATE KEY UPDATE
  13. # semantics via SELECT-then-INSERT/UPDATE; rules are left enabled=0 after
  14. # verification (kept in DB for audit replay). Detection_log entries are
  15. # not cleaned. id=34/52/53 must remain unaffected.
  16. set -euo pipefail
  17. PROJECT_DIR="${PROJECT_DIR:-/home/yy968/work/New9S/AiDOPWarehouse}"
  18. STORAGE_STATE="${PROJECT_DIR}/Web/tests/e2e/.auth/storage-state.json"
  19. BACKEND_BASE="${BACKEND_BASE:-http://localhost:5005}"
  20. TENANT_ID="${TENANT_ID:-1}"
  21. FACTORY_ID="${FACTORY_ID:-1}"
  22. DB_HOST="${DB_HOST:-123.60.180.165}"
  23. DB_PORT="${DB_PORT:-3306}"
  24. DB_NAME="${DB_NAME:-aidopdev}"
  25. DB_USER="${DB_USER:-aidopremote}"
  26. DB_PASS="${DB_PASS:-1234567890aiDOP#}"
  27. NO_HIT_RULE='G01_TEST_TIMEOUT_NO_HIT'
  28. FAILED_RULE='G01_TEST_TIMEOUT_FAILED'
  29. NO_HIT_PARAMS='{"dueAtField":"due_at","statusField":"status","completedStates":["CLOSED","DONE","COMPLETED"],"objectCodeField":"related_object_code","objectIdField":"source_object_id","graceMinutes":0,"exceptionTypeCode":"DELIVERY_DELAY"}'
  30. NO_HIT_EXPR="SELECT 'ORDER-NO-HIT-01' AS related_object_code, 'ORDER-NO-HIT-01' AS source_object_id, DATE_ADD(NOW(), INTERVAL 1 HOUR) AS due_at, 'PENDING' AS status"
  31. # FAILED rule: deliberately omit dueAtField so evaluator throws params_schema_invalid.
  32. FAILED_PARAMS='{"statusField":"status","completedStates":["CLOSED","DONE","COMPLETED"],"objectCodeField":"related_object_code","objectIdField":"source_object_id","graceMinutes":0,"exceptionTypeCode":"DELIVERY_DELAY"}'
  33. FAILED_EXPR="SELECT 'ORDER-FAILED-01' AS related_object_code, 'ORDER-FAILED-01' AS source_object_id, DATE_SUB(NOW(), INTERVAL 1 HOUR) AS due_at, 'PENDING' AS status"
  34. fail() { echo "FAIL: $*" >&2; exit 1; }
  35. ok() { echo "OK: $*"; }
  36. [[ "${DB_NAME}" == "aidopdev" ]] || fail "DB_NAME must be aidopdev, got ${DB_NAME}"
  37. [[ -f "${STORAGE_STATE}" ]] || fail "storage state not found: ${STORAGE_STATE}"
  38. AT="$(python3 -c "
  39. import json,sys
  40. d=json.load(open(sys.argv[1]))
  41. for kv in d['origins'][0]['localStorage']:
  42. if kv['name']=='admin.net:access-token':
  43. print(kv['value'].strip('\"')); break
  44. " "${STORAGE_STATE}")"
  45. XAT="$(python3 -c "
  46. import json,sys
  47. d=json.load(open(sys.argv[1]))
  48. for kv in d['origins'][0]['localStorage']:
  49. if kv['name']=='admin.net:x-access-token':
  50. print(kv['value'].strip('\"')); break
  51. " "${STORAGE_STATE}")"
  52. [[ -n "${AT}" && "${AT}" != "null" ]] || fail "access-token missing in storage-state"
  53. [[ -n "${XAT}" && "${XAT}" != "null" ]] || fail "x-access-token missing in storage-state"
  54. mysql_run() {
  55. MYSQL_PWD="${DB_PASS}" mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" "${DB_NAME}" \
  56. --default-character-set=utf8mb4 --connect-timeout=8 -N -B -e "$1" 2>/dev/null
  57. }
  58. run_once() {
  59. curl -fsS --max-time 30 -X POST \
  60. -H "Authorization: Bearer ${AT}" -H "X-Access-Token: ${XAT}" \
  61. "${BACKEND_BASE}/api/aidop/s8/watch-debug/run-once?tenantId=${TENANT_ID}&factoryId=${FACTORY_ID}"
  62. }
  63. # Idempotent upsert by rule_code: ensure enabled=1 with the desired expression+params for this run.
  64. upsert_rule() {
  65. local code="$1"; local expr="$2"; local params="$3"
  66. local exists
  67. exists=$(mysql_run "SELECT COUNT(*) FROM ado_s8_watch_rule WHERE rule_code='${code}' AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID};")
  68. if [[ "${exists}" == "0" ]]; then
  69. mysql_run "INSERT INTO ado_s8_watch_rule
  70. (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)
  71. VALUES (${TENANT_ID}, ${FACTORY_ID}, '${code}', 'S1S7_DELIVERY', 1, 'ORDER', \"${expr}\", 'HIGH', 60, 1, NOW(), 'TIMEOUT', 'ORDER', '${params}');" >/dev/null
  72. ok "upsert_rule INSERT ${code}"
  73. else
  74. mysql_run "UPDATE ado_s8_watch_rule SET expression=\"${expr}\", params_json='${params}', enabled=1, updated_at=NOW() WHERE rule_code='${code}' AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID};" >/dev/null
  75. ok "upsert_rule UPDATE ${code}"
  76. fi
  77. }
  78. baseline=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception_type WHERE tenant_id=0 AND factory_id=0;")
  79. [[ "${baseline}" == "13" ]] || fail "baseline regression: expected 13, got ${baseline}"
  80. ok "baseline = 13"
  81. # Pre-condition: id=34/52/53 must be the only active samples and not recovered.
  82. oor_active=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code='G01_TEST_WATCH' AND status<>'CLOSED' AND is_deleted=0;")
  83. to_active=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code='G01_TEST_TIMEOUT' AND status<>'CLOSED' AND is_deleted=0;")
  84. sh_active=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code='G01_TEST_SHORTAGE' AND status<>'CLOSED' AND is_deleted=0;")
  85. [[ "${oor_active}" == "1" ]] || fail "pre-OUT_OF_RANGE active count expected 1, got ${oor_active}"
  86. [[ "${to_active}" == "1" ]] || fail "pre-TIMEOUT active count expected 1, got ${to_active}"
  87. [[ "${sh_active}" == "1" ]] || fail "pre-SHORTAGE active count expected 1, got ${sh_active}"
  88. ok "pre-active counts: OUT_OF_RANGE=1 / TIMEOUT=1 / SHORTAGE=1"
  89. upsert_rule "${NO_HIT_RULE}" "${NO_HIT_EXPR}" "${NO_HIT_PARAMS}"
  90. upsert_rule "${FAILED_RULE}" "${FAILED_EXPR}" "${FAILED_PARAMS}"
  91. # Marker timestamp so subsequent log queries scope to this run only.
  92. marker=$(mysql_run "SELECT NOW();")
  93. sleep 1
  94. resp="$(run_once)"
  95. count=$(printf '%s' "${resp}" | python3 -c "import json,sys;print(json.load(sys.stdin).get('count',0))")
  96. ok "run-once HTTP 200, count=${count}"
  97. # 1. NO_HIT log present, no exception created.
  98. nohit_logs=$(mysql_run "SELECT COUNT(*) FROM ado_s8_detection_log WHERE rule_code='${NO_HIT_RULE}' AND detect_result='NO_HIT' AND detected_at >= '${marker}';")
  99. [[ "${nohit_logs}" -ge 1 ]] || fail "expected at least 1 NO_HIT log for ${NO_HIT_RULE}, got ${nohit_logs}"
  100. nohit_other=$(mysql_run "SELECT COUNT(*) FROM ado_s8_detection_log WHERE rule_code='${NO_HIT_RULE}' AND detect_result IN ('CREATED','RECOVERED','REFRESHED') AND detected_at >= '${marker}';")
  101. [[ "${nohit_other}" == "0" ]] || fail "${NO_HIT_RULE} should not produce CREATED/RECOVERED/REFRESHED in this run, got ${nohit_other}"
  102. nohit_excs=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code='${NO_HIT_RULE}' AND is_deleted=0;")
  103. [[ "${nohit_excs}" == "0" ]] || fail "${NO_HIT_RULE} must not create exceptions, got ${nohit_excs}"
  104. ok "NO_HIT log present (${nohit_logs}) / no CREATED/REFRESHED/RECOVERED / no exception"
  105. # 2. EVALUATE_FAILED log present with reason+message, no exception created.
  106. failed_logs=$(mysql_run "SELECT COUNT(*) FROM ado_s8_detection_log WHERE rule_code='${FAILED_RULE}' AND detect_result='EVALUATE_FAILED' AND detected_at >= '${marker}';")
  107. [[ "${failed_logs}" -ge 1 ]] || fail "expected at least 1 EVALUATE_FAILED log for ${FAILED_RULE}, got ${failed_logs}"
  108. failed_reason=$(mysql_run "SELECT failure_reason FROM ado_s8_detection_log WHERE rule_code='${FAILED_RULE}' AND detect_result='EVALUATE_FAILED' AND detected_at >= '${marker}' ORDER BY id DESC LIMIT 1;")
  109. [[ -n "${failed_reason}" && "${failed_reason}" != "NULL" ]] || fail "${FAILED_RULE} EVALUATE_FAILED missing failure_reason"
  110. failed_msg=$(mysql_run "SELECT CHAR_LENGTH(IFNULL(failure_message,'')) FROM ado_s8_detection_log WHERE rule_code='${FAILED_RULE}' AND detect_result='EVALUATE_FAILED' AND detected_at >= '${marker}' ORDER BY id DESC LIMIT 1;")
  111. [[ "${failed_msg}" -gt 0 ]] || fail "${FAILED_RULE} EVALUATE_FAILED missing failure_message"
  112. failed_excs=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code='${FAILED_RULE}' AND is_deleted=0;")
  113. [[ "${failed_excs}" == "0" ]] || fail "${FAILED_RULE} must not create exceptions, got ${failed_excs}"
  114. ok "EVALUATE_FAILED log present (${failed_logs}) / failure_reason='${failed_reason}' / message non-empty / no exception"
  115. # 3. Main three-class samples must still be REFRESHED in this run, and untouched (not recovered).
  116. for code in G01_TEST_WATCH G01_TEST_TIMEOUT G01_TEST_SHORTAGE; do
  117. cnt=$(mysql_run "SELECT COUNT(*) FROM ado_s8_detection_log WHERE rule_code='${code}' AND detect_result='REFRESHED' AND detected_at >= '${marker}';")
  118. [[ "${cnt}" -ge 1 ]] || fail "${code}: expected REFRESHED log in this run, got ${cnt}"
  119. done
  120. ok "main three-class samples each produced REFRESHED in this run (not blocked by FAILED)"
  121. main_recovered=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE id IN (34,52,53) AND recovered_at IS NOT NULL;")
  122. [[ "${main_recovered}" == "0" ]] || fail "id=34/52/53 must NOT be marked recovered, got ${main_recovered}"
  123. ok "id=34/52/53 recovered_at remain NULL"
  124. main_status=$(mysql_run "SELECT GROUP_CONCAT(CONCAT(id,':',status) ORDER BY id) FROM ado_s8_exception WHERE id IN (34,52,53);")
  125. [[ "${main_status}" == "34:IN_PROGRESS,52:NEW,53:NEW" ]] || fail "id=34/52/53 status drift: ${main_status}"
  126. ok "id=34/52/53 status unchanged: ${main_status}"
  127. # Cleanup: disable the two edge rules but keep them in DB for audit replay.
  128. mysql_run "UPDATE ado_s8_watch_rule SET enabled=0, updated_at=NOW() WHERE rule_code IN ('${NO_HIT_RULE}','${FAILED_RULE}') AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID};" >/dev/null
  129. ok "disabled edge rules ${NO_HIT_RULE} / ${FAILED_RULE}"
  130. ok "R6 detection_log edge regression PASSED"