488 lines
20 KiB
Bash
Executable File
488 lines
20 KiB
Bash
Executable File
#!/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 health(http=${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 package(versionId=${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 success(taskChainId=${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)"
|
||
|
||
# 扫完整个主 feed:0=找到并输出 versionId,1=确认到底仍不存在,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 已进入主 feed(versionId=${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 aggregated(play=${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"
|