CI 偶发失败:与业务改动无关的 crates.io 依赖缺口与两条用例自身缺陷 #327

Open
opened 2026-09-11 16:07:23 +08:00 by suzmii · 0 comments
Member

背景

PR #316(AGC 资源工作台 V3)的一次 push 3bfc3f084 上 CI 红了 Backend tests 与 Native shell tests。经排查,两条失败都与该 PR 的业务改动无关,本 Issue 单独跟踪。

字节级判据git diff 39a96aa41 3bfc3f084 只含前端 TS/TSX/CSS 与 .mdserver-rs 树哈希两边完全相同(9edd75582c0f7ea7557a24c9ac9d9dec662e2e55),apps/ai-game-creator-shell/src-tauri 树哈希也相同(f6d25066d1a5826a407cfa663783429b55baf6b7)。同一份 Rust 代码在 run 1947(39a96aa41)Backend / Native 全绿,在 run 1950(3bfc3f084)两红 → 同字节、不同结果 = 间歇性

问题 1:CI 基础设施缺口 —— 测试阶段依赖 live crates.io

Native shell tests(run 1950 job 7089)不是测试失败,前端部分全绿(Test Files 84 passedTests 1223 passed),挂在这一步:

> react-example@0.0.0 agent-runtime-core:check
> cargo test --manifest-path server-rs/crates/agent-runtime-core/Cargo.toml
    Updating crates.io index
error: failed to get `serde_json` as a dependency of package `agent-runtime-core v0.1.0`
Caused by: download of se/rd/serde_json failed -> curl failed
Caused by: [28] Timeout was reached (Connection timed out after 30000 milliseconds)
##[error]Process completed with exit code 101.

缺口server-rs/Cargo.tomlcrates/agent-runtime-core 同时出现在 members(第 10 行) 与 exclude(Cargo 以 exclude 为准)→ 它不在 workspace 内、没有提交 Cargo.lock(该目录下有 .gitignore),而 CI 的 Prepare native Rust dependencies 只对 3 个 manifest 执行 cargo fetch --lockedserver-rs/Cargo.tomlapps/desktop-shell/src-tauriapps/ai-game-creator-shell/src-tauri)—— 不含它。于是测试阶段才现场 Updating crates.io index,且无锁文件兜底 ⇒ 每次 CI 都在赌网络

同类还需核清:agent-runtime-orchestrationplatform-agent 是否也落在 exclude 且同样未纳入 fetch。

建议修法:把这些 crate 的 Cargo.toml 加进 Prepare native Rust dependenciescargo fetch 列表;或给它们提交 Cargo.lock 并在 fetch/test 用 --locked;或改为离线/预置 cargo 缓存。不要用 || true 之类掩盖

问题 2:两条既有用例自身的缺陷(并行执行下间歇红)

Backend tests(run 1950 job 7088):cargo test --locked --workspace --exclude spacetime-module --no-fail-fast1038 passed; 2 failed; finished in 31.17s

2.1 端口 TOCTOU

bgfilter_worker::tests::connect_retry_crosses_worker_restart_window_and_stops_on_http_response
panicked at crates/api-server/src/bgfilter_worker.rs:3622:18:
rebind test port: Os { code: 98, kind: AddrInUse, message: "Address already in use" }
(随后 3678:22 mock server should finish: JoinError::Panic(... 同上 ...))

辅助函数 reserved_loopback_port()bgfilter_worker.rs:3561-3568)= bind 127.0.0.1:0 → 取端口 → drop(listener);用例拿到端口后 sleep 700ms 才在 :3620 重新 bind。这 700ms 内并行用例可抢走该端口(典型 TOCTOU)。该文件最后改动为 2026-07-23 a2ee879fc(引入该用例的提交),此后未动。

建议修法:把 mock 的 sleep(700ms) 挪到 bind 之后、accept 之前(listen backlog 会挂住 connect),彻底消除窗口;或给 mock server 一个可注入 TcpListener 接缝不要只调 sleep 时长——那只是改窗口大小。

2.2 开放式 mock + 精确次数断言

editor_background_music_prompt_assist::tests::consecutive_valid_requests_are_not_subject_to_a_feature_rate_limit
panicked at crates/api-server/src/editor_background_music_prompt_assist.rs:849:9:
assertion `left == right` failed   left: 5   right: 4

