488 lines
20 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# 绘境AI staging 冒烟门。
# 默认模式只做公开读 readiness--dogfood-e2e 才执行真实创作、审核、试玩会话和遥测写链。
set -uo pipefail
BASE="${BASE:-http://100.64.0.7:48080}"
STUDIO_BASE="${STUDIO_BASE:-http://100.64.0.7:4173}"
ORIGIN="${ORIGIN:-$STUDIO_BASE}"
TENANT="${TENANT:-1}"
MODE="readiness"
if [ "${1:-}" = "--dogfood-e2e" ]; then
MODE="dogfood-e2e"
elif [ -n "${1:-}" ]; then
printf '未知参数:%s\n用法%s [--dogfood-e2e]\n' "$1" "$0" >&2
exit 2
fi
PASS=0
FAIL=0
FAILED_ITEMS=()
LAST_HTTP=""
LAST_BODY=""
BODY_FILE="$(mktemp)"
RUN_ID="${RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)-$$}"
EVIDENCE_OUT="${EVIDENCE_OUT:-/tmp/dogfood-e2e-${RUN_ID}.json}"
TASK_CHAIN_ID=""
AIGC_TASK_ID=""
GAME_ID=""
VERSION_ID=""
SESSION_ID=""
TELEMETRY_TRACE_ID=""
ARTIFACT_HASH=""
E2E_STATUS="not-started"
FAILURE_ITEM=""
cleanup() {
rm -f "$BODY_FILE"
}
trap cleanup EXIT
if [ -t 1 ]; then
G='\033[0;32m'; R='\033[0;31m'; B='\033[0;34m'; N='\033[0m'
else
G=''; R=''; B=''; N=''
fi
log() { printf "%b\n" "$*"; }
head_line() { printf "\n%b== %s ==%b\n" "$B" "$1" "$N"; }
# 请求响应分别放在全局 LAST_HTTP/LAST_BODY避免 token 或响应体进入日志。
request() {
local token="$1" method="$2" path="$3" data="${4:-}"
local url="$path"
local -a args
[[ "$path" == http://* || "$path" == https://* ]] || url="$BASE$path"
args=(-sS --connect-timeout 5 --max-time 30 -o "$BODY_FILE" -w '%{http_code}' -X "$method"
-H "tenant-id: $TENANT" -H "Origin: $ORIGIN")
[ -n "$token" ] && args+=(-H "Authorization: Bearer $token")
if [ -n "$data" ]; then
args+=(-H 'Content-Type: application/json' --data "$data")
fi
LAST_HTTP="$(curl "${args[@]}" "$url" 2>/dev/null || printf '000')"
LAST_BODY="$(<"$BODY_FILE")"
}
# 从 stdin JSON 按点路径读取标量;缺字段或非 JSON 返回空串。
json_path() {
local path="$1"
python3 -c '
import json,sys
path=sys.argv[1].split(".") if sys.argv[1] else []
try:
value=json.load(sys.stdin)
for part in path:
value=value[int(part)] if isinstance(value,list) else value.get(part)
if value is None: break
if value is None: print("")
elif isinstance(value,bool): print("true" if value else "false")
elif isinstance(value,(dict,list)): print(json.dumps(value,separators=(",",":"),ensure_ascii=False))
else: print(value)
except Exception:
print("")
' "$path"
}
body_value() {
printf '%s' "$LAST_BODY" | json_path "$1"
}
envelope_ok() {
[ "$LAST_HTTP" = "200" ] && [ "$(body_value code)" = "0" ]
}
mark_pass() {
PASS=$((PASS + 1))
log " ${G}PASS${N} $1"
}
mark_fail() {
FAIL=$((FAIL + 1))
FAILED_ITEMS+=("$1")
log " ${R}FAIL${N} $1"
}
# 狗粮安全边界业务码:逐字对账后端各模块 ErrorCodeConstants禁止用“任意失败”冒充安全门生效。
CODE_PASSWORD_REGISTER_DISABLED=1002090007 # system PASSWORD_REGISTER_DISABLED = 1_002_090_007
CODE_DOGFOOD_SMS_DISABLED=1002090008 # system DOGFOOD_SMS_DISABLED = 1_002_090_008
CODE_AD_DOGFOOD_DISABLED=1111002006 # ad AD_DOGFOOD_DISABLED = 1_111_002_006
CODE_TRADE_DOGFOOD_DISABLED=1106008000 # trade TRADE_DOGFOOD_MONETIZATION_DISABLED = 1_106_008_000
expect_business_code() {
local name="$1" expected="$2" token="$3" method="$4" path="$5" data="${6:-}"
request "$token" "$method" "$path" "$data"
local actual
actual="$(body_value code)"
if [ "$LAST_HTTP" = "200" ] && [ "$actual" = "$expected" ]; then
mark_pass "${name}(业务码=${expected}"
else
e2e_fail "$name 未返回预期业务码expected=$expected actual=${actual:-none} http=${LAST_HTTP}"
fi
}
read_check() {
local name="$1" path="$2" require_data="${3:-0}"
request "" GET "$path"
if envelope_ok; then
if [ "$require_data" = "1" ] && ! printf '%s' "$LAST_BODY" | python3 -c '
import json,sys
value=json.load(sys.stdin).get("data")
assert value is not None and value != [] and value != {} and value != ""
' >/dev/null 2>&1; then
mark_fail "${name}data 为空)"
else
mark_pass "$name"
fi
else
mark_fail "${name}http=${LAST_HTTP} code=$(body_value code)"
fi
}
require_env() {
local name
for name in "$@"; do
if [ -z "${!name:-}" ]; then
log "${R}缺少必填环境变量:$name${N}" >&2
exit 2
fi
done
}
write_evidence() {
local status="$1"
mkdir -p "$(dirname "$EVIDENCE_OUT")"
E2E_STATUS="$status" RUN_ID="$RUN_ID" TASK_CHAIN_ID="$TASK_CHAIN_ID" AIGC_TASK_ID="$AIGC_TASK_ID" \
GAME_ID="$GAME_ID" VERSION_ID="$VERSION_ID" SESSION_ID="$SESSION_ID" \
TELEMETRY_TRACE_ID="$TELEMETRY_TRACE_ID" ARTIFACT_HASH="$ARTIFACT_HASH" FAILURE_ITEM="$FAILURE_ITEM" \
python3 - "$EVIDENCE_OUT" <<'PY'
import json,os,sys
def number(name):
raw=os.environ.get(name,"")
return int(raw) if raw.isdigit() else None
doc={
"runId":os.environ["RUN_ID"], "status":os.environ["E2E_STATUS"],
"taskChainId":number("TASK_CHAIN_ID"), "aigcTaskId":number("AIGC_TASK_ID"),
"gameId":number("GAME_ID"), "versionId":number("VERSION_ID"),
"sessionId":number("SESSION_ID"), "telemetryTraceId":os.environ.get("TELEMETRY_TRACE_ID") or None,
"artifactHash":os.environ.get("ARTIFACT_HASH") or None,
"result":{"verdict":os.environ["E2E_STATUS"],"failedItem":os.environ.get("FAILURE_ITEM") or None},
"browserQaResult":"pending",
}
with open(sys.argv[1],"w",encoding="utf-8") as f:
json.dump(doc,f,ensure_ascii=False,indent=2)
PY
chmod 600 "$EVIDENCE_OUT"
}
e2e_fail() {
local message="$1"
E2E_STATUS="failed"
FAILURE_ITEM="$message"
write_evidence "$E2E_STATUS"
log "${R}DOGFOOD_E2E_FAIL${N} $message"
log "证据:$EVIDENCE_OUT"
exit 1
}
e2e_require_ok() {
local name="$1"
if envelope_ok; then
mark_pass "$name"
else
e2e_fail "${name}http=${LAST_HTTP} code=$(body_value code)"
fi
}
# 写模式具有明确副作用,账号凭据与人工确认必须在任何网络请求前齐备。
if [ "$MODE" = "dogfood-e2e" ]; then
require_env DOGFOOD_CREATOR_USERNAME DOGFOOD_CREATOR_PASSWORD \
DOGFOOD_REVIEWER_USERNAME DOGFOOD_REVIEWER_PASSWORD \
DOGFOOD_READONLY_USERNAME DOGFOOD_READONLY_PASSWORD \
DOGFOOD_E2E_ACK
if [ "$DOGFOOD_E2E_ACK" != "I_ACKNOWLEDGE_DOGFOOD_WRITES" ]; then
log "${R}DOGFOOD_E2E_ACK 无效;必须显式设置为 I_ACKNOWLEDGE_DOGFOOD_WRITES${N}" >&2
exit 2
fi
fi
head_line "公开只读 readiness"
request "" GET /actuator/health
if [ "$LAST_HTTP" = "200" ]; then mark_pass "backend health"; else mark_fail "backend healthhttp=${LAST_HTTP}"; fi
read_check "feed zones" /app-api/feed/zones 1
read_check "feed stream" "/app-api/feed/stream?size=1" 1
FIRST_VERSION_ID=""
request "" GET "/app-api/feed/stream?size=1"
if envelope_ok; then
FIRST_VERSION_ID="$(body_value data.list.0.versionId)"
fi
if [ -n "$FIRST_VERSION_ID" ]; then
read_check "published runtime packageversionId=${FIRST_VERSION_ID}" "/app-api/runtime/package/${FIRST_VERSION_ID}?scene=play" 1
else
mark_fail "feed 首条缺 versionId无法校验公开运行包"
fi
if [ "$MODE" = "readiness" ]; then
head_line "总判"
if [ "$FAIL" -eq 0 ]; then
log "${G}SMOKE_PASS${N} $PASS/$((PASS + FAIL)) 全过(默认模式无写请求)"
log "SMOKE_VERDICT=PASS"
exit 0
fi
log "${R}SMOKE_FAIL${N} $PASS/$((PASS + FAIL)) 通过"
printf ' - %s\n' "${FAILED_ITEMS[@]}"
log "SMOKE_VERDICT=FAIL"
exit 1
fi
# 以下模式会创建并发布一款带 RUN_ID 的游戏、写试玩会话和遥测事件,不做隐式清理。
E2E_TIMEOUT_SEC="${E2E_TIMEOUT_SEC:-1200}"
POLL_INTERVAL_SEC="${POLL_INTERVAL_SEC:-5}"
E2E_PROMPT="${E2E_PROMPT:-做一个躲避陨石、可以计分并有明确结束状态的小游戏}"
E2E_TITLE="${E2E_TITLE:-dogfood-${RUN_ID}}"
export DOGFOOD_CREATOR_USERNAME DOGFOOD_CREATOR_PASSWORD DOGFOOD_REVIEWER_USERNAME \
DOGFOOD_REVIEWER_PASSWORD DOGFOOD_READONLY_USERNAME DOGFOOD_READONLY_PASSWORD \
E2E_PROMPT E2E_TITLE RUN_ID
head_line "真实账号登录(凭据不落日志)"
CREATOR_LOGIN_BODY="$(python3 - <<'PY'
import json,os
print(json.dumps({"username":os.environ["DOGFOOD_CREATOR_USERNAME"],"password":os.environ["DOGFOOD_CREATOR_PASSWORD"]}))
PY
)"
request "" POST /app-api/passport/password-login "$CREATOR_LOGIN_BODY"
e2e_require_ok "creator password login"
CREATOR_TOKEN="$(body_value data.accessToken)"
CREATOR_USER_ID="$(body_value data.userId)"
CREATOR_FLAG="$(body_value data.creatorFlag)"
[ -n "$CREATOR_TOKEN" ] || e2e_fail "creator 登录响应缺 accessToken"
[ "$CREATOR_FLAG" = "1" ] || e2e_fail "creatorFlag=${CREATOR_FLAG},不是预置创作者"
admin_login() {
local role="$1" user_var="$2" pass_var="$3"
local payload
export DOGFOOD_LOGIN_USERNAME="${!user_var}" DOGFOOD_LOGIN_PASSWORD="${!pass_var}"
payload="$(python3 - <<'PY'
import json,os
print(json.dumps({"username":os.environ["DOGFOOD_LOGIN_USERNAME"],"password":os.environ["DOGFOOD_LOGIN_PASSWORD"]}))
PY
)"
request "" POST /admin-api/system/auth/login "$payload"
envelope_ok || e2e_fail "$role 登录失败http=$LAST_HTTP code=$(body_value code)"
ADMIN_LOGIN_TOKEN="$(body_value data.accessToken)"
[ -n "$ADMIN_LOGIN_TOKEN" ] || e2e_fail "$role 登录响应缺 accessToken"
}
ADMIN_LOGIN_TOKEN=""
admin_login reviewer DOGFOOD_REVIEWER_USERNAME DOGFOOD_REVIEWER_PASSWORD
REVIEWER_TOKEN="$ADMIN_LOGIN_TOKEN"
admin_login readonly DOGFOOD_READONLY_USERNAME DOGFOOD_READONLY_PASSWORD
READONLY_TOKEN="$ADMIN_LOGIN_TOKEN"
mark_pass "reviewer / readonly password login"
head_line "狗粮安全边界精确负例"
# 用户名最长 30 位,保留 dogreg + RUN_ID 数字前 24 位。
REGISTER_NEGATIVE_BODY="$(REGISTER_NAME="dogreg${RUN_ID//[^0-9]/}" python3 - <<'PY'
import json,os
print(json.dumps({"username":os.environ["REGISTER_NAME"][:30],"password":"DogfoodNoRegister1"}))
PY
)"
expect_business_code "password register disabled" "$CODE_PASSWORD_REGISTER_DISABLED" "" POST \
/app-api/passport/password-register "$REGISTER_NEGATIVE_BODY"
expect_business_code "SMS send disabled" "$CODE_DOGFOOD_SMS_DISABLED" "" POST \
/app-api/passport/send-sms-code '{"mobile":"13800000000"}'
expect_business_code "ad slot disabled" "$CODE_AD_DOGFOOD_DISABLED" "$CREATOR_TOKEN" GET \
/app-api/ad/slot/list-enabled
expect_business_code "charge balance disabled" "$CODE_TRADE_DOGFOOD_DISABLED" "$CREATOR_TOKEN" GET \
/app-api/trade/charge/points-balance
expect_business_code "withdraw page disabled" "$CODE_TRADE_DOGFOOD_DISABLED" "$CREATOR_TOKEN" GET \
'/app-api/trade/withdraw/page?pageNo=1&pageSize=1'
head_line "创建并等待生成终态"
CREATE_BODY="$(python3 - <<'PY'
import json,os
print(json.dumps({"title":os.environ["E2E_TITLE"],"prompt":os.environ["E2E_PROMPT"],
"templateId":"generic","idempotencyKey":"dogfood-"+os.environ["RUN_ID"]},ensure_ascii=False))
PY
)"
request "$CREATOR_TOKEN" POST /app-api/studio/create "$CREATE_BODY"
e2e_require_ok "studio one-step create"
TASK_CHAIN_ID="$(body_value data.id)"
AIGC_TASK_ID="$(body_value data.aigcTaskId)"
GAME_ID="$(body_value data.gameId)"
[ -n "$TASK_CHAIN_ID" ] && [ -n "$GAME_ID" ] || e2e_fail "create 响应缺 taskChainId/gameId"
deadline=$((SECONDS + E2E_TIMEOUT_SEC))
while [ "$SECONDS" -lt "$deadline" ]; do
request "$CREATOR_TOKEN" GET "/app-api/studio/task/$TASK_CHAIN_ID"
envelope_ok || e2e_fail "轮询任务链失败http=$LAST_HTTP code=$(body_value code)"
task_status="$(body_value data.status)"
AIGC_TASK_ID="$(body_value data.aigcTaskId)"
GAME_ID="$(body_value data.gameId)"
VERSION_ID="$(body_value data.versionId)"
if [ "$task_status" = "2" ]; then break; fi
[ "$task_status" != "3" ] || e2e_fail "生成进入失败终态taskChainId=${TASK_CHAIN_ID} aigcTaskId=${AIGC_TASK_ID}"
sleep "$POLL_INTERVAL_SEC"
done
[ "${task_status:-}" = "2" ] || e2e_fail "等待生成终态超时 ${E2E_TIMEOUT_SEC}s"
[ -n "$VERSION_ID" ] || e2e_fail "生成完成但 versionId 为空"
mark_pass "generation terminal successtaskChainId=${TASK_CHAIN_ID} versionId=${VERSION_ID}"
head_line "预览包与发布门禁"
request "$CREATOR_TOKEN" GET "/app-api/runtime/package/$VERSION_ID?scene=preview"
e2e_require_ok "creator preview package descriptor"
ARTIFACT_HASH="$(body_value data.checksum)"
if ! [[ "$ARTIFACT_HASH" =~ ^[0-9a-f]{64}$ ]]; then
e2e_fail "预览运行包缺少合法 SHA-256 checksum"
fi
request "$CREATOR_TOKEN" GET "/app-api/runtime/package/$VERSION_ID/manifest?scene=preview"
if [ "$LAST_HTTP" = "200" ] && printf '%s' "$LAST_BODY" | python3 -m json.tool >/dev/null 2>&1; then
mark_pass "creator preview manifest"
else
e2e_fail "预览 manifest 不可读http=${LAST_HTTP}"
fi
LAUNCH_ZONE_ID="${DOGFOOD_ZONE_ID:-}"
if [ -z "$LAUNCH_ZONE_ID" ]; then
request "" GET /app-api/feed/zones
envelope_ok || e2e_fail "无法读取发布专区"
LAUNCH_ZONE_ID="$(body_value data.0.id)"
fi
[ -n "$LAUNCH_ZONE_ID" ] || e2e_fail "没有可用发布专区,请设置 DOGFOOD_ZONE_ID"
PUBLISH_BODY="{\"versionId\":$VERSION_ID,\"launchZoneId\":$LAUNCH_ZONE_ID}"
request "$CREATOR_TOKEN" POST "/app-api/project/$GAME_ID/publish" "$PUBLISH_BODY"
e2e_require_ok "submit publish"
[ "$(body_value data.admitted)" = "true" ] || e2e_fail "发布门禁未全通过:$(body_value data.gates)"
# 扫完整个主 feed0=找到并输出 versionId1=确认到底仍不存在2=请求/游标异常导致无法下结论。
scan_main_feed() {
local cursor="" page encoded found
for page in $(seq 1 20); do
if [ -n "$cursor" ]; then
encoded="$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1],safe=""))' "$cursor")"
request "" GET "/app-api/feed/stream?size=30&cursor=$encoded"
else
request "" GET "/app-api/feed/stream?size=30"
fi
envelope_ok || return 2
found="$(printf '%s' "$LAST_BODY" | python3 -c 'import json,sys; gid=int(sys.argv[1]); d=json.load(sys.stdin); xs=(d.get("data") or {}).get("list",[]); print(next((x.get("versionId","") for x in xs if int(x.get("gameId",-1))==gid),""))' "$GAME_ID")"
if [ -n "$found" ]; then printf '%s' "$found"; return 0; fi
[ "$(body_value data.hasMore)" = "true" ] || return 1
cursor="$(body_value data.nextCursor)"
[ -n "$cursor" ] || return 2
done
return 2
}
head_line "审核队列与 RBAC 负验"
request "$REVIEWER_TOKEN" GET "/admin-api/project/review/page?pageNo=1&pageSize=100&status=1"
e2e_require_ok "reviewer review queue"
if ! printf '%s' "$LAST_BODY" | python3 -c 'import json,sys; gid=int(sys.argv[1]); d=json.load(sys.stdin); assert any(int(x.get("id",-1))==gid for x in (d.get("data") or {}).get("list",[]))' "$GAME_ID"; then
e2e_fail "新游戏未进入审核队列gameId=${GAME_ID}"
fi
REVIEW_BODY="{\"gameId\":$GAME_ID,\"versionId\":$VERSION_ID,\"decision\":1,\"reason\":\"dogfood $RUN_ID\"}"
request "$READONLY_TOKEN" POST /admin-api/project/review "$REVIEW_BODY"
if envelope_ok; then
e2e_fail "readonly 账号竟可审核RBAC 负验失败"
fi
request "$REVIEWER_TOKEN" GET "/admin-api/project/review/page?pageNo=1&pageSize=100&status=1"
if ! envelope_ok || ! printf '%s' "$LAST_BODY" | python3 -c 'import json,sys; gid=int(sys.argv[1]); d=json.load(sys.stdin); assert any(int(x.get("id",-1))==gid for x in (d.get("data") or {}).get("list",[]))' "$GAME_ID"; then
e2e_fail "readonly 拒绝后审核对象状态发生变化"
fi
mark_pass "readonly 审核写权限被拒绝且状态未变"
PRE_APPROVE_VERSION="$(scan_main_feed)"
PRE_APPROVE_SCAN_RC=$?
if [ "$PRE_APPROVE_SCAN_RC" -eq 0 ]; then
e2e_fail "审核通过前 gameId=$GAME_ID 已进入主 feedversionId=${PRE_APPROVE_VERSION}"
elif [ "$PRE_APPROVE_SCAN_RC" -eq 1 ]; then
mark_pass "pre-approve game absent from main feed"
else
e2e_fail "审核通过前无法完成主 feed 全量缺席证明"
fi
request "$REVIEWER_TOKEN" POST /admin-api/project/review "$REVIEW_BODY"
e2e_require_ok "reviewer approve"
head_line "发布态、feed 与公开运行包"
deadline=$((SECONDS + 90))
project_status=""
while [ "$SECONDS" -lt "$deadline" ]; do
request "$CREATOR_TOKEN" GET "/app-api/project/$GAME_ID"
if envelope_ok; then project_status="$(body_value data.status)"; fi
[ "$project_status" = "4" ] && break
sleep 2
done
[ "$project_status" = "4" ] || e2e_fail "审核通过后项目未进入 published(4)"
mark_pass "project published status=4"
FEED_VERSION_ID="$(scan_main_feed)"
POST_APPROVE_SCAN_RC=$?
[ "$POST_APPROVE_SCAN_RC" -eq 0 ] || e2e_fail "审核通过后未能在主 feed 找到 gameId=${GAME_ID}scanRc=${POST_APPROVE_SCAN_RC}"
[ "$FEED_VERSION_ID" = "$VERSION_ID" ] || e2e_fail "feed versionId=$FEED_VERSION_ID 与生成版本 $VERSION_ID 不一致"
mark_pass "exact game appears in main feed"
request "" GET "/app-api/runtime/package/$VERSION_ID?scene=play"
e2e_require_ok "public play package descriptor"
request "" GET "/app-api/runtime/package/$VERSION_ID/manifest?scene=play"
if [ "$LAST_HTTP" = "200" ] && printf '%s' "$LAST_BODY" | python3 -m json.tool >/dev/null 2>&1; then
mark_pass "public play manifest"
else
e2e_fail "公开试玩 manifest 不可读http=${LAST_HTTP}"
fi
head_line "试玩会话与遥测回流"
request "$READONLY_TOKEN" GET "/admin-api/telemetry/game-stat/$GAME_ID"
e2e_require_ok "readonly telemetry query"
BASE_PLAY_COUNT="$(body_value data.playCount)"; BASE_PLAY_COUNT="${BASE_PLAY_COUNT:-0}"
BASE_END_COUNT="$(body_value data.playEndCount)"; BASE_END_COUNT="${BASE_END_COUNT:-0}"
TELEMETRY_TRACE_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"
CLIENT_PLAY_TOKEN="ptk-${RUN_ID:0:36}"
SESSION_BODY="{\"gameId\":$GAME_ID,\"versionId\":$VERSION_ID,\"clientPlayToken\":\"$CLIENT_PLAY_TOKEN\",\"scene\":\"play\",\"traceId\":\"$TELEMETRY_TRACE_ID\"}"
request "$CREATOR_TOKEN" POST /app-api/runtime/session/start "$SESSION_BODY"
e2e_require_ok "runtime session start"
SESSION_ID="$(body_value data.sessionId)"
[ -n "$SESSION_ID" ] || e2e_fail "session start 未返回 sessionId"
request "$CREATOR_TOKEN" POST /app-api/runtime/session/end "{\"sessionId\":$SESSION_ID,\"durationMs\":1200,\"endReason\":\"game_end\"}"
e2e_require_ok "runtime session end"
export GAME_ID VERSION_ID SESSION_ID TELEMETRY_TRACE_ID CREATOR_USER_ID
TELEMETRY_BODY="$(python3 - <<'PY'
import json,os,time,uuid
now=int(time.time()*1000)
base={"schemaVersion":"v1","traceId":os.environ["TELEMETRY_TRACE_ID"],
"sessionId":os.environ["SESSION_ID"],"user":{"userId":os.environ["CREATOR_USER_ID"]},
"context":{"gameId":os.environ["GAME_ID"],"versionId":os.environ["VERSION_ID"],"deviceType":"desktop"}}
start={**base,"eventId":str(uuid.uuid4()),"event":"game_play_start","ts":now,"props":{}}
end={**base,"eventId":str(uuid.uuid4()),"event":"game_play_end","ts":now+1200,
"props":{"duration_ms":1200,"completed":True}}
print(json.dumps({"batch":[start,end]},separators=(",",":")))
PY
)"
request "$CREATOR_TOKEN" POST /app-api/telemetry/events/batch "$TELEMETRY_BODY"
e2e_require_ok "telemetry batch admission"
[ "$(body_value data.accepted)" = "2" ] && [ "$(body_value data.rejected)" = "0" ] || \
e2e_fail "遥测批量未全部受理accepted=$(body_value data.accepted) rejected=$(body_value data.rejected)"
deadline=$((SECONDS + 120))
PLAY_COUNT="$BASE_PLAY_COUNT"; END_COUNT="$BASE_END_COUNT"
while [ "$SECONDS" -lt "$deadline" ]; do
request "$READONLY_TOKEN" GET "/admin-api/telemetry/game-stat/$GAME_ID"
if envelope_ok; then
PLAY_COUNT="$(body_value data.playCount)"; PLAY_COUNT="${PLAY_COUNT:-0}"
END_COUNT="$(body_value data.playEndCount)"; END_COUNT="${END_COUNT:-0}"
if [ "$PLAY_COUNT" -gt "$BASE_PLAY_COUNT" ] 2>/dev/null && [ "$END_COUNT" -gt "$BASE_END_COUNT" ] 2>/dev/null; then break; fi
fi
sleep 3
done
if [ "$PLAY_COUNT" -le "$BASE_PLAY_COUNT" ] 2>/dev/null || [ "$END_COUNT" -le "$BASE_END_COUNT" ] 2>/dev/null; then
e2e_fail "遥测 MQ 聚合在 120s 内未回流play ${BASE_PLAY_COUNT}->${PLAY_COUNT}, end ${BASE_END_COUNT}->${END_COUNT}"
fi
mark_pass "telemetry persisted and aggregatedplay=${PLAY_COUNT} end=${END_COUNT}"
E2E_STATUS="passed"
write_evidence "$E2E_STATUS"
head_line "总判"
log "${G}DOGFOOD_E2E_PASS${N} gameId=$GAME_ID versionId=$VERSION_ID sessionId=$SESSION_ID"
log "浏览器真玩仍是独立硬门:$STUDIO_BASE/play/$GAME_ID/$VERSION_ID"
log "证据:$EVIDENCE_OUT"
log "DOGFOOD_E2E_VERDICT=PASS"