s8-regression-common.sh 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. #!/usr/bin/env bash
  2. # S8-REGRESSION-FIXTURE-1 通用回归工具(库脚本,被 source;不直接执行)。
  3. #
  4. # 提供:
  5. # - DB 连接 / token 解析
  6. # - mysql_run "$sql" → 返回 STDOUT,错误压制到 stderr
  7. # - record_pass / record_fail / record_skip / print_summary
  8. # - read_baseline / assert_baseline_unchanged
  9. # - require_demo_rule "<rule_code>" → 0=存在且 enabled=1,1=不存在/禁用
  10. # - get_rule_field <id> <column>
  11. # - run_once_endpoint
  12. #
  13. # 设计约束(CTO 约束 二/四/九):
  14. # - 不硬编码 baseline 数值;当前 dev=3 但脚本由 read_baseline 实测。
  15. # - 不强制 G01_TEST_*;旧 fixture 缺失 → 调用方记 SKIP。
  16. # - 不修改 demo rule 10/11/12 的最终态;调用方负责快照/恢复。
  17. set -uo pipefail
  18. PROJECT_DIR="${PROJECT_DIR:-/home/yy968/work/New9S/AiDOPWarehouse}"
  19. STORAGE_STATE="${STORAGE_STATE:-${PROJECT_DIR}/Web/tests/e2e/.auth/storage-state.json}"
  20. BACKEND_BASE="${BACKEND_BASE:-http://localhost:5005}"
  21. TENANT_ID="${TENANT_ID:-1}"
  22. FACTORY_ID="${FACTORY_ID:-1}"
  23. DB_HOST="${DB_HOST:-123.60.180.165}"
  24. DB_PORT="${DB_PORT:-3306}"
  25. DB_NAME="${DB_NAME:-aidopdev}"
  26. DB_USER="${DB_USER:-aidopremote}"
  27. DB_PASS="${DB_PASS:-1234567890aiDOP#}"
  28. S8REG_PASS_COUNT=0
  29. S8REG_FAIL_COUNT=0
  30. S8REG_SKIP_COUNT=0
  31. S8REG_NOTES=()
  32. s8reg_die() { echo "FATAL: $*" >&2; exit 2; }
  33. [[ "${DB_NAME}" == "aidopdev" ]] || s8reg_die "DB_NAME must be aidopdev, got ${DB_NAME}"
  34. mysql_run() {
  35. MYSQL_PWD="${DB_PASS}" mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" "${DB_NAME}" \
  36. --default-character-set=utf8mb4 --connect-timeout=8 -N -B -e "$1" 2>/dev/null
  37. }
  38. mysql_run_strict() {
  39. # 与 mysql_run 同,但失败立即 exit;用于必须成功的写入。
  40. MYSQL_PWD="${DB_PASS}" mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" "${DB_NAME}" \
  41. --default-character-set=utf8mb4 --connect-timeout=8 -N -B -e "$1" || \
  42. s8reg_die "mysql_run_strict failed: $1"
  43. }
  44. record_pass() { S8REG_PASS_COUNT=$((S8REG_PASS_COUNT + 1)); echo "PASS: $*"; S8REG_NOTES+=("PASS: $*"); }
  45. record_fail() { S8REG_FAIL_COUNT=$((S8REG_FAIL_COUNT + 1)); echo "FAIL: $*" >&2; S8REG_NOTES+=("FAIL: $*"); }
  46. record_skip() { S8REG_SKIP_COUNT=$((S8REG_SKIP_COUNT + 1)); echo "SKIP: $*"; S8REG_NOTES+=("SKIP: $*"); }
  47. print_summary() {
  48. echo "==== summary ===="
  49. echo "passed=${S8REG_PASS_COUNT}"
  50. echo "failed=${S8REG_FAIL_COUNT}"
  51. echo "skipped=${S8REG_SKIP_COUNT}"
  52. }
  53. read_baseline() {
  54. mysql_run "SELECT COUNT(*) FROM ado_s8_exception_type WHERE tenant_id=0 AND factory_id=0 AND enabled=1;"
  55. }
  56. # 用法: assert_baseline_unchanged "$before"
  57. assert_baseline_unchanged() {
  58. local before="$1"
  59. local after
  60. after=$(read_baseline)
  61. if [[ "${before}" == "${after}" ]]; then
  62. record_pass "baseline unchanged (before=after=${before})"
  63. return 0
  64. else
  65. record_fail "baseline drifted: before=${before} after=${after}"
  66. return 1
  67. fi
  68. }
  69. # 0 = 存在且 enabled=1;1 = 不存在或被禁用。
  70. require_demo_rule() {
  71. local code="$1"
  72. local cnt
  73. cnt=$(mysql_run "SELECT COUNT(*) FROM ado_s8_watch_rule WHERE rule_code='${code}' AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID} AND enabled=1;")
  74. [[ "${cnt}" == "1" ]]
  75. }
  76. get_rule_field() {
  77. local id="$1"; local col="$2"
  78. mysql_run "SELECT IFNULL(${col}, 'NULL') FROM ado_s8_watch_rule WHERE id=${id};"
  79. }
  80. get_rule_id_by_code() {
  81. local code="$1"
  82. mysql_run "SELECT id FROM ado_s8_watch_rule WHERE rule_code='${code}' AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID};"
  83. }
  84. auth_load() {
  85. [[ -f "${STORAGE_STATE}" ]] || s8reg_die "storage state not found: ${STORAGE_STATE}"
  86. AT="$(python3 -c "
  87. import json,sys
  88. d=json.load(open(sys.argv[1]))
  89. for kv in d['origins'][0]['localStorage']:
  90. if kv['name']=='admin.net:access-token':
  91. print(kv['value'].strip('\"')); break
  92. " "${STORAGE_STATE}")"
  93. XAT="$(python3 -c "
  94. import json,sys
  95. d=json.load(open(sys.argv[1]))
  96. for kv in d['origins'][0]['localStorage']:
  97. if kv['name']=='admin.net:x-access-token':
  98. print(kv['value'].strip('\"')); break
  99. " "${STORAGE_STATE}")"
  100. [[ -n "${AT}" && "${AT}" != "null" ]] || s8reg_die "access-token missing in storage-state"
  101. [[ -n "${XAT}" && "${XAT}" != "null" ]] || s8reg_die "x-access-token missing in storage-state"
  102. export AT XAT
  103. }
  104. run_once_endpoint() {
  105. curl -fsS --max-time 30 -X POST \
  106. -H "Authorization: Bearer ${AT}" -H "X-Access-Token: ${XAT}" \
  107. "${BACKEND_BASE}/api/aidop/s8/watch-debug/run-once?tenantId=${TENANT_ID}&factoryId=${FACTORY_ID}"
  108. }
  109. # 子脚本退出码语义:
  110. # 0 = 至少一项 PASS 或纯 SKIP(FAIL=0)
  111. # 1 = 至少一项 FAIL
  112. # 2 = 致命错误(s8reg_die)
  113. exit_by_summary() {
  114. if (( S8REG_FAIL_COUNT > 0 )); then
  115. return 1
  116. fi
  117. return 0
  118. }