Files
opendance-android/tasks/016-reconnect-integration.md
zijunchiang 6dfd90f7ca fix(reconnect): 上一次会话的关闭码污染下一次会话(v0.2.1)+ 重连链路的真实 WebSocket 集成测试
ReconnectPolicyTest(11 项)验的是**纯函数**「给定关闭码应该怎么退避」,而 ⑦
「连接频繁重连」的缺陷往往出在**调用点**。新增 PoseWebSocketReconnectTest(8 项):
用 MockWebServer.withWebSocketUpgrade 在 JVM 上起**真实** WebSocket 服务端
(能按 §3.2 回 welcome、按 §3.6 回 pong、按指定关闭码关连接),无需设备即可覆盖
「该重连的」与「不该重连的」两条分支。Robolectric 只用来提供 android.os.Build 桩。

它抓到的真实缺陷(本版修复):
  上一次会话的关闭码会污染下一次会话,导致重新开始推流后失去自动重连能力。
  复现:第一次连接被对端以 1001 关闭(按 §7 不重连,正确)→ 用户再点「开始推流」
  → 这一次是网络层失败(服务端还没起来),按 §7 **应当**重连 → 但 connect() 只重置了
  reconnectCounter 与 lastPongAtMs,**没有清 lastCloseCode** → scheduleReconnect()
  仍按 1001 判定 → 直接 stopped,**再也不会自动重连**。
  测试失败信息(修复前):
    ✗ 上一次 1001 关闭后重新连接,遇到网络失败仍应按策略重连
      AssertionError: 当前 state=STOPPED,lastCloseCode=1001,attempts=0

修复(PoseWebSocketClient.kt):
  * connect():清掉会话级关闭状态(lastCloseCode=0、lastCloseReason="");
  * onFailure():记录 recordClose(0, …)(契约 §7 中 0=未收到关闭帧 → 应重连),
    顺带让界面「最近一次断开原因」显示本次失败而不是旧值。

用例清单:握手→streaming 与 welcome 保活参数解析、按协商间隔发 ping 收 pong、
1011 按退避重连且等待落在策略 ±20% 区间、成功后计数清零、1001/1002 不重连、
主动断开不重连、上一次 1001 后再连遇网络失败仍应重连(本次回归锁)、
服务端未启动时 1.5s 内 ≤3 次尝试(退避生效)。

实测:./gradlew :app:testDebugUnitTest → 6 个测试类 / 64 项用例 / 0 失败
(原 56 项);build/verify.sh 全绿(签名、APK 版本元数据 0.2.1/201、native 库体检、
ABI、R8、dex)。版本号 0.2.0/200 → 0.2.1/201。

踩坑:第一版 MockWebServer 夹具的 101 响应没有回显 Sec-WebSocket-Protocol
(RFC 6455 要求),客户端记录的 negotiatedProtocol 为空导致断言失败 —— 那是
**夹具**不合规而非客户端问题,已在夹具里补上。写「真实对端」夹具时要按 RFC/契约
把服务端做对,否则会把夹具缺陷误判成产品缺陷。
2026-09-17 05:44:03 +08:00

