| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- #!/usr/bin/env bash
- # S8-REGRESSION-FIXTURE-1 通用回归工具(库脚本,被 source;不直接执行)。
- #
- # 提供:
- # - DB 连接 / token 解析
- # - mysql_run "$sql" → 返回 STDOUT,错误压制到 stderr
- # - record_pass / record_fail / record_skip / print_summary
- # - read_baseline / assert_baseline_unchanged
- # - require_demo_rule "<rule_code>" → 0=存在且 enabled=1,1=不存在/禁用
- # - get_rule_field <id> <column>
- # - run_once_endpoint
- # - snapshot_demo_rule_state / assert_demo_rule_state_unchanged
- # 回归"漂移检测"模型:捕获初态 → 末态比对,不再硬编码 enabled=1。
- # - arm_demo_rules_for_regression / restore_demo_rules_disabled_after_regression
- # 配合 CTO 决策"demo rule 默认关闭":driver 入场前 arm,收尾恢复 disabled。
- # - cleanup_temp_sched_approval_ghost_tasks
- # teardown 后清除 TEMP_SCHED_% 软删异常遗留的 Pending ApprovalFlowTask。
- #
- # 设计约束(CTO 约束 二/四/九):
- # - 不硬编码 baseline 数值;当前 dev=3 但脚本由 read_baseline 实测。
- # - 不强制 G01_TEST_*;旧 fixture 缺失 → 调用方记 SKIP。
- # - 不修改 demo rule 10/11/12 的最终态:driver 自责 arm/restore;child 走 snapshot 漂移检测。
- set -uo pipefail
- PROJECT_DIR="${PROJECT_DIR:-/home/yy968/work/New9S/AiDOPWarehouse}"
- STORAGE_STATE="${STORAGE_STATE:-${PROJECT_DIR}/Web/tests/e2e/.auth/storage-state.json}"
- BACKEND_BASE="${BACKEND_BASE:-http://localhost:5005}"
- TENANT_ID="${TENANT_ID:-1}"
- FACTORY_ID="${FACTORY_ID:-1}"
- DB_HOST="${DB_HOST:-123.60.180.165}"
- DB_PORT="${DB_PORT:-3306}"
- DB_NAME="${DB_NAME:-aidopdev}"
- DB_USER="${DB_USER:-aidopremote}"
- DB_PASS="${DB_PASS:-1234567890aiDOP#}"
- S8REG_PASS_COUNT=0
- S8REG_FAIL_COUNT=0
- S8REG_SKIP_COUNT=0
- S8REG_NOTES=()
- s8reg_die() { echo "FATAL: $*" >&2; exit 2; }
- [[ "${DB_NAME}" == "aidopdev" ]] || s8reg_die "DB_NAME must be aidopdev, got ${DB_NAME}"
- mysql_run() {
- MYSQL_PWD="${DB_PASS}" mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" "${DB_NAME}" \
- --default-character-set=utf8mb4 --connect-timeout=8 -N -B -e "$1" 2>/dev/null
- }
- mysql_run_strict() {
- # 与 mysql_run 同,但失败立即 exit;用于必须成功的写入。
- MYSQL_PWD="${DB_PASS}" mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" "${DB_NAME}" \
- --default-character-set=utf8mb4 --connect-timeout=8 -N -B -e "$1" || \
- s8reg_die "mysql_run_strict failed: $1"
- }
- record_pass() { S8REG_PASS_COUNT=$((S8REG_PASS_COUNT + 1)); echo "PASS: $*"; S8REG_NOTES+=("PASS: $*"); }
- record_fail() { S8REG_FAIL_COUNT=$((S8REG_FAIL_COUNT + 1)); echo "FAIL: $*" >&2; S8REG_NOTES+=("FAIL: $*"); }
- record_skip() { S8REG_SKIP_COUNT=$((S8REG_SKIP_COUNT + 1)); echo "SKIP: $*"; S8REG_NOTES+=("SKIP: $*"); }
- print_summary() {
- echo "==== summary ===="
- echo "passed=${S8REG_PASS_COUNT}"
- echo "failed=${S8REG_FAIL_COUNT}"
- echo "skipped=${S8REG_SKIP_COUNT}"
- }
- read_baseline() {
- mysql_run "SELECT COUNT(*) FROM ado_s8_exception_type WHERE tenant_id=0 AND factory_id=0 AND enabled=1;"
- }
- # 用法: assert_baseline_unchanged "$before"
- assert_baseline_unchanged() {
- local before="$1"
- local after
- after=$(read_baseline)
- if [[ "${before}" == "${after}" ]]; then
- record_pass "baseline unchanged (before=after=${before})"
- return 0
- else
- record_fail "baseline drifted: before=${before} after=${after}"
- return 1
- fi
- }
- # 0 = 存在且 enabled=1;1 = 不存在或被禁用。
- require_demo_rule() {
- local code="$1"
- local cnt
- 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;")
- [[ "${cnt}" == "1" ]]
- }
- get_rule_field() {
- local id="$1"; local col="$2"
- mysql_run "SELECT IFNULL(${col}, 'NULL') FROM ado_s8_watch_rule WHERE id=${id};"
- }
- get_rule_id_by_code() {
- local code="$1"
- mysql_run "SELECT id FROM ado_s8_watch_rule WHERE rule_code='${code}' AND tenant_id=${TENANT_ID} AND factory_id=${FACTORY_ID};"
- }
- # ── demo rule 漂移检测(child 用) ────────────────────────────────────────
- # 用法:
- # DEMO_SNAPSHOT=$(snapshot_demo_rule_state)
- # ...
- # assert_demo_rule_state_unchanged "${DEMO_SNAPSHOT}"
- # 不依赖 enabled=1 假设;driver arm 后 child 看到 enabled=1,单跑 child 看到 enabled=0,
- # 两种场景都只检查"未漂移"。
- snapshot_demo_rule_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);"
- }
- assert_demo_rule_state_unchanged() {
- local before="$1"
- local after
- after=$(snapshot_demo_rule_state)
- if [[ "${before}" == "${after}" ]]; then
- record_pass "demo rule 10/11/12 conserved (drift-free, snapshot=${before})"
- return 0
- else
- record_fail "demo rule 10/11/12 drifted: before=${before} after=${after}"
- return 1
- fi
- }
- # ── demo rule arm/restore(driver 用) ───────────────────────────────────
- # CTO 决策(S8-DEMO-RULE-DEFAULT-DISABLED-1):业务 demo 规则默认 enabled=0,
- # 由业务人员现场启用。回归 driver 入场前临时 arm,收尾必须恢复 disabled。
- # 仅触动 enabled / paused_until / pause_reason / lock 三件套 / trigger / recover;
- # 不改 rule_code / scene_code / rule_type / expression / params_json。
- arm_demo_rules_for_regression() {
- mysql_run "UPDATE ado_s8_watch_rule
- SET enabled=1,
- paused_until=NULL, pause_reason=NULL,
- lock_token=NULL, locked_by=NULL, lock_until=NULL,
- trigger_count_required=1, recover_count_required=1,
- updated_at=NOW()
- WHERE id IN (10,11,12);" >/dev/null
- echo "[arm-demo] demo rule 10/11/12 enabled=1 (regression-only; will restore on exit)"
- }
- restore_demo_rules_disabled_after_regression() {
- mysql_run "UPDATE ado_s8_watch_rule
- SET enabled=0,
- paused_until=NULL, pause_reason=NULL,
- lock_token=NULL, locked_by=NULL, lock_until=NULL,
- trigger_count_required=1, recover_count_required=1,
- updated_at=NOW()
- WHERE id IN (10,11,12);" >/dev/null
- echo "[restore-demo] demo rule 10/11/12 enabled=0 (CTO policy: default off)"
- }
- # ── TEMP_SCHED_% 审批幽灵待办清理 ─────────────────────────────────────────
- # 当 child fixture 关闭 TEMP rule、软删 TEMP exception 后,对应的
- # ApprovalFlowInstance / ApprovalFlowTask 不会被业务级联取消;这些 Pending(0) 任务
- # 会污染审批中心(Demo01/Demo02 出现幽灵代办)。
- #
- # 严格范围:
- # 1. 仅命中 ado_s8_watch_rule.rule_code LIKE 'TEMP_SCHED_%' 关联的异常;
- # 2. 仅命中 ado_s8_exception.is_deleted=1 的异常(已 fixture teardown 软删);
- # 3. 仅命中 ApprovalFlowInstance.BizType ∈ {EXCEPTION_REPORT, EXCEPTION_ESCALATION, EXCEPTION_CLOSURE};
- # 4. 仅 Status=0(Pending) 任务改为 Status=4(Cancelled),不物理删除;
- # 5. 实例仅在该实例下已无 Pending task 时改 Status=4。
- # 不取消 DEMO_/真实业务异常的待办;不动 detection_log;不动 rule_detection_state。
- #
- # 表关联事实(已读核确认):
- # ApprovalFlowInstance.BizId = ado_s8_exception.id
- # ado_s8_exception.source_rule_id = ado_s8_watch_rule.id
- # ApprovalFlowTask.Status: 0=Pending / 4=Cancelled
- # ApprovalFlowInstance.Status: 4=Cancelled
- cleanup_temp_sched_approval_ghost_tasks() {
- local cancelled_tasks
- cancelled_tasks=$(mysql_run "
- UPDATE ApprovalFlowTask t
- JOIN ApprovalFlowInstance i ON i.Id = t.InstanceId
- JOIN ado_s8_exception e ON e.id = i.BizId
- JOIN ado_s8_watch_rule r ON r.id = e.source_rule_id
- SET t.Status = 4,
- t.ActionTime = NOW(),
- t.UpdateTime = NOW(),
- t.Comment = LEFT(CONCAT(IFNULL(t.Comment,''),'[regression-teardown:cancelled-temp-sched-ghost]'), 1024)
- WHERE t.Status = 0
- AND r.rule_code LIKE 'TEMP_SCHED_%'
- AND e.is_deleted = 1
- AND i.BizType IN ('EXCEPTION_REPORT','EXCEPTION_ESCALATION','EXCEPTION_CLOSURE');
- SELECT ROW_COUNT();" | tail -n1)
- local cancelled_instances
- cancelled_instances=$(mysql_run "
- UPDATE ApprovalFlowInstance i
- JOIN ado_s8_exception e ON e.id = i.BizId
- JOIN ado_s8_watch_rule r ON r.id = e.source_rule_id
- SET i.Status = 4,
- i.EndTime = COALESCE(i.EndTime, NOW()),
- i.UpdateTime = NOW()
- WHERE i.Status <> 4
- AND r.rule_code LIKE 'TEMP_SCHED_%'
- AND e.is_deleted = 1
- AND i.BizType IN ('EXCEPTION_REPORT','EXCEPTION_ESCALATION','EXCEPTION_CLOSURE')
- AND NOT EXISTS (
- SELECT 1 FROM ApprovalFlowTask t2
- WHERE t2.InstanceId = i.Id AND t2.Status = 0
- );
- SELECT ROW_COUNT();" | tail -n1)
- echo "[cleanup-ghost] cancelled_pending_tasks=${cancelled_tasks:-0} cancelled_instances=${cancelled_instances:-0}"
- }
- auth_load() {
- [[ -f "${STORAGE_STATE}" ]] || s8reg_die "storage state not found: ${STORAGE_STATE}"
- AT="$(python3 -c "
- import json,sys
- d=json.load(open(sys.argv[1]))
- for kv in d['origins'][0]['localStorage']:
- if kv['name']=='admin.net:access-token':
- print(kv['value'].strip('\"')); break
- " "${STORAGE_STATE}")"
- XAT="$(python3 -c "
- import json,sys
- d=json.load(open(sys.argv[1]))
- for kv in d['origins'][0]['localStorage']:
- if kv['name']=='admin.net:x-access-token':
- print(kv['value'].strip('\"')); break
- " "${STORAGE_STATE}")"
- [[ -n "${AT}" && "${AT}" != "null" ]] || s8reg_die "access-token missing in storage-state"
- [[ -n "${XAT}" && "${XAT}" != "null" ]] || s8reg_die "x-access-token missing in storage-state"
- export AT XAT
- }
- run_once_endpoint() {
- curl -fsS --max-time 30 -X POST \
- -H "Authorization: Bearer ${AT}" -H "X-Access-Token: ${XAT}" \
- "${BACKEND_BASE}/api/aidop/s8/watch-debug/run-once?tenantId=${TENANT_ID}&factoryId=${FACTORY_ID}"
- }
- # 子脚本退出码语义:
- # 0 = 至少一项 PASS 或纯 SKIP(FAIL=0)
- # 1 = 至少一项 FAIL
- # 2 = 致命错误(s8reg_die)
- exit_by_summary() {
- if (( S8REG_FAIL_COUNT > 0 )); then
- return 1
- fi
- return 0
- }
|