Files
opendance-windows/tasks/024-config-push-and-legacy-peer.md
zijunchiang e0ba4ea833 fix(ws): config 改为变更驱动(原每 2 秒无条件广播)+ 契约「可选/兜底」语义定向验收(v0.2.5)
一、缺陷:config 被当成心跳
_maybe_push_config() 原来每 CONFIG_PUSH_INTERVAL_MS(2 秒)无条件广播一次 config。
契约 §3.3 的语义是「运行中**调整**时下发」,不是心跳 —— 一次 30 分钟推流就是 900 条
没有任何信息量的消息;更糟的是它让「收到 config」无法被当作「配置变了」的信号。
实测 v0.2.4 产物:17 秒内收到 9 条。
修复:
  * 抽出 config_payload() 作为唯一载荷来源(同时用作「是否变化」的指纹);
  * hello 应答顺序固定为「先 welcome、再 config」(若靠周期 tick 补发,
    新对端第一条消息可能变成 config,只认握手应答的实现会困惑);
  * _maybe_push_config() 仅在载荷真的变化时广播,并用 CONFIG_PUSH_INTERVAL_MS 去抖;
  * 显式 broadcast_config()(UI 改设置)同步更新指纹,避免下一 tick 重发。
实测 v0.2.5:17 秒内 1 条。

二、契约「可选/兜底」语义的定向验收(沿用 Android 侧的教训:
   契约里可选的字段必须用**不提供**它的对端来测,否则等于没测)
新增 3 个场景(客户端断言 30 → 40 项):
  * v0.1.0 风格对端:只发二进制帧(每 500ms)、从不 ping/stats、hello 不带 deviceId,
    持续 17 秒(> 静默超时 15 秒)→ 连接必须保持(契约 §1.1 的向后兼容承诺);
  * 两条都不带 deviceId 的连接同时在线 → 不做去重、按连接计数(§5),
    device_replaced 只应有那一次真正重复 deviceId 的事件;
  * 完全不发 hello、只发 5 帧的对端 → 不断开、不崩、帧仍计入。

三、验收脚本自身修掉两处误判
  * 「跨连接的 frame_id 回退」被当成缺陷:frame_id 是**每条连接各自从 0 递增**,
    新连接重新开始完全合规 —— 指标用错了。已改名并标注「仅参考」,
    真正相关的判据是逐连接的 frames_dropped == 3;
  * 「静默超时次数」被当成固定值:套件里本来就有"完全不发数据"的对端,
    空闲的替换连接也会被正常超时。已改为**定向断言**:由 hello 记录建立
    client_id → device 映射(探针新增 hellos[]),只检查 v0.1.0 风格对端没被误踢。

四、测试客户端改为顺序无关
固定「welcome 之后紧跟 config」后,"第一条消息就是 welcome"的假设立刻暴露。
契约没规定消息顺序,客户端必须顺序无关 —— 已加 nextOfType(type) 按类型取消息
并跳过其它类型(同时计入 counts)。

实测(v0.2.5 发布包,四条验收全绿):
  * accept-release.sh:pck 无开发残留 / PE 版本资源 0.2.5 / pck 启动 180 帧 / 运行时探针
  * accept-ws.sh:客户端 40 通过 / 0 失败 + 服务端侧全部事实
    (10 次连接、111 帧、frames_dropped=3、malformed=1、device_replaced 仅 1 次、
     v0.1.0 风格对端未被静默超时踢掉)
  * accept-frame-vectors.sh:解码 12/12、396 点 0 差异、编码器 0 差异
  * verify.sh:785 通过 / 0 失败
2026-09-18 02:47:00 +08:00