= assert_eq!(mock.finish().len(), 4)。该用例使用 spawn_open_ended_mock_llm_server(Vec::new(), fallback)开放式 mock,来多少答多少),却把"精确 4 次请求"钉死;多出的第 5 次来自本模块的重试轮(同文件已有 4 条 retry 用例为证),而用例里设了 llm_max_retries: 0 期望不重试。文件最后改动 2026-08-08 3c00bcd2e,用例由 281c84b7b(#142)引入。

诚实标注:CI 日志只能证明"多一次请求",具体触发哪条重试轮无法从日志判定,需单跑或用带捕获打印的有界 mock 钉死。这不影响"与本 PR 无关"的定性。

建议修法:断言改成客户端口径("4 次调用都 200 且都不是 429"——用例名本来就只承诺这一点);若要钉"无额外重试",把开放式 mock 换成有界 mock(第 5 次请求直接 panic 并打印捕获到的请求原文),把不确定性变成可诊断的确定性失败。

历史红率(最近 13 次 run)

  • Native shell tests:5 红 8 绿。但红因各不相同:1948/1945 是各自分支自己的 TS 类型错误(useUiEditorPage.ts(1211) TS2339)、1949 是另一 PR 的 vitest 失败、1950 是本次的 crates.io 超时(新形态)
  • Backend tests:3 红 10 绿。其中 1948/1945 是 pull request head does not contain the latest base commit分支落后守卫,非测试失败)、1938 是 icon 白名单断言(已在 PR #316 修复)。

验收/完成判据

  1. CI 测试阶段不再触碰 live crates.io--offline 或锁文件 + --locked 可过);
  2. bgfilter_worker 那条用例在默认并行下连续多轮不再出现 AddrInUse
  3. editor_background_music_prompt_assist 那条用例的断言与其用例名承诺的语义一致,且失败时能给出可诊断信息;
  4. 相关修复不改业务语义(沿用仓库已认可的"局部有界超时 / 可注入接缝"先例,不放宽断言)。

关联

