From a07d19f3d7854a519a13c59a61a955df34bc82a0 Mon Sep 17 00:00:00 2001 From: wtp <15908282659@163.com> Date: Mon, 9 Nov 2026 18:41:44 +0800 Subject: [PATCH 1/2] =?UTF-8?q?test:=20afterMonotonic=20=E6=8C=89=E5=AE=9E?= =?UTF-8?q?=E9=99=85=E5=9B=9E=E9=80=80=E7=82=B9=E5=88=87=E5=88=86=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=EF=BC=8C=E6=B6=88=E9=99=A4=20-j4=20=E5=9F=BA=E7=BA=BF?= =?UTF-8?q?=E5=81=87=E7=BA=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 按序列内容定位实际回退点 rewindAt(首个 < before 的位置),新会话单调判据从 rewindAt + 1 起算,避开 first 快照到 seek 生效之间旧会话尾部追加 position 造成的固定下标切错。 - 仅调整测试判据,不改任何生产代码。 - 不放宽判据:所有 CHECK/REQUIRE、断言条数、用例/suite 名、数值(3000ms / stepMs=20 / seek 100)保持不变;waitFor(rewound) 仍保证 beforeCount 之后存在 < before 的值,故 rewindAt < after.size() 成立。 --- sdk/test/unit/test_player_integration.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/test/unit/test_player_integration.cpp b/sdk/test/unit/test_player_integration.cpp index 916c7d780..5706063fa 100644 --- a/sdk/test/unit/test_player_integration.cpp +++ b/sdk/test/unit/test_player_integration.cpp @@ -1396,8 +1396,14 @@ TEST_SUITE("PublicPlayer Interactive Paths") }, 3000); const std::vector after = callbacks.positionValues(); + // seek 是异步 WorkQueue 任务;first 快照到 seek 生效之间旧会话仍会追加 position。 + // 新会话起点必须按序列内容定位实际回退点,不能用固定快照下标猜。 + size_t rewindAt = after.size(); + for (size_t i = beforeCount; i < after.size(); ++i) { + if (after[i] < before) { rewindAt = i; break; } + } bool afterMonotonic = true; - for (size_t i = beforeCount + 1; i < after.size(); ++i) { + for (size_t i = rewindAt + 1; i < after.size(); ++i) { if (after[i] < after[i - 1]) { afterMonotonic = false; } From fa59b1b34dc204e541fc7ea07d023822aa966df6 Mon Sep 17 00:00:00 2001 From: wtp <15908282659@163.com> Date: Mon, 9 Nov 2026 18:44:00 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20#115=20=E5=86=B3=E8=AE=AE=E8=90=BD?= =?UTF-8?q?=E6=A1=A3=20=E2=80=94=E2=80=94=20=E5=BC=82=E6=AD=A5=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E8=BE=B9=E7=95=8C=E5=8F=8D=E6=A8=A1=E5=BC=8F=E4=B8=8E?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AGENTS.md NOTES 新增「按固定快照下标切会话」反模式条:记录 afterMonotonic 基线假红根因(seek 异步生效 → 旧会话追加 position → 合法回退落到 beforeCount+k)、正确切分方式、以及「实证先行 / 变异验证 / 同轮数 -j4 对照」三道闸门。 - 明确按实际回退点切分不是放宽判据;备选生产 resetFloor 二次上报须另开新票。 - 决议依既有一次性批量授权采用 AI 推荐;票正文不改,结论与逐轮数据留 #116 resolution 回填。 --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index e08db922c..005d85520 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,6 +120,7 @@ cmake --build build --target package_portable 冷构建 24 分钟做完、缓存没写回,下一轮又白等一次冷构建)。 - **判定 CI 成败只认 `gh run view --json status,conclusion,attempt`**。`gh run watch --exit-status | tail` 未加 `set -o pipefail` 时退出码取 `tail` 的 0,失败 run 会被看成成功(2026-11-08 踩过一次假绿)。纯 `doc/` / `docs/` / `AGENTS.md` 提交不触发 CI:两个 workflow 的 `push` 与 `pull_request` 都带 `paths` 过滤(`CMakeLists.txt` / `cmake/**` / `app/**` / `scripts/**` / `sdk/**` / workflow 自身),这类提交不出现在 run 列表里属预期,别误判成「CI 挂了没跑」。 - **判 flake 四条证据**:① 同一代码在 PR run 已绿;② 失败目标不在本次改动文件;③ 该目标常态耗时吻合;④ `gh run rerun --failed` 全绿。Linux 侧有低概率单目标 flake(2026-11-08:`sdk_badinput_rejection_tests` 的 `getMusicCount()==1` 20s 等待超时;PR #111 合并后 `sdk_interaction_tests` 的 `seek to middle moves position forward` 还出现过 `SIGSEGV` 进程崩溃、14 断言全过的新形态)。**别为此放宽超时或改动 BadInput 55 例基线**,只做一次 rerun 取证。当 ② 不成立时用替代取证:比对 merge commit 与 PR 分支头的 tree(相同即逐字节同代码)、用 doctest suite 过滤器确认失败用例与新增用例零交集、确认生产包装层默认关闭时行为等价。日志里 grep 到的 `sigsegv` 多数是 g3log 示例 `main_sigsegv.cpp` 的编译输出,别当成历史崩溃。 +- **异步事件的会话起点一律按事件内容定位,不要用固定快照下标猜**(2026-11-09 `afterMonotonic` 基线假红,[#115](https://github.com/wt159/SoundBridge/issues/115) / [#116](https://github.com/wt159/SoundBridge/issues/116)):`sdk/test/unit/test_player_integration.cpp` 的 `position callbacks stay monotonic and a legal seek still rewinds` 曾用 `beforeCount+1` 当「seek 后的新会话起点」,但 `Player::seek` 经 `m_workQueue.asyncRunTask` 异步生效,`first` 快照到 seek 生效之间旧会话仍会追加 position;只要追加 ≥1 个值,合法回退 100 就落在 `beforeCount+k`,被 `afterMonotonic` 误判为「新会话内回退」→ `sdk_interaction_tests` 低概率假红(11-09 PR #114 稳定性对照两组各 9/10、11-08 C1/C5 master 9/10 同源)。正解是在序列中定位实际回退点 `rewindAt`(首个 `< before`),只检查其后的新会话单调;真回退仍会被抓到、断言条数与所有数值不变,**这不是放宽判据**。修判据前必须先抓到失败序列实证(临时诊断只写 stdout、取证后零残留),再做「注入真回退必红 / 还原绿」的变异验证与同轮数 `-j4` 对照;备选「生产 `resetFloor` 二次上报」若成立须另开新票,不在测试票里改生产代码。 - **新增测试专用钩子(哪怕默认关闭)也要走稳定性闸门**:release/acquire 内存序、只在控制线程读写、绝不落解码线程 / 消费主循环 / 判据主循环;放行前必须做 master 与分支**同轮数** `ctest -j4` 对照(2026-11-08 C1/C5:分支 10/10 vs master 9/10 才放行),单目标串行绿不算证据。落在解码线程或判据主循环的额外写会把 `-j4` 假红率从 0 抬到约 30%(O1 对照实验)。(2026-11-09 反例:#112 / #113 / PR #114 的 `stop()` 清槽 + 两个测试钩子,虽通过变异验证、两轴 review 与 PR 阶段 CI 双绿,但分支两组 `-j4` 各 9/10、master 同轮数 10/10,按「假红率高于基线即撤回」的闸门**全部撤回、PR 关闭、改动未进 master**;取证与 resolution 见 [PR #114](https://github.com/wt159/SoundBridge/pull/114)。CI 双绿不能替代同轮数对照;失败用例与改动零交集也不是事后放宽闸门的理由。) - **`SIGSEGV` flake 的「反复出现」门槛(方向②闸门)**:截至 2026-11-08,全历史 50 个 `sdk-coverage` run 里 `test case CRASHED: SIGSEGV` **仅 1 次**(push run `42488656322` attempt 1,`sdk_interaction_tests` / `test_player_integration.cpp:726` 的 `seek to middle moves position forward`),`rerun --failed` 后 attempt 2 全绿;其余 6 个历史 `failure` 分别是编译错误、`skip not allowed in CI`、断言失败、第三方 boost 拷贝、`Build coverage target` 等待超时,形态都不同。**满足任一才算「反复出现」、才另开票重审**:① 不同 `headSha` 上同形态 SIGSEGV ≥2 次且 `rerun --failed` 不能稳定消解;② 本机 `ctest -j4` 同轮数对照中该用例可稳定复现(≥2/10);③ 拿到崩溃栈 / ASan 报告指向具体竞态。单次偶发 + rerun 全绿 → 维持观测,不改判据 / TIMEOUT / 坏输入 55 例基线。取证:`gh run list --workflow sdk-coverage.yml --limit 50 --json databaseId,conclusion,attempt` 列候选,再对每个 `failure` run 用 `gh run view --log-failed | grep -Ei "CRASHED|SIGSEGV"`;**run 已是 attempt 2 成功时 `--log-failed` 看不到 attempt 1**,须用 `gh api repos///actions/runs//attempts/1/jobs` 补查(注意 `gh api` 不吃 `-R`)。 (2026-11-09 补:门槛②已实测 —— 本机 Windows / MinGW730 跑 10 轮 `ctest -j4` 全套 14/14 全绿(57–60s/轮,`sdk_interaction_tests` 13.58–14.21s)、**0 次 SIGSEGV**;另用 `UnitTests -ts="MusicPlayer Interactive Paths" -tc="seek to middle moves position forward"` 单用例连跑 100 次,**0 崩溃 0 非零退出**(127s)。**平台局限**:CI 那次 SIGSEGV 发生在 Linux ubuntu-24.04,本地实验是 Windows,不能等价外推;三条门槛至 11-09 均未命中,维持观测,不另开票、不改生产代码。)**2026-11-09 一次性破例留痕**:用户明确指令「强制重审」,主动打破本条门槛、不等第二次出现,开 [#112](https://github.com/wt159/SoundBridge/issues/112) / [#113](https://github.com/wt159/SoundBridge/issues/113) 并出 PR #114;该 effort 事前写死边界「无复现、无崩溃栈、CI 在 Linux / 本机在 Windows,交付状态契约加固 + 判据 + 稳定性对照,**不是修复 SIGSEGV**」,且因上述稳定性闸门未过而**全部撤回,终态仍是不改生产代码**。三条门槛本身不删、不放宽,本次破例不构成后续自动启动 effort 的先例;SIGSEGV 根因仍未定位,H1 在 Windows 侧 240 次窗口命中 0 崩溃已证伪、H2 未改、H3/H4 未验。取证与反悔条件见 `doc/player-state-contract.md` §10.4 与 `doc/streaming-playback-spec.md` §12.6。 - **真触发时 seek 竞态的最小重审范围**(2026-11-08 只读勘察;日志无崩溃栈,以下均未证实为根因):① `AudioStreamDecoder::seekToMs()`(`AudioStreamDecoder.cpp:219-233`)只写 `m_seekRequestMs` 单槽,native RA 定位在解码线程 `exchange(-1)` 后执行;`cancelRequested()` 把「槽内有值」当取消(`:457`),seek 期间刻意不置 `m_abortDecode`(`:226-231`,#59/#60/#65 已知时序坑),而 stop 靠 `m_stopRequest + m_abortDecode` 中断读回调 —— **「seek 槽未消费完就 stop」是最可疑的叠加窗口**。② 析构期已有 issue #70 的 `SDL_CloseAudioDevice` 双释放防护(`MusicPlayer.cpp:153-157`:`m_workQueue.shutdown()` 必须先于 `_releaseAudio()`);`stop()` 是异步队列任务(`:219`),`_stop()` 已走过一次 `_releaseAudio()`(`:369-370`),析构再走一次,重复路径幂等性无专门判据覆盖。③ **已排除**:`m_seekBaseMs` / `m_seekDiscardBytes` 虽非 atomic,但只在解码线程内读写(seek 分支 `:538/:614/:677/:975` 与 EOS 汇总 `:345/:353/:366`);消费线程经 `MusicPlayer.cpp:555` 调的 `positionMs()`(`:430-441`)只读 atomic `m_segmentBaseBytes` + `m_ring->readPos()`,**不构成数据竞争**,别重复怀疑这条。