Files
opendance-windows/tasks/019-cross-end-integration.md
T
zijunchiang a78a536476 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。
2026-09-17 05:18:03 +08:00

5.4 KiB
Raw Permalink Blame History

T-WIN-019:跨端集成验收(包内客户端 × 真实 Web 服务端)

  • 状态: done
  • 依赖: T-WIN-013(场景选择)/ T-WIN-018(发布包级验收)/ T-WEB-015(场景管理)
  • 文档: v1.6 §5.6 / §7.5
  • 契约: rest-api 0.5.0、data-models 0.7.0、skeleton-topology 0.2.0

为什么需要

本仓的单测用的是假 HTTP 夹具(tests/test_api_integration.gd 打本地小服务), Web 端的测试只验自己那一侧。两端各自绿灯 ≠ 契约字段真的对得上: 字段名、嵌套层级、null 与缺字段、单位(米/度/秒)、归一化范围, 都可能在一侧对、另一侧错。契约 0.5.0 新增的 scenes[] 与物理参数尤其如此。

所以本任务是:拿发布包里的客户端(包内 pck)打真实服务端。

发现并修复的缺陷

探针首次运行就抓到一个真实缺口:

服务端 GET /api/catalog/export 的 motion 条目(契约 data-models 0.6.0 起):
  ...,"keypointUrl":"...","thumbnailUrl":"/static/motion-thumbnail/.../x.png","keypointFrames":2901,...
客户端的 CatalogTypes.Motion:
  ✗ 没有 thumbnail_url 字段(服务端一直在发,客户端默默丢掉)
而同一文件的 Model / Scene 都有 thumbnail_url。

修复:src/net/catalog_types.gd 的 Motion 增加 thumbnail_url 并解析 thumbnailUrl(缺失时为 "",兼容旧服务端);新增 tests/test_catalog_types.gd(4 项 / 20 条断言)用服务端真实报文形状锁住 「三个资源类型都消费 thumbnailUrl」,并显式记录一处刻意不消费的差异: Model.usable / unusableReason —— 它们是 Web 端审核用的, 而 catalog/export 只下发可用模型(目录里恒为 true), 不可用模型的可见性由 Web 管理端负责。

实现

文件 作用
build/integration/probe-integration.gd 跨端集成探针:只加载包内代码(--main-pack + res://),打真实服务端并逐项断言
build/integration/seed-catalog.mjs 通过真实 Web API 播种一份真实目录(真 VMD → 内部标准;真 PMX → VRM;场景 GLB + 物理参数),并写出期望值 JSON
build/accept-integration.sh 编排:检查服务端可达 → 播种(可跳过)→ 对包内 pck 跑探针 → 汇报

探针断言(对应需求编号)

需求 断言
⑤ API 升级 /health 连通 + 六个契约版本与 Web 一致;/api/catalog/version 指纹非空;catalog/export 四类资源齐备;defaultModelId 解析为默认模型 id
⑤ 场景选择 + 物理参数落地 逐项比对类型化后的场景参数:重力 y、基础风 x/z、阵风幅度/频率、风坐标系、地面摩擦/坡度、碰撞体模式、出生点 yaw、相机 mode/distance/height、环境光与雾;另外校验 Wind.sample(0) 等于基础风;场景 GLB 可下载且魔数与 fileSize 一致
② 动作内部标准 keypointFormat == mediapipe33@0.1.0、服务端帧数、is_scorable();拉取 NDJSON 后客户端解析帧数 == 服务端帧数、skipped_lines == 0、首帧 33 点、每帧带服务端关节角、时间轴推导帧率 ≈ 30 fps、时长 ≈ 96.7 s
③ 动作预览(资产链路) 动作缩略图 可下载且为 PNG
① 模型交付 模型格式 vrm、isPrimary、模型文件可下载且为 glTF 容器、大小与 fileSize 一致

实测(v0.2.3 包内 pck × 真实 Web v0.7.4 + 真实 MySQL)

[integration] Windows 包内客户端 × 真实 Web 服务端:http://127.0.0.1:3100
  ✓ GET /health 200(服务端版本 0.7.4);契约 6 项全部一致
  ✓ 目录指纹非空;目录内嵌契约版本与 /health 一致
  ✓ 目录四类资源齐备:songs=1 motions=1 models=1 scenes=1
  ✓ defaultModelId 解析为 1
  ✓ 场景 glbUrl/fileSize;重力 y=-3.50;基础风 (2.0,0.0,-1.0);阵风 4.0 m/s @ 0.50 Hz
  ✓ 风坐标系 world;地面摩擦/坡度 0.80/30°;碰撞体 mesh;出生点 yaw 90°
  ✓ 相机 orbit/6.0/2.0;环境光 2.0/雾 0.20;风采样 t=0 等于基础风
  ✓ 场景 GLB 可下载且魔数正确(glTF,1153244 字节)
  ✓ 动作内部标准 mediapipe33@0.1.0;客户端解析帧数 2901 == 服务端 2901
  ✓ 无解析失败行;首帧 33 点;每帧带服务端关节角;30.00 fps;96.67 s
  ✓ 动作缩略图可下载且为 PNG
  ✓ 模型格式 vrm、默认模型、文件可下载且 glTF 容器
[integration] ✓ 跨端集成探针全部通过

真实素材

播种用的都是仓库根 示例文件/ 下的真实素材(不是编造的): 【动作】霜雪千年.vmd(827,973 B → 2901 帧内部标准)、 模型_初音未来Hatsune Miku_by_Boku_Wa_Luka.zip(1.0 MB → VRM,humanoid 19/22、drivable)。

踩坑记录

  1. autoload 单例在 --script 模式下不是全局标识符: AppConfig / CatalogStore 直接当名字用会 Compile Error: Identifier not found (脚本来自项目外,编译期不认这两个 autoload 名)。改为 root.get_node_or_null("AppConfig") 取节点后动态调用。
  2. 协程里一处运行时错误会让进程永不退出:_finish() 不会被调用, 表现为「无输出卡住」,很难定位。已加 120 秒看门狗:超时判失败并退出。
  3. 输出缓冲:print() 管道输出在进程被 SIGTERM 杀死时会丢, 调试这类脚本要重定向到文件而不是 | head。