#!/usr/bin/env bash # R2-TIMEOUT regression script — S8-REGRESSION-FIXTURE-1 重构版(dev/test only, aidopdev)。 # # 演变史: # - 旧版基于 G01_TEST_TIMEOUT 旧 fixture(id=52)+ 强依赖 baseline=13 + G01_TEST_WATCH 兼容路径。 # - 重构后:默认复用 demo rule 10 DEMO_ORDER_DELIVERY_TIMEOUT(CTO 拍板二.A), # 若 demo rule 缺失则 record_skip,不再 FAIL。 # # 验证项(demo rule 10 模式): # 1. 双 run-once HTTP 200; # 2. 同一 dedup_key 下仅 1 条活动 exception(断言作用域 = TARGET_OBJECT_CODE); # 3. last_detected_at 在两次运行之间推进(同样按 TARGET_OBJECT_CODE 取窗); # 4. source_payload 含 __ruleType / __sourceObjectType / __sourceObjectId 元数据; # 5. demo rule 10 守恒:enabled=1 / paused_until=NULL / trigger=1 / recover=1 / # params_json / rule_type / expression 不变(运行态字段 last_run_at/next_run_at 等允许刷新)。 # # 作用域设计(S8-REGRESSION-R2-TIMEOUT-SCOPE-FIX-1): # demo rule 10 的 expression 是 `SELECT ... FROM demo_test_order` 无 WHERE, # 因此 demo_test_order 中所有已超期的行(TEST-ORDER-001 + DEMO-S2-001 + # DEMO-S3-001 等演示资产)都会命中产生独立 active exception。 # 旧版用全局 active TIMEOUT count==1 作硬断言会与演示数据并存事实冲突。 # 新版仅校验 TARGET_OBJECT_CODE(默认 TEST-ORDER-001)对应的 active exception # 存在且唯一;全局 active TIMEOUT 数量仅作 INFO 输出,不参与 PASS/FAIL 判定。 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 RULE_CODE="${RULE_CODE:-DEMO_ORDER_DELIVERY_TIMEOUT}" TARGET_OBJECT_CODE="${R2_TARGET_OBJECT_CODE:-TEST-ORDER-001}" # 目标对象作用域过滤:覆盖 dedup_key / related_object_code / source_object_id 三类匹配。 SCOPE_FILTER="(related_object_code='${TARGET_OBJECT_CODE}' OR source_object_id='${TARGET_OBJECT_CODE}' OR dedup_key LIKE '%:${TARGET_OBJECT_CODE}')" baseline_before=$(read_baseline) echo "==== r2-timeout-regression RULE_CODE=${RULE_CODE} TARGET_OBJECT_CODE=${TARGET_OBJECT_CODE} baseline_before=${baseline_before} ====" if ! require_demo_rule "${RULE_CODE}"; then record_skip "TIMEOUT fixture '${RULE_CODE}' not present or disabled — historic G01_TEST_TIMEOUT removed from dev" print_summary exit 0 fi record_pass "${RULE_CODE} enabled" RULE_ID=$(get_rule_id_by_code "${RULE_CODE}") # 守恒快照(CTO 约束 五.D:结束时确认未被污染) snap_enabled=$(get_rule_field "${RULE_ID}" enabled) snap_paused=$(get_rule_field "${RULE_ID}" paused_until) snap_trigger=$(get_rule_field "${RULE_ID}" trigger_count_required) snap_recover=$(get_rule_field "${RULE_ID}" recover_count_required) snap_rtype=$(get_rule_field "${RULE_ID}" rule_type) snap_expr=$(get_rule_field "${RULE_ID}" expression) snap_params=$(get_rule_field "${RULE_ID}" params_json) resp1=$(run_once_endpoint) count1=$(printf '%s' "${resp1}" | python3 -c "import json,sys;print(json.load(sys.stdin).get('count',0))") [[ "${count1}" -ge 1 ]] && record_pass "first run-once HTTP 200, count=${count1}" || record_fail "first run-once empty (${count1})" before_ts=$(mysql_run "SELECT IFNULL(MAX(last_detected_at), '1970-01-01') FROM ado_s8_exception WHERE source_rule_code='${RULE_CODE}' AND status<>'CLOSED' AND is_deleted=0 AND ${SCOPE_FILTER};") echo "pre-second-run last_detected_at(target=${TARGET_OBJECT_CODE})=${before_ts}" sleep 1 resp2=$(run_once_endpoint) count2=$(printf '%s' "${resp2}" | python3 -c "import json,sys;print(json.load(sys.stdin).get('count',0))") [[ "${count2}" -ge 1 ]] && record_pass "second run-once HTTP 200, count=${count2}" || record_fail "second run-once empty (${count2})" global_active=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code='${RULE_CODE}' AND status<>'CLOSED' AND is_deleted=0;") echo "INFO: global active TIMEOUT exceptions (rule=${RULE_CODE}) = ${global_active}; scoped target=${TARGET_OBJECT_CODE}" scoped_active=$(mysql_run "SELECT COUNT(*) FROM ado_s8_exception WHERE source_rule_code='${RULE_CODE}' AND status<>'CLOSED' AND is_deleted=0 AND ${SCOPE_FILTER};") [[ "${scoped_active}" == "1" ]] \ && record_pass "scoped active TIMEOUT exception count = 1 for target ${TARGET_OBJECT_CODE} (no duplicate creation; global=${global_active})" \ || record_fail "expected exactly 1 active TIMEOUT exception for target ${TARGET_OBJECT_CODE}, got ${scoped_active} (global=${global_active})" after_ts=$(mysql_run "SELECT MAX(last_detected_at) FROM ado_s8_exception WHERE source_rule_code='${RULE_CODE}' AND status<>'CLOSED' AND is_deleted=0 AND ${SCOPE_FILTER};") [[ "${after_ts}" > "${before_ts}" ]] && record_pass "last_detected_at refreshed for target ${TARGET_OBJECT_CODE}: ${before_ts} -> ${after_ts}" || record_fail "last_detected_at did not refresh for target ${TARGET_OBJECT_CODE} (before=${before_ts}, after=${after_ts})" payload=$(mysql_run "SELECT source_payload FROM ado_s8_exception WHERE source_rule_code='${RULE_CODE}' AND status<>'CLOSED' AND is_deleted=0 AND ${SCOPE_FILTER} ORDER BY id DESC LIMIT 1;") metadata_ok=1 for field in __sourceObjectId __sourceObjectType __ruleType; do echo "${payload}" | grep -q "${field}" || { record_fail "source_payload missing ${field}"; metadata_ok=0; } done (( metadata_ok == 1 )) && record_pass "source_payload metadata fields present (__ruleType / __sourceObjectType / __sourceObjectId)" # 守恒断言 final_enabled=$(get_rule_field "${RULE_ID}" enabled) final_paused=$(get_rule_field "${RULE_ID}" paused_until) final_trigger=$(get_rule_field "${RULE_ID}" trigger_count_required) final_recover=$(get_rule_field "${RULE_ID}" recover_count_required) final_rtype=$(get_rule_field "${RULE_ID}" rule_type) final_expr=$(get_rule_field "${RULE_ID}" expression) final_params=$(get_rule_field "${RULE_ID}" params_json) [[ "${final_enabled}" == "${snap_enabled}" && "${final_paused}" == "${snap_paused}" \ && "${final_trigger}" == "${snap_trigger}" && "${final_recover}" == "${snap_recover}" \ && "${final_rtype}" == "${snap_rtype}" && "${final_expr}" == "${snap_expr}" \ && "${final_params}" == "${snap_params}" ]] \ && record_pass "${RULE_CODE} 守恒: enabled/paused/trigger/recover/rule_type/expression/params_json 全部不变" \ || record_fail "${RULE_CODE} 守恒失败: 配置字段被污染" assert_baseline_unchanged "${baseline_before}" print_summary exit_by_summary