lili e200bc4577
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
fix(smoke): --deep 写路径改便宜档真实流(空 templateId→归一 generic 派 worker)
原 --deep draft 用 templateId=idle,撞 AigcGenerateExecutor:535-536 硬相等分派闸
(仅 templateId=='generic' 投 worker),fall-through 到进程内 LLM 旧路、到不了 :9501,
冒烟测的是废弃路径=假绿。改为不设 templateId(复刻真实「一句话不选模板」流:
后端归一 generic→派外置 worker),并更新陈旧注释(旧 poll=10s 落包→executor 派发 worker)。
R5 grounding 真 e2e 坐实(闭环-fix),配 staging-ops §8。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 11:23:50 -07:00

183 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
# EXTRAenvelope_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/metoken 有效)" 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 首条携带 qualityScorefeed 排序由质量分驱动 = 回路已回灌)
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
# draft 不设 templateId便宜档真实流「一句话不选模板」→ 后端归一 generic → 分派闸templateId=='generic')投外置 worker :9501
# 旧四模板名idle/clicker 等会撞硬相等分派闸、fall-through 到进程内 LLM 旧路、到不了 worker见 staging-ops §8
# 入队触发 executor 派发 worker异步产包经回调落版本本冒烟**只验入队 200/code=0**,不等生成完成。
# 不验 telemetry/interact 写:① 遥测回路产出已由链路⑤(feed 携 qualityScore)结构性验证;
# ② 完整深度 e2eplay→judge→publish是 agent-loop orchestrator 批跑的职责,不与冒烟门重叠。
SID="$(api POST /app-api/studio/draft '{"title":"冒烟门-deep-generic"}' | 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