# 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 秒以上。