lili 0daaac0306 fix(smoke): U6 smoke-test.sh 可移植性——裸 $var 紧贴全角字符加花括号(R3)
bare `$var` 紧贴全角()(等 → strict-locale bash(macOS 5.3)把多字节首字节并进变量名 → set -u unbound;
smoke 在 lili-mac 一贯断在 template/list 行。4 处加 ${}(tcount/VID/QSCORE/SID),语义不变、跨环境可跑。

U6 验证(staging 五业务链路,本机 curl 可达):11/12 真绿——创作 draft / 发布(feed/zones/project) /
试玩取包(vid=93112) / 广告钱包(ad/trade) / 数据回路(qualityScore=60)。唯一 FAIL=template/list count=0
= W-CLEAN 清旧四模板 + generic 桥接的设计态(总账 §1 记非回归);R3=链路就绪门(不代理 R1)成立。
follow-up: template/list assertion 按 generic 桥接更新(非本单元业务语义裁定)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:43:33 +00:00

181 lines
10 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 五链路冒烟门(deploy smoke gate)
# -----------------------------------------------------------------------------
# 用途:每次 staging 部署后跑一遍,**秒级**确认 5 条端到端闭环的关键 API 在岗、
# 鉴权/CORS/质量分回路结构正常。这是「部署后是否健康」的就绪检查(liveness/
# readiness),**不是**深度 e2e(深度 e2e 走 agent-loop orchestrator 批跑)。
#
# 设计原则:
# - 默认 **以读为主**(health/列表/feed/钱包/包体),可随时重复跑;唯一写=studio/draft 建一条
# 草稿(status 0,不发布、无下游副作用、可忽略),用于验证创作入口+鉴权+gameId 分配在岗;
# - `--deep` 才走重写路径(generate 入队触发真实生成 + 遥测事件 + 互动),会落库,按需开启;
# - 每条链路逐项 PASS/FAIL,末尾总判 + 退出码(0=全过,1=有 FAIL)——可挂 CI/部署脚本。
#
# 用法:
# bash deploy/smoke-test.sh # 默认 staging 只读冒烟
# BASE=http://100.64.0.7:48080 TOKEN=test1 bash deploy/smoke-test.sh
# bash deploy/smoke-test.sh --deep # 含写路径(生成入队+遥测事件)
#
# 约定:内网地址/测试 token 可入仓(内网非密);**真实密钥严禁写入本文件**。
# =============================================================================
set -uo pipefail
BASE="${BASE:-http://100.64.0.7:48080}" # staging 后端(mini-desktop)
TOKEN="${TOKEN:-test1}" # staging 批跑/冒烟测试态 token(种子创作者)
TENANT="${TENANT:-1}"
ORIGIN="${ORIGIN:-http://100.64.0.7:4173}" # 前端 origin(验 CORS 同源放行)
DEEP=0
[ "${1:-}" = "--deep" ] && DEEP=1
PASS=0; FAIL=0; FAILED_ITEMS=()
# 颜色(非 TTY 自动降级为纯文本)
if [ -t 1 ]; then G='\033[0;32m'; R='\033[0;31m'; Y='\033[0;33m'; B='\033[0;34m'; N='\033[0m'; else G=''; R=''; Y=''; B=''; N=''; fi
_log() { printf "%b\n" "$*"; }
_head() { printf "\n%b== %s ==%b\n" "$B" "$1" "$N"; }
# api METHOD PATH [DATA] —— 返回 "HTTP_CODE\nBODY";鉴权头正确引用(防 'Bearer test1' 空格被拆)
api() {
local method="$1" path="$2" data="${3:-}"
local url="$path"; [[ "$path" == http* ]] || url="$BASE$path"
if [ -n "$data" ]; then
curl -s -w $'\n%{http_code}' -X "$method" "$url" \
-H "Authorization: Bearer $TOKEN" -H "tenant-id: $TENANT" \
-H "Origin: $ORIGIN" -H "Content-Type: application/json" -d "$data" 2>/dev/null
else
curl -s -w $'\n%{http_code}' -X "$method" "$url" \
-H "Authorization: Bearer $TOKEN" -H "tenant-id: $TENANT" \
-H "Origin: $ORIGIN" 2>/dev/null
fi
}
# check NAME METHOD PATH MODE [EXTRA] [DATA]
# MODE=health → HTTP 200 即过(actuator)
# MODE=envelope → HTTP 200 ∧ huijing body code==0 即过
# MODE=envelope_nonempty → 再要求 data 非空(list 长度≥1 或 data 非 null)
# EXTRA:envelope_nonempty 下可传 jq 风格 python 取数表达式(默认探 data.list 或 data)
check() {
local name="$1" method="$2" path="$3" mode="$4" data="${5:-}"
local out code body
out="$(api "$method" "$path" "$data")"
code="$(printf '%s' "$out" | tail -n1)"
body="$(printf '%s' "$out" | sed '$d')"
local ok=0 detail=""
case "$mode" in
health)
[ "$code" = "200" ] && ok=1; detail="http=$code" ;;
envelope|envelope_nonempty)
if [ "$code" = "200" ]; then
# 解析 huijing 信封 {code,data};envelope_nonempty 再判 data 是否非空
local pyout
pyout="$(printf '%s' "$body" | python3 -c "
import sys,json
try: d=json.load(sys.stdin)
except Exception as e: print('ERR|parse'); sys.exit()
c=d.get('code')
data=d.get('data')
if isinstance(data,dict): n=len(data.get('list')) if isinstance(data.get('list'),list) else (1 if data else 0)
elif isinstance(data,list): n=len(data)
elif data is None: n=0
else: n=1
print('%s|%s'%(c,n))
" 2>/dev/null)"
local ecode="${pyout%%|*}" ecount="${pyout##*|}"
if [ "$ecode" = "0" ]; then
if [ "$mode" = "envelope_nonempty" ]; then
[ "${ecount:-0}" -ge 1 ] 2>/dev/null && ok=1 || ok=0
detail="code=0 count=$ecount"
else ok=1; detail="code=0"; fi
else detail="http=$code code=$ecode"; fi
else detail="http=$code"; fi ;;
esac
if [ "$ok" = "1" ]; then PASS=$((PASS+1)); _log " ${G}✓${N} $name ($detail)"; else FAIL=$((FAIL+1)); FAILED_ITEMS+=("$name [$detail]"); _log " ${R}✗${N} $name ($detail)"; fi
}
_log "${B}造梦AI staging 五链路冒烟门${N} BASE=$BASE TOKEN=${TOKEN:0:2}*** deep=$DEEP"
# ---------------- 基础设施 + 鉴权 ----------------
_head "基础设施 / 鉴权"
check "infra: actuator/health" GET /actuator/health health
check "auth: passport/me(token 有效)" GET /app-api/passport/me envelope
# ---------------- 链路① 创作 → 生成 → 预览试玩 ----------------
_head "链路① 创作→生成→预览"
# 四模板就绪(执行器 banner 同源:getTemplateList 应返 clicker/merge/idle/tycoon)
out="$(api GET /app-api/aigc/template/list)"
tcount="$(printf '%s' "$out" | sed '$d' | python3 -c "import sys,json
try: d=json.load(sys.stdin); data=d.get('data') or []; print(len(data) if isinstance(data,list) else 0)
except: print(-1)" 2>/dev/null)"
if [ "${tcount:-0}" -ge 4 ] 2>/dev/null; then PASS=$((PASS+1)); _log " ${G}✓${N} aigc/template/list(四模板就绪 count=${tcount})"; else FAIL=$((FAIL+1)); FAILED_ITEMS+=("template/list count=$tcount"); _log " ${R}✗${N} aigc/template/list(期望≥4,实=${tcount})"; fi
check "studio/draft(创建草稿拿 gameId)" POST /app-api/studio/draft envelope_nonempty '{"title":"冒烟门-clicker","templateId":"clicker"}'
# ---------------- 链路② 发布 → 审核 → 游戏流 ----------------
_head "链路② 发布→审核→游戏流"
check "feed/stream(内容池非空)" GET "/app-api/feed/stream?size=5" envelope_nonempty
check "feed/zones(分区结构)" GET /app-api/feed/zones envelope
check "project/my(创作者项目区)" GET "/app-api/project/my?pageNo=1&pageSize=5" envelope
# ---------------- 链路③ 游戏流 → 试玩 → 互动 → 分享 ----------------
_head "链路③ 试玩→互动→分享"
# 真实取包走 GET /app-api/runtime/package/{versionId}(descriptor 含 entry/manifest/sandbox);
# 注意:feed item 的 packageUrl 字段恒为 null,**不是**取包入口(runtime.ts:25 实证)。
VID="$(api GET "/app-api/feed/stream?size=1" | sed '$d' | python3 -c "
import sys,json
try:
d=json.load(sys.stdin); lst=(d.get('data') or {}).get('list') or []
print(lst[0].get('versionId','') if lst else '')
except: print('')" 2>/dev/null)"
if [ -n "$VID" ] && [ "$VID" != "None" ]; then
check "runtime/package/{versionId}(试玩取包 vid=${VID})" GET "/app-api/runtime/package/$VID" envelope
else FAIL=$((FAIL+1)); FAILED_ITEMS+=("feed 首条无 versionId"); _log " ${R}✗${N} feed 首条无 versionId(无法验取包)"; fi
# ---------------- 链路④ 广告 → 收益 → 钱包 ----------------
_head "链路④ 广告→收益→钱包"
check "ad/slot/list-enabled(广告位)" GET /app-api/ad/slot/list-enabled envelope
check "trade/account/mine(钱包)" GET /app-api/trade/account/mine envelope
check "trade/income/page(收益流水)" GET "/app-api/trade/income/page?pageNo=1&pageSize=5" envelope
# ---------------- 链路⑤ 数据回路(遥测→质量分→feed 重排)----------------
_head "链路⑤ 数据回路(质量分回灌)"
# 结构断言:feed 首条携带 qualityScore(feed 排序由质量分驱动 = 回路已回灌)
QSCORE="$(api GET "/app-api/feed/stream?size=1" | sed '$d' | python3 -c "
import sys,json
try:
d=json.load(sys.stdin); lst=(d.get('data') or {}).get('list') or []
print(lst[0].get('qualityScore') if lst and lst[0].get('qualityScore') is not None else 'NONE')
except: print('NONE')" 2>/dev/null)"
if [ "$QSCORE" != "NONE" ] && [ -n "$QSCORE" ]; then PASS=$((PASS+1)); _log " ${G}✓${N} feed 携带 qualityScore=${QSCORE}(质量分回路已回灌)"; else FAIL=$((FAIL+1)); FAILED_ITEMS+=("feed 无 qualityScore"); _log " ${R}✗${N} feed 首条无 qualityScore"; fi
# ---------------- --deep:写路径(生成入队 + 遥测事件)----------------
if [ "$DEEP" = "1" ]; then
_head "深度(--deep)写路径:创作→生成入队"
# 真实创作链写路径:create_draft → studio_generate(契约 backend_gw.py:123/130)。
# generate body = {sessionId: draft.id, prompt}(注意 sessionId=draft 的 id 非 gameId;无 templateId,模板在 draft 时绑定)。
# 入队会触发真实执行器(poll=10s,约 18-27s 落包),本冒烟**只验入队 200/code=0**,不等生成完成。
# 不验 telemetry/interact 写:① 遥测回路产出已由链路⑤(feed 携 qualityScore)结构性验证;
# ② 完整深度 e2e(play→judge→publish)是 agent-loop orchestrator 批跑的职责,不与冒烟门重叠。
SID="$(api POST /app-api/studio/draft '{"title":"冒烟门-deep-idle","templateId":"idle"}' | sed '$d' | python3 -c "import sys,json
try: print((json.load(sys.stdin).get('data') or {}).get('id',''))
except: print('')" 2>/dev/null)"
if [ -n "$SID" ] && [ "$SID" != "None" ]; then
check "studio/generate(入队 sessionId=${SID})" POST /app-api/studio/generate envelope \
"{\"sessionId\":$SID,\"prompt\":\"一个挂机种树攒果实的放置小游戏\"}"
else FAIL=$((FAIL+1)); FAILED_ITEMS+=("deep: draft 无 id"); _log " ${R}✗${N} deep: draft 未拿到 sessionId(id)"; fi
fi
# ---------------- 总判 ----------------
_head "总判"
TOTAL=$((PASS+FAIL))
if [ "$FAIL" = "0" ]; then
_log "${G}SMOKE_PASS${N} $PASS/$TOTAL 全过"
echo "SMOKE_VERDICT=PASS"
exit 0
else
_log "${R}SMOKE_FAIL${N} $PASS/$TOTAL 过,$FAIL 项 FAIL:"
for it in "${FAILED_ITEMS[@]}"; do _log " ${R}-${N} $it"; done
echo "SMOKE_VERDICT=FAIL"
exit 1
fi