Files
opendance-windows/tasks/019-cross-end-integration.md
zijunchiang cc8b6ba729 test(integration): 跨端验收补两类契约边界(不可用模型 / 场景 config 缺字段)
正常路径之外的**字段边界**同样只在跨端跑起来才暴露。配合 Web v0.7.5 增加:

1. 不可用模型:用 Assimp 把真实 PMX 转成 GLB 后入库(等价"管线只产出 GLB"),
   Web 侧断言 usable=false、原因准确(骨架达标时不得说"humanoid 不达标")、
   不在 catalog/export、defaultModelId 不变;Windows 侧(包内探针)断言
   bundle.models 里没有任何非 VRM 模型、按 id 找不到该模型、defaultModelId 仍指向可用 VRM。
2. 场景 config 只给 {physics:{gravity:{y:-9.8}}}:Web 侧断言服务端补齐默认
   (碰撞体 auto、地面 0.6/45°、静风、相机 follow、环境光 1、出生点 1 条);
   Windows 侧断言客户端解析出的**同一组默认值**逐项一致。

实测(Web v0.7.5 × Windows v0.2.5 包内 pck,真实 MySQL):
  ✓ usable=false 且原因为「未产出 VRM(格式为 GLB;humanoid 覆盖 19/22 已达标)…」
  ✓ 目录里的模型全是 VRM/不可用模型 #2 不在目录里/defaultModelId 仍为 1
  ✓ 最简配置:重力 -9.8 保留,碰撞体 auto、地面 0.6/45°、静风、相机 follow、
    环境光 1.0、出生点 1 条,两端逐项一致
跨端集成验收全部通过。

顺带修掉播种脚本自身的顺序错误(把依赖 catalog 的断言插到了 catalog 取回之前),
以及把断言改成「先取目录再断言」。不影响发布产物(build/ 已排除出 pck)。
2026-09-18 03:05:51 +08:00

116 lines
7.4 KiB
Markdown
Raw Permalink 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.
# 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` 一致 |
## 契约边界场景(续,配合 Web v0.7.5)
正常路径之外的**字段边界**同样只在跨端跑起来才会暴露。`seed-catalog.mjs` 与
`probe-integration.gd` 因此增加两类场景,**两端都断言**:
| 场景 | Web 侧(播种脚本断言) | Windows 侧(包内探针断言) |
|--|--|--|
| **不可用模型**:真实 PMX 用 Assimp 转 GLB 后入库(等价"管线只产出 GLB") | `usable=false`;原因准确(骨架达标时**不得**说"humanoid 不达标");**不在** `catalog/export`;`defaultModelId` 不变 | `bundle.models` 里没有任何非 VRM 模型;按 id 找不到该模型;`defaultModelId` 仍指向可用的 VRM |
| **场景 config 只给最少字段**(`{physics:{gravity:{y:-9.8}}}`) | 服务端补齐默认(碰撞体 `auto`、地面 `0.6/45°`、静风、相机 `follow`、环境光 `1`、出生点 1 条) | 客户端解析出的**同一组默认值**逐项一致 |
实测(Web v0.7.5 × Windows v0.2.5 包内 pck):
```
✓ 该模型标记为不可用(usable=false)
✓ 不可用原因说明"未产出 VRM":未产出 VRM(格式为 GLB;humanoid 覆盖 19/22 已达标),需重新以 VRM 交付才能进入客户端目录
✓ 骨架达标(drivable=true,humanoid 19/22)时不得归因于 humanoid
✓ 目录里的模型全是 VRM(不可用模型不进目录)/✓ 不可用模型 #2 不在客户端目录里
✓ defaultModelId 仍是可用的 VRM 模型(实际 1,期望 1)
✓ 最简配置:重力保留 -9.8 / 碰撞体补默认 auto / 地面默认 0.60 / 45°
✓ 最简配置:风默认静风(is_calm=true)/ 相机默认 follow / 环境光默认 1.0 / 出生点 1 条
```
> 这两类场景的价值在**同一份契约的两侧解释**:Web 认为"不可用"的模型长什么样、
> 客户端是否真的看不到它;以及"缺字段"时两端的默认值是否逐项一致
> (服务端补齐 vs 客户端收敛,任一侧不一致都会表现为"配置看起来生效了但行为不对")。
## 实测(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`。