## 背景 PR #316(AGC 资源工作台 V3)的一次 push `3bfc3f084` 上 CI 红了 Backend tests 与 Native shell tests。经排查,**两条失败都与该 PR 的业务改动无关**,本 Issue 单独跟踪。 **字节级判据**:`git diff 39a96aa41 3bfc3f084` 只含前端 TS/TSX/CSS 与 `.md`;`server-rs` 树哈希两边完全相同(`9edd75582c0f7ea7557a24c9ac9d9dec662e2e55`),`apps/ai-game-creator-shell/src-tauri` 树哈希也相同(`f6d25066d1a5826a407cfa663783429b55baf6b7`)。同一份 Rust 代码在 run 1947(`39a96aa41`)Backend / Native 全绿,在 run 1950(`3bfc3f084`)两红 → **同字节、不同结果 = 间歇性**。 ## 问题 1:CI 基础设施缺口 —— 测试阶段依赖 live crates.io Native shell tests(run 1950 job 7089)**不是测试失败**,前端部分全绿(`Test Files 84 passed`、`Tests 1223 passed`),挂在这一步: ``` > react-example@0.0.0 agent-runtime-core:check > cargo test --manifest-path server-rs/crates/agent-runtime-core/Cargo.toml Updating crates.io index error: failed to get `serde_json` as a dependency of package `agent-runtime-core v0.1.0` Caused by: download of se/rd/serde_json failed -> curl failed Caused by: [28] Timeout was reached (Connection timed out after 30000 milliseconds) ##[error]Process completed with exit code 101. ``` **缺口**:`server-rs/Cargo.toml` 里 `crates/agent-runtime-core` **同时出现在 `members`(第 10 行) 与 `exclude`**(Cargo 以 `exclude` 为准)→ 它不在 workspace 内、**没有提交 `Cargo.lock`**(该目录下有 `.gitignore`),而 CI 的 `Prepare native Rust dependencies` 只对 3 个 manifest 执行 `cargo fetch --locked`(`server-rs/Cargo.toml`、`apps/desktop-shell/src-tauri`、`apps/ai-game-creator-shell/src-tauri`)—— **不含它**。于是测试阶段才现场 `Updating crates.io index`,且无锁文件兜底 ⇒ **每次 CI 都在赌网络**。 同类还需核清:`agent-runtime-orchestration`、`platform-agent` 是否也落在 `exclude` 且同样未纳入 fetch。 **建议修法**:把这些 crate 的 `Cargo.toml` 加进 `Prepare native Rust dependencies` 的 `cargo fetch` 列表;或给它们提交 `Cargo.lock` 并在 fetch/test 用 `--locked`;或改为离线/预置 cargo 缓存。**不要用 `|| true` 之类掩盖**。 ## 问题 2:两条既有用例自身的缺陷(并行执行下间歇红) Backend tests(run 1950 job 7088):`cargo test --locked --workspace --exclude spacetime-module --no-fail-fast` → `1038 passed; 2 failed; finished in 31.17s` ### 2.1 端口 TOCTOU ``` bgfilter_worker::tests::connect_retry_crosses_worker_restart_window_and_stops_on_http_response panicked at crates/api-server/src/bgfilter_worker.rs:3622:18: rebind test port: Os { code: 98, kind: AddrInUse, message: "Address already in use" } (随后 3678:22 mock server should finish: JoinError::Panic(... 同上 ...)) ``` 辅助函数 `reserved_loopback_port()`(`bgfilter_worker.rs:3561-3568`)= `bind 127.0.0.1:0` → 取端口 → **`drop(listener)`**;用例拿到端口后 sleep **700ms** 才在 `:3620` 重新 bind。**这 700ms 内并行用例可抢走该端口**(典型 TOCTOU)。该文件最后改动为 2026-07-23 `a2ee879fc`(引入该用例的提交),此后未动。 **建议修法**:把 mock 的 `sleep(700ms)` 挪到 `bind` 之后、`accept` 之前(listen backlog 会挂住 connect),彻底消除窗口;或给 mock server 一个**可注入 `TcpListener` 接缝**。**不要只调 sleep 时长**——那只是改窗口大小。 ### 2.2 开放式 mock + 精确次数断言 ``` editor_background_music_prompt_assist::tests::consecutive_valid_requests_are_not_subject_to_a_feature_rate_limit panicked at crates/api-server/src/editor_background_music_prompt_assist.rs:849:9: assertion `left == right` failed left: 5 right: 4 ``` = `assert_eq!(mock.finish().len(), 4)`。该用例使用 `spawn_open_ended_mock_llm_server(Vec::new(), fallback)`(**开放式 mock,来多少答多少**),却把"精确 4 次请求"钉死;多出的第 5 次来自本模块的重试轮(同文件已有 4 条 retry 用例为证),而用例里设了 `llm_max_retries: 0` 期望不重试。文件最后改动 2026-08-08 `3c00bcd2e`,用例由 `281c84b7b`(#142)引入。 > 诚实标注:CI 日志只能证明"多一次请求",**具体触发哪条重试轮无法从日志判定**,需单跑或用带捕获打印的有界 mock 钉死。这不影响"与本 PR 无关"的定性。 **建议修法**:断言改成**客户端口径**("4 次调用都 200 且都不是 429"——用例名本来就只承诺这一点);若要钉"无额外重试",把开放式 mock 换成**有界 mock**(第 5 次请求直接 panic 并打印捕获到的请求原文),把不确定性变成可诊断的确定性失败。 ## 历史红率(最近 13 次 run) - **Native shell tests:5 红 8 绿**。但红因各不相同:1948/1945 是各自分支自己的 TS 类型错误(`useUiEditorPage.ts(1211) TS2339`)、1949 是另一 PR 的 vitest 失败、**1950 是本次的 crates.io 超时(新形态)**。 - **Backend tests:3 红 10 绿**。其中 1948/1945 是 `pull request head does not contain the latest base commit`(**分支落后守卫,非测试失败**)、1938 是 icon 白名单断言(已在 PR #316 修复)。 ## 验收/完成判据 1. CI 测试阶段**不再触碰 live crates.io**(`--offline` 或锁文件 + `--locked` 可过); 2. `bgfilter_worker` 那条用例在**默认并行**下连续多轮不再出现 `AddrInUse`; 3. `editor_background_music_prompt_assist` 那条用例的断言与其用例名承诺的语义一致,且失败时能给出可诊断信息; 4. 相关修复**不改业务语义**(沿用仓库已认可的"局部有界超时 / 可注入接缝"先例,不放宽断言)。 ## 关联 - PR #316(发现处,本次 push `3bfc3f084`) - Tracking Issue #309 - 相关日志端点:`GET /api/v1/repos/{owner}/{repo}/actions/jobs/{job_id}/logs`(不需要 run id)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GenarrativeAI/Genarrative#327