feat(integration): 跨端集成验收(包内客户端 × 真实 Web 服务端)+ 修两个真实缺陷(v0.2.3)

单测打的是假 HTTP 夹具,Web 端测试只验自己那一侧 —— 两端各自绿灯不代表契约字段
真的对得上。本版加了跨端集成验收,并在首次运行就抓到两个真实缺陷。

一、跨端集成验收(T-WIN-019)
  * build/integration/probe-integration.gd:只加载**包内**代码
    (`godot --main-pack <pck> --script <探针>` + res://),打真实服务端并逐项断言:
    /health 六个契约版本、catalog/version 指纹、catalog/export 解析出的四类资源与
    defaultModelId;**场景物理参数逐项比对**(重力/基础风/阵风/风坐标系/地面摩擦坡度/
    碰撞体/出生点/相机/环境)+ Wind.sample(0) + 场景 GLB 可下载;
    动作 NDJSON → 客户端解析帧数 == 服务端帧数、33 点、每帧服务端关节角、
    推导帧率 30 fps、时长 96.67 s;缩略图 PNG;模型 VRM 可下载。
  * build/integration/seed-catalog.mjs:用**真实素材**播种目录
    (示例文件/ 下的真 VMD → 内部标准 33 点;真 PMX → VRM;场景 GLB + 物理参数),
    并把期望值(含从 /health 取的契约版本)写成 JSON。
  * build/accept-integration.sh:编排「服务端可达 → 播种 → 包内探针 → 汇报」。
  实测(v0.2.3 包内 pck × 真实 Web v0.7.4 + 真实 MySQL):全部通过。

二、探针抓到的两个真实缺陷
  1. `CatalogTypes.Motion` 缺 `thumbnail_url`:服务端 catalog/export 从
     data-models 0.6.0 起一直在发 thumbnailUrl(Model/Scene 都有),客户端默默丢掉。
     已补字段与解析(缺失为 ""),新增 tests/test_catalog_types.gd(4 项 20 断言)
     用**服务端真实报文形状**锁住三个资源类型的覆盖,并显式记录一处刻意不消费的差异
     (Model.usable/unusableReason —— 目录只下发可用模型,恒为 true)。
  2. `ApiClient._request()` 对**所有**响应无条件 `raw.get_string_from_utf8()`:
     数据本身没错(二进制路径返回原始字节),但每下载一个文件都会打印
     「Unicode parsing error … Invalid UTF-8 leading byte (N)」噪声,并把整个文件
     (模型可达数十 MB)解码成 String 拷贝后丢弃。改为只在需要文本时解码。

三、顺带修掉测试里的写死契约版本
  tests/test_api_integration.gd 断言 rest-api/websocket-keypoints/skeleton-topology
  均为 "0.1.0"。该用例只在服务端可达时才跑,平时一直 skip,所以契约升到 0.5.0 后
  没有任何地方发现它。改为从仓库 contracts/VERSION 读期望值并与 /health 比对
  (顺带成为「客户端契约快照 vs 服务端」的跨端校验)。

证据:verify.sh 全绿(820 通过 / 0 失败,比上版 +46,因为服务端在线时 API 集成组
真的跑了);包内 pck 跨端集成探针全部通过;check-version/PE 版本资源 = 0.2.3。
This commit is contained in:
2026-09-17 05:18:03 +08:00
parent e4e534f2a2
commit a78a536476
15 changed files with 889 additions and 23 deletions
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# ============================================================
# OpenDance Studio Windows 客户端 —— **跨端集成验收**(T-WIN-019)
#
# Windows 发布包里的客户端(包内 pck) × 真实 Web 服务端
#
# 为什么需要它:本仓单测打的是**假 HTTP 夹具**,Web 端测试只验自己那一侧。
# 两端各自绿灯 ≠ 契约字段真的对得上(字段名/嵌套/null/单位/范围都可能一侧对一侧错)。
# 契约 0.5.0 新增的 `scenes[]` 与重力/风向参数尤其如此。
#
# 本脚本:
# 1. 要求一个**已部署好的** Web 服务端(不做部署,与项目边界一致);
# 2. 用真实素材播种一份目录(真 VMD → 内部标准;真 PMX → VRM;场景 GLB + 物理参数),
# 并写出期望值 JSON(`--skip-seed` 可跳过,复用已有目录);
# 3. 用 Godot 加载**发布包里的 pck**(`--main-pack`)跑
# `build/integration/probe-integration.gd`,逐项断言并给出退出码。
#
# 用法:
# GODOT_BIN=/path/to/godot API_BASE=http://127.0.0.1:3000 \
# bash build/accept-integration.sh dist-release/opendance-windows-v0.2.3.zip
#
# # 已播种过、只重跑探针:
# GODOT_BIN=... API_BASE=... bash build/accept-integration.sh <zip> --skip-seed
#
# 环境变量:
# API_BASE Web 服务端基地址(默认 http://127.0.0.1:3000)
# OPENDANCE_SAMPLES_DIR 真实素材目录(默认 仓库根/示例文件)
# EXPECT_PATH 期望值 JSON 输出路径(默认 $WORK_DIR/catalog-expect.json)
# WORK_DIR 解包目录(默认 mktemp -d)
# ============================================================
set -uo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
TARGET="${1:-}"
SKIP_SEED=0
for arg in "$@"; do [ "$arg" = "--skip-seed" ] && SKIP_SEED=1; done
API_BASE="${API_BASE:-http://127.0.0.1:3000}"
SAMPLES_DIR="${OPENDANCE_SAMPLES_DIR:-$PROJECT_DIR/../示例文件}"
WORK_DIR="${WORK_DIR:-$(mktemp -d)}"
EXPECT_PATH="${EXPECT_PATH:-$WORK_DIR/catalog-expect.json}"
FAILED=0
log() { printf '\033[1;34m[accept-int]\033[0m %s\n' "$*"; }
fail() { printf '\033[1;31m[accept-int] ✗ %s\033[0m\n' "$*" >&2; FAILED=1; }
if [ -z "$TARGET" ] || [ ! -e "$TARGET" ]; then
echo "用法:GODOT_BIN=... API_BASE=... bash build/accept-integration.sh <发布包 zip 或已解压目录> [--skip-seed]" >&2
exit 2
fi
# 必须**先**建目录:期望值 JSON 默认就落在 $WORK_DIR 下,播种阶段就要写它
mkdir -p "$WORK_DIR"
# ---- 0. 服务端可达性 ----
log "服务端:$API_BASE"
HEALTH="$(curl -sf -m 10 "$API_BASE/health" || true)"
if [ -z "$HEALTH" ]; then
fail "GET $API_BASE/health 不可达 —— 请先部署 Web 服务端(本项目不部署它)"
exit 1
fi
echo "$HEALTH" | node -e '
let s = ""; process.stdin.on("data", (d) => (s += d)).on("end", () => {
const j = JSON.parse(s);
console.log(` ✓ 服务端版本 ${j.version}|数据库 ${j.database?.connected ? "已连接" : "未连接"}|scenes.available=${j.scenes?.available}`);
});'
# ---- 1. 播种目录 ----
if [ "$SKIP_SEED" = "1" ]; then
log "跳过播种(--skip-seed);期望值文件:$EXPECT_PATH"
else
log "播种真实目录(真 VMD / 真 PMX / 场景 GLB + 物理参数)…"
if [ ! -d "$SAMPLES_DIR" ]; then
fail "找不到素材目录 $SAMPLES_DIR(用 OPENDANCE_SAMPLES_DIR 指定)"
exit 1
fi
if ! command -v node >/dev/null 2>&1; then
fail "播种需要 node(Web 端已依赖 node,本机应已安装)"
exit 1
fi
API_BASE="$API_BASE" OPENDANCE_SAMPLES_DIR="$SAMPLES_DIR" EXPECT_PATH="$EXPECT_PATH" \
node "$PROJECT_DIR/build/integration/seed-catalog.mjs" || { fail "播种失败"; exit 1; }
fi
if [ ! -f "$EXPECT_PATH" ]; then
fail "缺少期望值文件 $EXPECT_PATH(首次运行不要加 --skip-seed)"
exit 1
fi
# ---- 2. 取包内 pck ----
if [ -f "$TARGET" ]; then
log "解压发布包…"
if ! unzip -qq -o "$TARGET" -d "$WORK_DIR" 2>/dev/null; then
command -v 7z >/dev/null 2>&1 && 7z x -y -o"$WORK_DIR" "$TARGET" >/dev/null || { fail "解压失败"; exit 1; }
fi
PACKAGE_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
else
PACKAGE_DIR="$TARGET"
fi
PCK="$PACKAGE_DIR/OpenDance.pck"
[ -f "$PCK" ] || { fail "找不到包内 pck:$PCK"; exit 1; }
log "包内 pck:$PCK($(stat -c%s "$PCK") 字节)"
# ---- 3. Godot ----
GODOT_BIN="${GODOT_BIN:-}"
if [ -z "$GODOT_BIN" ]; then
for candidate in godot godot4 Godot; do
command -v "$candidate" >/dev/null 2>&1 && { GODOT_BIN="$candidate"; break; }
done
fi
[ -n "$GODOT_BIN" ] || { fail "未找到 Godot(用 GODOT_BIN=<路径> 指定,需与本项目同版本 4.7.x)"; exit 1; }
# ---- 4. 跨端探针 ----
log "跨端集成探针(包内客户端 × $API_BASE)…"
PROBE_LOG="$WORK_DIR/probe-integration.log"
if timeout 600 "$GODOT_BIN" --headless --main-pack "$PCK" \
--script "$PROJECT_DIR/build/integration/probe-integration.gd" \
-- --api-base "$API_BASE" --expect "$EXPECT_PATH" >"$PROBE_LOG" 2>&1; then
PROBE_OK=1
else
PROBE_OK=0
fi
grep -vE "^Godot Engine|^$" "$PROBE_LOG" | sed 's/^/ /'
[ "$PROBE_OK" = 1 ] || fail "跨端集成探针未通过"
echo ""
if [ "$FAILED" -ne 0 ]; then
echo "============================================================"
echo " 跨端集成验收失败(工作目录:$WORK_DIR)"
echo "============================================================"
exit 1
fi
echo "============================================================"
echo " 跨端集成验收全部通过(工作目录:$WORK_DIR)"
echo "============================================================"