修复: shell 探测缓存测试顺序污染
PWSH_CACHE 从 OnceLock 改互斥可恢复形态,写缓存测试进时备份出时写回 涉缓存测试共持串行闸,消除全量跑时序性互踩(约一成概率触发的假失败) 生产读写语义不变:有缓存读缓存,无缓存探测后写,超时不写入
This commit is contained in:
@@ -421,6 +421,9 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn detect_returns_cached_static_ref() {
|
||||
// 持 shell.rs 缓存串行闸:本测试可能首次创建 SNAPSHOT,其 shell 字段取自创建时刻的
|
||||
// 缓存状态;若与写缓存的 shell.rs 测试并行,漂移测试会读到与 current_shell 不一致的快照。
|
||||
let _gate = crate::shell::PWSH_CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// 两次 detect 返回同一引用(OnceLock 全局缓存)。
|
||||
let a = EnvSnapshot::detect().await as *const _;
|
||||
let b = EnvSnapshot::detect().await as *const _;
|
||||
@@ -432,10 +435,14 @@ mod tests {
|
||||
/// 历史 bug:detect_shell 持独立 probe_command_success 探测,与 shell.rs::probe_pwsh 各填各的
|
||||
/// OnceLock → prompt 告诉 LLM 用 pwsh,执行却走 powershell(或反之)。根本修:二者共用
|
||||
/// shell.rs 的 probe_pwsh_blocking + PWSH_CACHE 单源。本测试锁定「无漂移」不变量。
|
||||
///
|
||||
/// 隔离:持 shell.rs 缓存串行闸,快照值与 current_shell() 须读自同一缓存状态,
|
||||
/// 防与写缓存的 shell.rs 测试并行互踩(Mutex 化后写值会被复位,闸保证读窗口内无交叉写)。
|
||||
#[cfg(windows)]
|
||||
#[tokio::test]
|
||||
async fn detect_shell_matches_shell_rs_current_shell() {
|
||||
// detect() 内 detect_shell → probe_pwsh_blocking 填 PWSH_CACHE,再读 current_shell 映射。
|
||||
let _gate = crate::shell::PWSH_CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// detect() 内 detect_shell → 读 PWSH_CACHE 映射,与下方 current_shell() 同源。
|
||||
let snap = EnvSnapshot::detect().await;
|
||||
let current = crate::shell::current_shell();
|
||||
// 二者必须一致(同一 PWSH_CACHE 单源读出)。
|
||||
|
||||
@@ -38,7 +38,7 @@ impl Default for ShellType {
|
||||
// 远优于 cmd,从根上避 kms 类引号转义地狱。AI 写文件执行见 env_profile。
|
||||
// 优先 pwsh(PS7,支持 && 运算符)——LLM 训练数据 Unix 多,普遍生成 `cd x && y`,
|
||||
// PS5 不支持 && 致命令失败(实测会话 6acb7f9b `cd ... && git init` InvalidEndOfLine)。
|
||||
// 探测失败(未装 pwsh)回退 PS5。探测结果 OnceLock 缓存(只探一次)。
|
||||
// 探测失败(未装 pwsh)回退 PS5。探测结果缓存于模块级 PWSH_CACHE(只探一次)。
|
||||
//
|
||||
// 【序约束 / 死缓存修复】Default 为同步签名,读模块级单源 PWSH_CACHE(由 probe_pwsh 写入)。
|
||||
// 缓存有两个填充点(均走 probe_pwsh_blocking 单真相源,无漂移):
|
||||
@@ -53,7 +53,7 @@ impl Default for ShellType {
|
||||
// 非单源 + env_snapshot 另持 probe_command_success 独立探测。根本修:合并为模块级 PWSH_CACHE 单源,
|
||||
// 写读同源;detect_shell 复用 probe_pwsh_blocking(单一探测实现)。
|
||||
if cfg!(windows) {
|
||||
if PWSH_CACHE.get().copied().unwrap_or(false) { ShellType::Pwsh } else { ShellType::PowerShell }
|
||||
if cache_get().unwrap_or(false) { ShellType::Pwsh } else { ShellType::PowerShell }
|
||||
} else {
|
||||
ShellType::Sh
|
||||
}
|
||||
@@ -62,13 +62,34 @@ impl Default for ShellType {
|
||||
|
||||
/// pwsh(PowerShell 7)可用性的全局单源缓存。
|
||||
///
|
||||
/// 由异步 `probe_pwsh()`(execute 路径)或同步 `probe_pwsh_blocking()`(detect_shell 路径)填充,
|
||||
/// 由异步 `probe_pwsh()`(execute 路径)或同步 `probe_pwsh_blocking()`(detect_shell 路径)探测,
|
||||
/// 同步路径 `ShellType::default()` / `current_shell()` 读取——写读同源,
|
||||
/// 杜绝历史上「两个 OnceLock 各填各的、Default 读的永空」死缓存 bug。
|
||||
///
|
||||
/// 语义:`get() == None` 表示尚未探测(首次启动 / Windows 未装 pwsh 也仅表示探测未跑或返 false);
|
||||
/// `get() == Some(true)` 表示已探测且 pwsh 可用。
|
||||
static PWSH_CACHE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
/// 语义:`None` 表示尚未探测(首次启动 / Windows 未装 pwsh 也仅表示探测未跑或返 false);
|
||||
/// `Some(true)` 表示已探测且 pwsh 可用。
|
||||
///
|
||||
/// 用 `Mutex<Option<bool>>` 而非 OnceLock:OnceLock 写入后进程级不可清,写缓存的测试
|
||||
/// 与依赖缓存一致性的测试(env_snapshot 漂移测试)全量并行跑时互踩。Mutex 化后测试可
|
||||
/// 备份/复位隔离(配 PWSH_CACHE_TEST_LOCK 串行闸);生产读写语义不变,有缓存读缓存、
|
||||
/// 无缓存探测后写。
|
||||
static PWSH_CACHE: std::sync::Mutex<Option<bool>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// 读缓存当前值(毒化容错:持锁测试 panic 后其余测试仍可继续,行为不变)。
|
||||
fn cache_get() -> Option<bool> {
|
||||
*PWSH_CACHE.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// 写缓存当前值(探测产物写入 / 测试复位共用,覆盖写)。
|
||||
fn cache_set(v: Option<bool>) {
|
||||
*PWSH_CACHE.lock().unwrap_or_else(|e| e.into_inner()) = v;
|
||||
}
|
||||
|
||||
/// 测试串行闸:缓存为进程级单源,写缓存的测试与依赖缓存一致性的测试并行互踩,
|
||||
/// 凡改写缓存或依赖缓存/EnvSnapshot 一致性的测试须持锁(仅测试隔离用,非生产路径)。
|
||||
/// 对齐 path_auth.rs 的 BLACKLIST_BYPASS_TEST_LOCK 模式;env_snapshot 漂移测试持同闸。
|
||||
#[cfg(test)]
|
||||
pub(crate) static PWSH_CACHE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
/// 同步探测 pwsh(PowerShell 7)是否可用(std::Command::status),成功返回 true。
|
||||
///
|
||||
@@ -94,7 +115,7 @@ pub(crate) fn probe_pwsh_blocking() -> bool {
|
||||
cmd.status().map(|s| s.success()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 探测 pwsh(PowerShell 7)是否可用(OnceLock 缓存,3s 超时,只探一次)。
|
||||
/// 探测 pwsh(PowerShell 7)是否可用(模块级缓存,3s 超时,只探一次)。
|
||||
///
|
||||
/// LLM 普遍生成 `&&`(Unix 习惯),仅 PS7+ 支持,Windows 自带 PS5 不支持。
|
||||
/// 探测:成功 spawn `pwsh -Command exit 0` 即可用。Windows 加 CREATE_NO_WINDOW 防黑窗闪现。
|
||||
@@ -110,16 +131,16 @@ pub(crate) fn probe_pwsh_blocking() -> bool {
|
||||
/// - 仅「正常完成(Ok(Ok(_)))」时 set 缓存(此时结果是可信的探测产物)。
|
||||
/// 对齐 env_snapshot.rs:64-80 的「spawn_blocking + timeout + 超时不 set」模式。
|
||||
async fn probe_pwsh() -> bool {
|
||||
if let Some(cached) = PWSH_CACHE.get() {
|
||||
return *cached;
|
||||
if let Some(cached) = cache_get() {
|
||||
return cached;
|
||||
}
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(3),
|
||||
tokio::task::spawn_blocking(probe_pwsh_blocking),
|
||||
).await {
|
||||
Ok(Ok(result)) => {
|
||||
// 正常完成:best-effort set(多任务竞态以先到者为准,均等价)
|
||||
let _ = PWSH_CACHE.set(result);
|
||||
// 正常完成:写入缓存(多任务竞态覆盖写,各写者探测值等价)
|
||||
cache_set(Some(result));
|
||||
result
|
||||
}
|
||||
Ok(Err(join_err)) => {
|
||||
@@ -155,18 +176,25 @@ mod tests {
|
||||
/// 死缓存回归测试:probe_pwsh 写入后,Default 同步路径必须读到同一缓存值。
|
||||
///
|
||||
/// 历史 bug:probe_pwsh_cached() 与 probe_pwsh() 各持独立 OnceLock,Default 读的永空。
|
||||
/// 本测试通过手动 set 模块级 PWSH_CACHE 后断言 default() 返回 Pwsh,锁定「写读同源」不变量。
|
||||
/// 本测试经 probe_pwsh 写模块级 PWSH_CACHE 后断言 default() 与探测值一致,锁定「写读同源」不变量。
|
||||
///
|
||||
/// 隔离:持串行闸,进时备份、断言前复位。OnceLock 时代写入后全进程不可清,
|
||||
/// 会污染并行的 env_snapshot 漂移测试(其快照先以空缓存创建,断言即漂移)。
|
||||
#[cfg(windows)]
|
||||
#[tokio::test]
|
||||
async fn probe_pwsh_cache_shared_with_default() {
|
||||
let _gate = PWSH_CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let backup = cache_get();
|
||||
// 探测一次填充缓存(无论机器是否装 pwsh,只要写读同源即应一致)。
|
||||
// 注:正常完成路径才会 set 缓存;本机 pwsh 探测不超时/不 panic,故 cached 应与 probed 一致。
|
||||
// 注:正常完成路径才会写缓存;本机 pwsh 探测不超时/不 panic,故 cached 应与 probed 一致。
|
||||
let probed = probe_pwsh().await;
|
||||
let cached = PWSH_CACHE.get().copied();
|
||||
// 探测完成后缓存必已填充(同步路径由此读到)
|
||||
assert_eq!(cached, Some(probed));
|
||||
let cached = cache_get();
|
||||
// Default 必须读到与 probe 一致的判定:Pwsh ↔ true,PowerShell ↔ false
|
||||
let default_shell = ShellType::default();
|
||||
// 复位放在断言前:中途 panic 不残留污染态(对齐 path_auth.rs 先复位再断言)
|
||||
cache_set(backup);
|
||||
// 探测完成后缓存必已填充(同步路径由此读到)
|
||||
assert_eq!(cached, Some(probed));
|
||||
match probed {
|
||||
true => assert_eq!(default_shell, ShellType::Pwsh),
|
||||
false => assert_eq!(default_shell, ShellType::PowerShell),
|
||||
@@ -179,14 +207,19 @@ mod tests {
|
||||
/// status() 永不返回 → spawn_blocking 线程永不返回 → probe_pwsh().await 永久挂 →
|
||||
/// execute()/execute_streaming() 卡死 → run_agentic_loop 死锁(与 env_snapshot::detect 同源 bug)。
|
||||
/// 本测试外包 10s timeout(远大于 3s 内部超时),无论探测成败都应在 10s 内返回,锁定「不挂死」不变量。
|
||||
///
|
||||
/// 隔离:持串行闸,探测在缓存为空时会写入缓存,备份/复位防污染并行测试。
|
||||
#[cfg(windows)]
|
||||
#[tokio::test]
|
||||
async fn probe_pwsh_completes_within_timeout() {
|
||||
let _gate = PWSH_CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let backup = cache_get();
|
||||
// 10s >> probe_pwsh 内部 3s 超时;若 10s 仍未返回 → 探测挂死,不变量被破坏。
|
||||
let result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
probe_pwsh(),
|
||||
).await;
|
||||
cache_set(backup);
|
||||
assert!(result.is_ok(), "probe_pwsh 必须在 10s 内返回(内部 3s 超时已兜底),不应挂死");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user