test(probe)+docs: W44 判定 W41 缺口——A(把 503 判成 Offline)已否;B 只到形态(那个 Unreachable 生在连接之前,三种成因在外面同形,需一次设备取证)
判定 A:否。ChatSocket.isAuthFailure 只认 HTTP 401(ChatSocket.kt:320-323),非 101 的握手响应
(代理回 503)走 SessionEnd.Closed ⇒ Reconnecting 退避重连(ChatSocket.kt:301-304 / 230-235);
而顶栏「重连中…」正是 Reconnecting 的文案(ChatScreen.kt:918,Offline 那档是「离线」)。
⇒ 那一刻 socket 真值就是 Reconnecting,sendRouteFor 在这一档会看探测结论,镜像也没滞后。
判定 B:形态成立,但「这条链根本发不出请求」不成立。
- 调用点是无条件求值的实参(ChatViewModel.kt:1614)⇒ Queued 这一档必然调用探测,
连 Offline 那档也会(sendRouteFor 只是把结论丢掉,与本轮前的 KDoc 说法相反,已改)。
- 新增 ProbeClientTest(6 例,真 OkHttp + 真 Retrofit + JDK HttpServer):这条链能在真 socket 上
留下 GET /api/v1/health、不带 Authorization、200 ⇒ Reachable、503 ⇒ Unreachable、
2xx 但回执解析不出来 ⇒ Unreachable、黑洞服务端在 2.5 秒整通上限处失败
⇒「探测压根发不出去」不是代码事实。
- 真机代理日志里一条 health 都没有 ⇒ 那个 Unreachable 生在连接之前。三种成因在外面完全同形:
(甲) 建 client 就抛(baseUrl 空/非法,Retrofit.baseUrl 直接抛);
(乙) 连上了但一个字都没写(callTimeout 是整通上限含连接,而探测不复用连接池 ⇒ 每次冷握手,
预算只有 2.5 秒,比共用客户端的 10 秒连接预算紧 4 倍);
(丙) 请求出去了、回执非 2xx(只有这一档在对端留痕)。
⇒「代理日志没有 health 那一行」推不出「App 没探过」;(甲)(乙) 都不留痕。
本轮改动(除一处 DEBUG 日志外全是文档与测试,无行为变化):
- 新增 app/src/test/kotlin/top/yeij/cyrene/data/api/ProbeClientTest.kt(6 例)。
- ApiClientFactory.createProbe:DEBUG 下挂 BASIC 日志(与通用 create 同一约定)。这是本轮要的
诊断仪——--> 打在连接之前,让「到底有没有探」与「死在连接还是死在回执」当场可分;探测不带令牌、
没有请求体,日志不涉凭据。
- KDoc 修正「连探测都不做」这处失真(SendFallback 的 sendRouteFor、ChatViewModel 的调用点与
probeReachability),把「三种成因 + logcat 判据」写进 ConnectivityRepository 类注释,
并给 WsState.Offline 补上「非 401 的握手失败不属于这一档」。
- sendRouteFor 的 fail-closed、探测的 2.5 秒上限、三档路由一个字都没改;下一步要设备取证,
步骤写在 devdocs/05 的 W44 一节。
验证:assembleDebug 成功;testDebugUnitTest 1156 例 / 0 失败 / 0 错误(新增 6 例全绿)。
This commit is contained in:
1 parent
866a3c37cd
commit
06028cc352
7 files changed
+322
-3
No files matched your search
@@ -1076,3 +1076,33 @@ curl -s -X PATCH "$BASE/api/v1/sessions/$SID" -H "Authorization: Bearer $TOK" \
|
||||
> ### 另外两轮的失败原因(免得下次又猜)
|
||||
> 第 1、2 轮之所以「什么都没发生」:**前台根本不是我们的 App,而是 `com.android.settings/.SubSettings`**(`dumpsys window` 看出来的)。所以我的点击全落在设置页上。
|
||||
> ⇒ 教训升级:**不只要确认输入框聚焦,还要先确认前台就是我们的 App**(`dumpsys window | grep mCurrentFocus`)。
|
||||
|
||||
> ## W44(2026-10-06 · W41 那个「缺口」的判定:A 已否,B 只到形态,根因要一次设备取证)
|
||||
>
|
||||
> 分支 `feat/W44-probe-diagnosis`(基线 `866a3c3`)。本轮**没动任何语义**:`sendRouteFor` 的 fail-closed、探测的 2.5 秒上限、三档路由一个字都没改——判定的结论是「代码这条路是对的,失败生在连接阶段」,而那一档在本机看不出成因。
|
||||
>
|
||||
> ### 1. 假设 A(503 被判成 Offline、界面镜像滞后)——**否**
|
||||
> `ChatSocket.isAuthFailure`(`app/src/main/kotlin/top/yeij/cyrene/data/ws/ChatSocket.kt:320-323`)**只认 `response.code == 401`**(拿不到 response 时才退化成消息文本含 401);非 101 的握手响应(代理回 503)落到 `SessionEnd.Closed` ⇒ `Reconnecting` + 退避重连(同文件 `:301-304`、`:230-235`)。而顶栏副标题「重连中…」正是 `WsState.Reconnecting` 的文案(`ui/chat/ChatScreen.kt:918`,`Offline` 那档是「离线」)。
|
||||
> ⇒ 那一刻 socket 的真值就是 `Reconnecting`,`sendRouteFor` 在这一档**会**看探测结论,界面镜像也没有滞后。A 不成立。
|
||||
>
|
||||
> ### 2. 假设 B ——**形态成立,但「这条链根本发不出请求」不成立**
|
||||
> - 调用点是**无条件求值的实参**:`sendRouteFor(wsDispatched = false, connection = state, reachability = probeReachability())`(`ui/chat/ChatViewModel.kt:1614`)⇒ 只要走到 `WsSendOutcome.Queued`,探测**一定被调用**(连 `Offline` 那一档也会,`sendRouteFor` 只是把结论丢掉——这一点与本轮前的 KDoc 说法相反,已改)。
|
||||
> - 新增 `app/src/test/kotlin/top/yeij/cyrene/data/api/ProbeClientTest.kt`(6 例,真 OkHttp + 真 Retrofit + JDK `HttpServer`,与 `ChatRepositoryTest` 同一手法):证实这条链**能**在真 socket 上留下 `GET /api/v1/health`、**不带 `Authorization`**、200 ⇒ `Reachable`、503 ⇒ `Unreachable`、2xx 但回执解析不出来 ⇒ `Unreachable`、黑洞服务端在 2.5 秒上限处失败。⇒ 「探测压根发不出去」不是代码事实。
|
||||
> - 但真机代理日志里一条 `/api/v1/health` 都没有 ⇒ 那个 `Unreachable` 是在**连接之前**产生的。而它在外面**完全同形**(三种成因写在 `data/repository/ConnectivityRepository.kt` 类注释新增的那一节):
|
||||
> - **(甲) 建 client 就抛**:baseUrl 为空或格式不合法时 `Retrofit.baseUrl` 直接抛,被 `probeReachability` 的 `catch (Exception)` 折成不通。socket 上一个字节都没有。
|
||||
> - **(乙) 连上了、一个字都没写**:`createProbe` 的 `callTimeout` 是**整通上限(含连接)**,而探测**从不复用连接池** ⇒ 每次都要现做一次冷握手,预算只有 2.5 秒,比共用客户端的 10 秒**连接**预算紧 4 倍。连接比它慢时 OkHttp 在写请求之前就放弃了 ⇒ 对端只看到「连上又不说话」,**日志里一条请求行都不会有**。
|
||||
> - **(丙) 请求出去了、回执非 2xx**(或回执解析不出来):**只有这一档在对端留痕**。
|
||||
> ⇒ 所以「代理日志里没有 health 那一行」**推不出**「App 没探过」;(甲)(乙) 都不留痕。上一轮结论里那句「App 从来没探过健康端点」需要收回一半:**没探过是可能的,(甲)(乙) 也同样符合全部现有证据**。
|
||||
>
|
||||
> ### 3. 本轮改动(除一处 DEBUG 日志外,全部是文档/测试;无行为变化)
|
||||
> - 新增测试 `app/src/test/kotlin/top/yeij/cyrene/data/api/ProbeClientTest.kt`(6 例)。
|
||||
> - `data/api/ApiClientFactory.kt` 的 `createProbe`:**DEBUG 下挂 BASIC 日志**(与通用 `create` 同一约定)。这是本轮要的**诊断仪**:`-->` 打在连接**之前**,所以「到底有没有探」与「死在连接还是死在回执」当场可分。探测无令牌、无请求体,日志不涉凭据。
|
||||
> - KDoc 失真修正:`ui/chat/SendFallback.kt` 的 `sendRouteFor`(「连探测都不做」只在判据层面成立)、`ChatViewModel` 的调用点与 `probeReachability`、`ConnectivityRepository` 补「三种成因 + logcat 判据」、`data/ws/WsState.kt` 的 `Offline` 补「非 401 的握手失败不属于这一档」。
|
||||
>
|
||||
> ### 4. 下一步(要设备,一次就能定论)
|
||||
> 1. 同一套场景重跑(`ws_block_proxy.py` + `adb reverse tcp:10942 tcp:18098`),在输入栏发一条纯文本。
|
||||
> 2. 看**发消息那一刻**的 `adb logcat -s OkHttp`(探针客户端走 OkHttp 的 Platform 日志):
|
||||
> - **没有** `--> GET http://127.0.0.1:10942/api/v1/health` ⇒ **(甲)**:死在建 client,去查 App 里配置的服务器地址(空 / 缺 scheme / 有非法字符)。
|
||||
> - 有 `-->` 后跟 `HTTP FAILED: java.io.InterruptedIOException: timeout` ⇒ **(乙)**:2.5 秒整通上限被连接吃掉。(**不许**为此放宽 fail-closed;要不要让探测的连接预算与链路对齐,是主人裁决。)
|
||||
> - 有 `-->` 后跟 `HTTP FAILED: java.net.ConnectException` ⇒ 对端根本没在听(`adb reverse` 被冲掉 / 代理没起)⇒ **这不是 App 的 bug**。
|
||||
> 3. 同时确认**发完消息之后**代理日志里还有没有新的 `BLOCKED /ws/chat`:没有就说明路已经断了(⇒ 第 2 条的第三种),那一轮阶梯其实从没在「HTTP 通」的条件下被考过 —— W40 那一轮就是用 `adb kill-server`/`adb root` 造「真断线」的,这两件都会把 `adb reverse` 一起冲掉(见本文件 W39/W40 两处踩坑记录)。
|
||||
Reference in new issue
Block a user