From 24b10f066095ffe0ab20a995ca395faee46b37ab Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 23 Jul 2026 15:38:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DLLM=E5=8E=9F=E6=96=87?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=B9=B6=E8=A1=8C=E6=B5=8B=E8=AF=95=E6=B1=A1?= =?UTF-8?q?=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原文日志目录收敛为LlmConfig实例配置 迁移platform-llm与api-server测试到独立日志目录 补充Rust异步测试环境变量隔离规范 --- docs/project-memory/shared-memory/pitfalls.md | 8 ++++ .../api-server/src/creation_agent_llm_turn.rs | 9 +---- .../src/custom_world_foundation_draft.rs | 26 +++++++------ server-rs/crates/platform-llm/src/lib.rs | 37 ++++++++++++------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index e31c1c8cd..a3eb37fbe 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -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>` 时才使用全局锁。 +- 验证:先精确运行目标用例,再以默认并行度重复运行完整 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('画布图片:...')` 偶发或稳定找不到图片;前一张图可能通过,紧接着添加的第二张失败。 diff --git a/server-rs/crates/api-server/src/creation_agent_llm_turn.rs b/server-rs/crates/api-server/src/creation_agent_llm_turn.rs index 86bbfa93a..8c9d12c41 100644 --- a/server-rs/crates/api-server/src/creation_agent_llm_turn.rs +++ b/server-rs/crates/api-server/src/creation_agent_llm_turn.rs @@ -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"); } diff --git a/server-rs/crates/api-server/src/custom_world_foundation_draft.rs b/server-rs/crates/api-server/src/custom_world_foundation_draft.rs index fb5a93e14..c022db3d8 100644 --- a/server-rs/crates/api-server/src/custom_world_foundation_draft.rs +++ b/server-rs/crates/api-server/src/custom_world_foundation_draft.rs @@ -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( diff --git a/server-rs/crates/platform-llm/src/lib.rs b/server-rs/crates/platform-llm/src/lib.rs index 8c5e41aae..fbfd0f54d 100644 --- a/server-rs/crates/platform-llm/src/lib.rs +++ b/server-rs/crates/platform-llm/src/lib.rs @@ -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) -> Self { + self.raw_log_dir = raw_log_dir.into(); + self + } + pub fn ark_default(api_key: String, model: String) -> Result { 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) -> String {