84 lines
4.3 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-AND-016:重连链路的 JVM 集成测试(真实 WebSocket)+ 修一处真实缺陷
- **状态**: done
- **依赖**: T-AND-011(连接状态机与重连)
- **文档**: v1.6 §6.5 / §7.6
- **契约**: `websocket-keypoints 0.2.0` §7 / §7.1 / §8
## 为什么需要
`ReconnectPolicyTest`(11 项)验的是**纯函数**「给定关闭码应该怎么退避」;
但 ⑦「连接频繁重连」的缺陷往往出在**调用点**:关闭码有没有被正确记录、
重连计数有没有在成功后清零、上一次的状态会不会污染下一次会话。
这些只有把**真实的 OkHttp WebSocket 接到真实服务端**上才能验出来。
因此新增 `PoseWebSocketReconnectTest`(8 项):用 `MockWebServer.withWebSocketUpgrade`
在 JVM 上起**真实** WebSocket 服务端(能按 §3.2 回 `welcome`、按 §3.6 回 `pong`、
按指定关闭码关连接),无需设备即可覆盖两条分支:**该重连的**与**不该重连的**。
Robolectric 只用来提供 `android.os.Build.*` 桩。
## 它抓到的真实缺陷(本版修复)
**上一次会话的关闭码会污染下一次会话,导致新版一启动就失去自动重连能力。**
复现路径:
1. 第一次连接被对端以 `1001` 关闭 → `lastCloseCode = 1001`,按契约 §7 **不重连**(正确);
2. 用户再次点「开始推流」→ `connect()`;
3. 这一次是**网络层失败**(服务端还没起来 / 被拒),按 §7 **应当**重连;
4. 但 `connect()` 只重置了 `reconnectCounter` 与 `lastPongAtMs`,**没有清 `lastCloseCode`**;
`scheduleReconnect()` 仍按 1001 判定 → 直接 `stopped`,**再也不会自动重连**。
测试输出的失败信息(修复前):
```
✗ 上一次 1001 关闭后重新连接,遇到网络失败仍应按策略重连
java.lang.AssertionError: 上一次的 1001 不应影响本次会话:网络失败后必须按 §7 重连
(当前 state=STOPPED,lastCloseCode=1001,attempts=0)
```
**修复**(`PoseWebSocketClient.kt`):
| 位置 | 改动 |
|--|--|
| `connect()` | 清掉会话级关闭状态:`lastCloseCode = 0`、`lastCloseReason = ""`(每次新会话都从干净状态开始) |
| `onFailure()` | 记录 `recordClose(0, …)`(契约 §7 中 0 = 未收到关闭帧 → 应重连),同时让界面「最近一次断开原因」显示**本次**失败而不是旧值 |
## 用例清单(8 项)
| 用例 | 验的是什么 |
|--|--|
| 握手成功 → `streaming`,解析 `welcome` 的 `sessionId` / 保活参数,`hello` 带 `deviceId` | §3.1 / §3.2 发送端一侧 |
| 按协商间隔发送应用层 `ping` 并收到 `pong` | §1.1 / §3.6(协议层 ping 不能替代) |
| 异常关闭码 `1011` → 按退避重连、等待时间落在策略区间、成功后计数清零 | §7 / §7.1 / §8 |
| `1001` 对端离开 → `stopped` 且**不重连**,日志记录「不重连」 | §7 |
| `1002` 协议错误 → **不重连** | §7 |
| 用户主动 `disconnect()` → **不重连** | §7 |
| **上一次 1001 后再连、遇网络失败仍应重连** | 本次修复的缺陷(回归锁) |
| 服务端未启动时的连续尝试遵守退避(1.5 s 内 ≤3 次) | §7.1「不会疯狂重连」 |
## 实测
```
$ ./gradlew :app:testDebugUnitTest
BUILD SUCCESSFUL
测试类 6 个,用例 64 项,失败 0,跳过 0
(新增 8 项;修复前该项失败,其余 7 项通过)
```
`build/verify.sh` 全绿(含签名、版本元数据、native 库体检、ABI、R8、dex)。
## 踩坑记录
**夹具也要合规**:第一版 MockWebServer 的 101 响应没有回显
`Sec-WebSocket-Protocol`(RFC 6455 要求服务端带上选中的子协议),于是客户端记录的
`negotiatedProtocol` 是空串,断言失败。那是**夹具**不合规,不是客户端问题 ——
已在夹具里补上 `setHeader("Sec-WebSocket-Protocol", …)`。
写这类"真实对端"夹具时,要按 RFC / 契约把服务端做对,否则会把夹具缺陷误判成产品缺陷。
## 未覆盖(诚实声明)
- 真机 Wi-Fi 抖动、切网、进程被杀、Doze 省电等**真实断网场景**仍需设备实测;
- 契约 §7.1 的**封顶 15 s** 行为由纯函数单测覆盖(穷举 1..8 次),
集成测试只验证「等待时间取自策略且落在 ±20% 区间」,否则单测要跑 30 秒以上。