68 lines
4.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-024:config 改为变更驱动 + 契约「可选/兜底」语义的定向验收
- **状态**: done
- **依赖**: T-WIN-020(WS 契约一致性验收)
- **文档**: v1.6 §6.5 / §7.6.1
- **契约**: `websocket-keypoints 0.2.0` §1.1 / §3.1 / §3.3 / §5
## 一、缺陷:config 被当成心跳周期性广播
`_maybe_push_config()` 原来每 `CONFIG_PUSH_INTERVAL_MS`(2 秒)**无条件**广播一次 `config`:
* 契约 §3.3 的语义是「**运行中调整**时下发」,不是心跳。一次 30 分钟推流 = **900 条**
没有任何信息量的消息;
* 更糟的是它让「收到 config」无法被当作「配置变了」的信号 —— 发送端想据此做点什么
就无从判断。实测(v0.2.4 产物):17 秒内收到 **9 条** config。
**修复**:
1. 抽出 `config_payload()` 作为唯一载荷来源(同时用作「是否变化」的指纹);
2. `hello` 应答时**顺序固定**为「先 `welcome`、再 `config`」—— 契约没规定顺序,
但若让周期 tick 补发,新对端的第一条消息可能变成 `config`,只认握手应答的实现会困惑;
3. `_maybe_push_config()` 只在**载荷真的变化**时广播,并用 `CONFIG_PUSH_INTERVAL_MS`
做去抖(连续拖动滑块不会每个中间值都发一条);
4. 显式 `broadcast_config()`(UI 改镜像/帧率时调用)也同步更新指纹,避免下一 tick 重发。
实测(v0.2.5 产物):17 秒内 **1 条**。
## 二、契约「可选/兜底」语义的定向验收(新增 3 个场景)
上一轮 Android 侧的教训是「契约里**可选**的字段必须用**不提供**它的对端来测,
否则等于没测」(自家两端总是互相迎合)。这轮把它用到接收端:
| 场景 | 契约条款 | 断言 |
|--|--|--|
| **v0.1.0 风格对端**:只发二进制帧(每 500ms 一帧)、**从不发 `ping`/`stats`**、`hello` **不带 `deviceId`**,持续 17 秒(> 静默超时 15 秒) | §1.1「仅实现 v0.1.0 的一端仍可正常互通(它不发 ping、不读 silentTimeoutMs,但每帧与每秒 stats 已足够保活)」 | 连接**保持打开**;服务端**没有**把它静默超时踢掉(靠 hello 记录建立 `client_id → device` 映射后定向核对) |
| **两条都不带 `deviceId`** 的连接同时在线 | §5「`deviceId` 缺失或为空 → 不做去重,按连接计数」 | 两条都保持打开,`device_replaced` **只有**那次真正重复 `deviceId` 的事件 |
| **完全不发 `hello`**、只发 5 帧的对端 | 契约未规定(不该崩、不该断) | 服务端不断开、不崩,帧仍被计入 |
## 三、验收脚本自身修掉的两处误判
1. **把"跨连接的 frame_id 回退"当成缺陷**:`frame_id` 是**每条连接各自从 0 递增**,
新连接重新从 0 开始完全合规。探针原来做的是全局计数,属**指标用错**。
已改名为 `frameIdRegressionsAcrossConnections` 并标明「仅参考」,
不再作为判定依据(真正相关的判据是逐连接的 `frames_dropped == 3`)。
2. **把"静默超时次数"当成固定值**:套件里本来就有一条专门"完全不发数据"的对端,
另外那条空闲的替换连接也会被正常超时 —— 全局计数会混入这些**预期**超时。
已改为**定向断言**:先由 hello 记录找出 v0.1.0 风格对端的 `client_id`,
再检查它**没有**被静默超时踢掉。
## 四、测试客户端也要"顺序无关"
固定「先 welcome 再 config」之后,`ws-client.mjs` 里"取到的第一条消息就是 welcome"
的假设暴露了:接收端在 `welcome` 之后紧跟 `config`,断言就会拿到 config。
契约没有规定消息顺序,**客户端必须顺序无关** —— 已加 `nextOfType(type)`,
按类型取消息并跳过其它类型(同时计入 `counts`)。
## 五、实测
```
== 结果:40 通过 / 0 失败 == # 原 30 项,新增 10 项
✓ 只发帧(每 500ms 一帧、共 34 帧)持续 17.0s 后连接仍打开
✓ 17 秒内 config 条数应在合理范围(收到 1 条;周期广播会是 ~8 条)
✓ 报告里应能识别出 v0.1.0 风格对端(client_id=[7])
✓ 只发二进制帧、从不 ping 的对端没有被静默超时踢掉(契约 §1.1:每帧同样算活动)
✓ 第一条仍在打开(未被去重顶掉)/✓ 第二条仍在打开
✓ 一个控制帧都没发过
服务端侧:10 次连接 / 111 帧 / frames_dropped=3 / malformed_frames=1 / device_replaced 仅 1 次
```