修复LLM原文日志并行测试污染
将原文日志目录收敛为LlmConfig实例配置 迁移platform-llm与api-server测试到独立日志目录 补充Rust异步测试环境变量隔离规范
This commit is contained in:
@@ -63,6 +63,14 @@
|
||||
- 验证:运行触达文件的定向 `vitest`,必要时追加 `npm run typecheck`、`npm run check:encoding` 和 `git diff --check`。
|
||||
- 关联:`docs/technical/【前端测试】React组件测试准则-2026-06-26.md`、`src/components/image-editor/useCanvasGenerationDialogs.test.tsx`、`src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx`。
|
||||
|
||||
## Rust 并行测试不要在 await 跨度内修改进程环境变量
|
||||
|
||||
- 现象:单独运行的异步测试稳定通过,默认并行运行整个 crate 时却看到临时目录多出其它测试的文件、文件对被拆散,或目录清理与并发写入互相竞争;Gitea Backend CI 可能表现为日志数量断言偶发增加。
|
||||
- 原因:`std::env::set_var` / `remove_var` 修改整个测试进程,不属于当前 async task。测试在 `await` 前设置目录、结束后恢复时,同一 test binary 的其它用例会在中间窗口读取该值;只锁修改环境变量的测试也无效,除非所有间接读取方都参与同一把锁。
|
||||
- 处理:文件、队列、缓存等副作用目录进入实例配置,在构造时一次性解析环境默认值,并允许测试显式注入唯一临时目录。不要靠 `--test-threads=1`、固定 sleep 或只过滤自己的文件名掩盖错误路由;纯环境解析测试只有在全部相关读写都封闭于同一 `OnceLock<Mutex<()>>` 时才使用全局锁。
|
||||
- 验证:先精确运行目标用例,再以默认并行度重复运行完整 crate;失败类测试同时执行时,各实例目录只能包含自己的输入 / 输出日志,测试结束后临时目录必须清理。
|
||||
- 关联:`server-rs/crates/platform-llm/src/lib.rs`、`server-rs/crates/api-server/src/creation_agent_llm_turn.rs`、`server-rs/crates/api-server/src/custom_world_foundation_draft.rs`。
|
||||
|
||||
## 带 objectKey 的画布图片测试要等待换签后可见
|
||||
|
||||
- 现象:测试点击“添加素材”后,图层状态已经写入,但立即用 `getByAltText('画布图片:...')` 偶发或稳定找不到图片;前一张图可能通过,紧接着添加的第二张失败。
|
||||
|
||||
@@ -359,9 +359,6 @@ mod tests {
|
||||
.expect("system time should be after epoch")
|
||||
.as_nanos()
|
||||
));
|
||||
unsafe {
|
||||
std::env::set_var("LLM_RAW_LOG_DIR", &log_dir);
|
||||
}
|
||||
let success_json = serde_json::json!({
|
||||
"replyText": "好,我们先把玩具王国定住。",
|
||||
"progressPercent": 12,
|
||||
@@ -405,7 +402,8 @@ mod tests {
|
||||
0,
|
||||
1,
|
||||
)
|
||||
.expect("LLM config should build");
|
||||
.expect("LLM config should build")
|
||||
.with_raw_log_dir(log_dir.clone());
|
||||
let llm_client = platform_llm::LlmClient::new(config).expect("LLM client should build");
|
||||
let mut visible_replies = Vec::new();
|
||||
|
||||
@@ -437,9 +435,6 @@ mod tests {
|
||||
assert!(requests[0].contains("\"web_search\""));
|
||||
assert!(!requests[1].contains("\"tools\""));
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("LLM_RAW_LOG_DIR");
|
||||
}
|
||||
if log_dir.exists() {
|
||||
fs::remove_dir_all(log_dir).expect("temporary LLM raw log dir should be removed");
|
||||
}
|
||||
|
||||
@@ -2587,9 +2587,6 @@ mod tests {
|
||||
.expect("system time should be after epoch")
|
||||
.as_nanos()
|
||||
));
|
||||
unsafe {
|
||||
std::env::set_var("LLM_RAW_LOG_DIR", &log_dir);
|
||||
}
|
||||
let request_capture = Arc::new(Mutex::new(Vec::new()));
|
||||
let server_url = spawn_mock_server_with_statuses(
|
||||
request_capture.clone(),
|
||||
@@ -2604,7 +2601,7 @@ mod tests {
|
||||
},
|
||||
],
|
||||
);
|
||||
let llm_client = build_test_llm_client(server_url);
|
||||
let llm_client = build_test_llm_client_with_raw_log_dir(server_url, log_dir.clone());
|
||||
|
||||
let parsed = request_foundation_json_stage(
|
||||
&llm_client,
|
||||
@@ -2628,9 +2625,6 @@ mod tests {
|
||||
assert!(requests[0].contains("\"web_search\""));
|
||||
assert!(!requests[1].contains("\"tools\""));
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("LLM_RAW_LOG_DIR");
|
||||
}
|
||||
if log_dir.exists() {
|
||||
std::fs::remove_dir_all(log_dir).expect("temporary LLM raw log dir should be removed");
|
||||
}
|
||||
@@ -3256,7 +3250,19 @@ mod tests {
|
||||
}
|
||||
|
||||
fn build_test_llm_client(base_url: String) -> LlmClient {
|
||||
let config = LlmConfig::new(
|
||||
LlmClient::new(build_test_llm_config(base_url)).expect("llm client should build")
|
||||
}
|
||||
|
||||
fn build_test_llm_client_with_raw_log_dir(
|
||||
base_url: String,
|
||||
raw_log_dir: std::path::PathBuf,
|
||||
) -> LlmClient {
|
||||
let config = build_test_llm_config(base_url).with_raw_log_dir(raw_log_dir);
|
||||
LlmClient::new(config).expect("llm client should build")
|
||||
}
|
||||
|
||||
fn build_test_llm_config(base_url: String) -> LlmConfig {
|
||||
LlmConfig::new(
|
||||
LlmProvider::Ark,
|
||||
base_url,
|
||||
"test-key".to_string(),
|
||||
@@ -3265,9 +3271,7 @@ mod tests {
|
||||
0,
|
||||
1,
|
||||
)
|
||||
.expect("llm config should build");
|
||||
|
||||
LlmClient::new(config).expect("llm client should build")
|
||||
.expect("llm config should build")
|
||||
}
|
||||
|
||||
fn spawn_mock_server(
|
||||
|
||||
@@ -40,6 +40,7 @@ pub struct LlmConfig {
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
model: String,
|
||||
raw_log_dir: PathBuf,
|
||||
request_timeout_ms: u64,
|
||||
max_retries: u32,
|
||||
retry_backoff_ms: u64,
|
||||
@@ -426,6 +427,9 @@ impl LlmConfig {
|
||||
let base_url = normalize_non_empty(base_url, "LLM base_url 不能为空")?;
|
||||
let api_key = normalize_non_empty(api_key, "LLM api_key 不能为空")?;
|
||||
let model = normalize_non_empty(model, "LLM model 不能为空")?;
|
||||
let raw_log_dir = env::var("LLM_RAW_LOG_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from(DEFAULT_LLM_RAW_LOG_DIR));
|
||||
|
||||
if request_timeout_ms == 0 {
|
||||
return Err(LlmError::InvalidConfig(
|
||||
@@ -438,6 +442,7 @@ impl LlmConfig {
|
||||
base_url,
|
||||
api_key,
|
||||
model,
|
||||
raw_log_dir,
|
||||
request_timeout_ms,
|
||||
max_retries,
|
||||
retry_backoff_ms,
|
||||
@@ -450,6 +455,11 @@ impl LlmConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_raw_log_dir(mut self, raw_log_dir: impl Into<PathBuf>) -> Self {
|
||||
self.raw_log_dir = raw_log_dir.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ark_default(api_key: String, model: String) -> Result<Self, LlmError> {
|
||||
Self::new(
|
||||
LlmProvider::Ark,
|
||||
@@ -478,6 +488,10 @@ impl LlmConfig {
|
||||
&self.model
|
||||
}
|
||||
|
||||
fn raw_log_dir(&self) -> &PathBuf {
|
||||
&self.raw_log_dir
|
||||
}
|
||||
|
||||
pub fn request_timeout_ms(&self) -> u64 {
|
||||
self.request_timeout_ms
|
||||
}
|
||||
@@ -1368,9 +1382,7 @@ fn write_llm_raw_failure(
|
||||
failure_stage: &str,
|
||||
raw_output: &str,
|
||||
) -> Result<(), String> {
|
||||
let log_dir = env::var("LLM_RAW_LOG_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from(DEFAULT_LLM_RAW_LOG_DIR));
|
||||
let log_dir = config.raw_log_dir();
|
||||
fs::create_dir_all(&log_dir).map_err(|error| format!("创建日志目录失败:{error}"))?;
|
||||
|
||||
let prefix = build_llm_raw_log_prefix(failure_stage);
|
||||
@@ -2354,9 +2366,6 @@ mod tests {
|
||||
"platform-llm-raw-log-test-{}",
|
||||
build_llm_raw_log_prefix("parse_error")
|
||||
));
|
||||
unsafe {
|
||||
std::env::set_var("LLM_RAW_LOG_DIR", &log_dir);
|
||||
}
|
||||
|
||||
let server_url = spawn_mock_server(vec![MockResponse {
|
||||
status_line: "200 OK",
|
||||
@@ -2365,7 +2374,8 @@ mod tests {
|
||||
extra_headers: Vec::new(),
|
||||
}]);
|
||||
|
||||
let client = build_test_client(server_url, 0);
|
||||
let config = build_test_config(server_url, 0).with_raw_log_dir(log_dir.clone());
|
||||
let client = LlmClient::new(config).expect("client should be created");
|
||||
let error = client
|
||||
.request_single_message_text("系统原文", "用户原文")
|
||||
.await
|
||||
@@ -2398,14 +2408,15 @@ mod tests {
|
||||
assert!(!input_text.contains("test-key"));
|
||||
assert_eq!(output_text, "不是合法 JSON");
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("LLM_RAW_LOG_DIR");
|
||||
}
|
||||
fs::remove_dir_all(log_dir).expect("log dir should be removed");
|
||||
}
|
||||
|
||||
fn build_test_client(base_url: String, max_retries: u32) -> LlmClient {
|
||||
let config = LlmConfig::new(
|
||||
LlmClient::new(build_test_config(base_url, max_retries)).expect("client should be created")
|
||||
}
|
||||
|
||||
fn build_test_config(base_url: String, max_retries: u32) -> LlmConfig {
|
||||
LlmConfig::new(
|
||||
LlmProvider::Ark,
|
||||
base_url,
|
||||
"test-key".to_string(),
|
||||
@@ -2414,9 +2425,7 @@ mod tests {
|
||||
max_retries,
|
||||
1,
|
||||
)
|
||||
.expect("config should be valid");
|
||||
|
||||
LlmClient::new(config).expect("client should be created")
|
||||
.expect("config should be valid")
|
||||
}
|
||||
|
||||
fn spawn_mock_server(responses: Vec<MockResponse>) -> String {
|
||||
|
||||
Reference in New Issue
Block a user