fix(accounts,probe): 测试缝换稳定身份(ABA)+ 超时墙钟对齐真实预算(M236)

两条 M230 台账判定「真开着」的测试债:

1. accounts 测试缝的 key 是 Arc::as_ptr(&job.cancelled)。job 一 drop,控制块
   立刻可能分给下一个 LoginJob,于是新 job 读到旧 job 的记录(ABA),并行跑的
   「设备码入口不许置位 browser_first」因此偶发假红。改成 LoginJob 自带的
   稳定身份 JobId(cfg(test) 字段,进程内单调递增、永不复用);ENTRIES 从
   Vec<(usize,bool)> 改成带 Weak 的 Entry:record 时 retain 收走已 drop 的 job,
   同一 job 覆写成「最后一次」(消掉 mode_for 取第一条的歧义)。
   反向证据:身份退回堆地址 ⇒ the_test_seam_does_not_reuse_a_heap_address_as_identity
   红(mode_for(&younger) 读到 Some(true))。

2. probe 的 silent_server_times_out 断 `elapsed < 3s`,跟真实预算(400ms)无关。
   改成 预算 + SLACK(400ms + 1s):抓「预算没生效」(那要等假服务器睡满 5s),
   放开毫秒级精度;并新增两条判据——detail 里的时限就是本次预算(证明走的是
   预算分支)、假服务器真的收到 1 次请求(不是没连上就早退)。
   反向证据:摘掉 tokio::time::timeout ⇒ 5.08s / unreachable 红;把计时值硬编码
   3s ⇒ 3.006s > 1.4s 红。
This commit is contained in:
AskaEth committed 2026-11-11 06:53:52 +08:00
1 parent 30d4163816
commit 4952ee2706
3 files changed
+224 -19

No files matched your search

+46 -5
View File
@@ -5,7 +5,7 @@
//! | `auth_failure_is_classified_as_auth` | 401 → 鉴权失败 | `map_error` 的 401 分支改成 `Other` |
//! | `missing_model_is_model_not_found` | 404 → 模型不存在 | 把 404 归进 auth |
//! | `closed_port_is_unreachable` | 连不上 → 端点不可达 | 把 `ErrorKind::Http` 归到 `Other` |
//! | `silent_server_times_out` | 不响应 → 超时 | 去掉 `tokio::time::timeout` |
//! | `silent_server_times_out` | 不响应 → 超时,且在**本次预算**到点收手 | 去掉 `tokio::time::timeout`(那就得等假服务器睡满 5s) |
//! | `success_is_one_real_completion` | 200 + SSE → 通过 | 把 `Ok(Ok(_))` 当成失败 |
//! | `the_request_carries_the_configured_model` | **请求体里的 `model` 就是配置那个** | `probe_with` 里把 model 写死成别的名字 |
//! | 每条失败用例都断言 `target` | 失败结论带「对哪个端点/模型」 | `outcome_from_error` 丢掉 target(r1 复审 P2-1) |
@@ -203,24 +203,65 @@ fn closed_port_is_unreachable() {
);
}
/// 超时用例的预算:跟产品默认时限(`DEFAULT_TIMEOUT` = 8s)同一个量级,取 1/20 只是为了让整个
/// 测试套不必等 8 秒。判据只依赖「预算生效时远早于假服务器沉默的 5 秒」,不依赖这个绝对值。
const TIMEOUT_BUDGET: Duration = Duration::from_millis(400);
/// 允许的调度噪声:一次本地回环往返 + 建 tokio 运行时是毫秒级,1 秒是给繁忙机器的余量;它必须
/// **远小于**「预算没生效」的代价(假服务器要睡 5s 才收手),否则两种情形区分不开。
const TIMEOUT_SLACK: Duration = Duration::from_secs(1);
/// 不回应 ⇒ 超时,且在**本次预算**到点时就收手(不是等对面 5 秒沉默、也不是等别处的默认时限)。
///
/// 三层判据各管一件事,别混:
///
/// 1. **走了哪条分支**:`kind == "timeout"`。没被本模块预算截住的「不回应」落到的是传输层错误
/// ——实测:摘掉 `tokio::time::timeout` ⇒ 假服务器睡满 5s 后关连接,结论是 `unreachable`,
/// 所以这一条先把「根本没走到超时分支」挡在外面。
/// 2. **是本次预算那条分支**:`detail` 里的时限就是传进去的预算(Timeout 分支把 `timeout` 原样
/// 写进 detail)。它证明「结论由这条预算分支产出」,**不**证明「计时器用的就是这个值」:分支
/// 里把计时值换掉、detail 照抄参数,本条抓不住——那是下一条的活。
/// 3. **墙钟**:`elapsed < 预算 + SLACK`,抓的正是上一条抓不住的形态(「分支对、但等的不是这个
/// 预算」)。实测:把计时值硬编码成 3s ⇒ 本断言红(3.006s > 1.4s)。
///
/// 抓得住:「预算没生效 / 生效的不是这个预算」——那种情况要等假服务器睡满 5s 或等 3s 之类的
/// 别处时限,远超 `预算 + SLACK`。
/// 抓不住:预算到底是 400ms 还是 1.2s(都落在 SLACK 里),也不量定时器精度;机器再忙只吃掉
/// SLACK 的余量,不吃预算本身。
#[test]
fn silent_server_times_out() {
// 假服务器收下请求后睡 5s(`Behaviour::Silent`):这就是「预算没生效」时本用例会等到的墙钟。
let server = Server::start(Behaviour::Silent);
let started = std::time::Instant::now();
let outcome = probe_with(
&RigProvider::factory(),
&settings(&server.base_url(), "gpt-test"),
Duration::from_millis(400),
TIMEOUT_BUDGET,
);
let elapsed = started.elapsed();
assert_eq!(
outcome.kind(),
"timeout",
"不回应的端点必须归到超时,实际 {outcome:?}"
);
let ProbeOutcome::Timeout { detail, .. } = &outcome else {
unreachable!("上一行已经断言 kind 是 timeout")
};
assert!(
started.elapsed() < Duration::from_secs(3),
"超时必须在时限附近收手,实际 {:?}",
started.elapsed()
detail.contains(&TIMEOUT_BUDGET.as_secs_f32().to_string()),
"超时结论里的时限不是本次预算({TIMEOUT_BUDGET:?}):{detail}"
);
assert_eq!(
server.hits().len(),
1,
"超时之前必须真的把一次请求发给了假服务器(否则「超时」可能只是没连上)"
);
assert!(
elapsed < TIMEOUT_BUDGET + TIMEOUT_SLACK,
"预算没生效或收手太晚:预算 {TIMEOUT_BUDGET:?},实际 {elapsed:?};假服务器会沉默 5s,\
超过 {:?} 就说明等的不是这个预算",
TIMEOUT_BUDGET + TIMEOUT_SLACK
);
}