feat: 编辑器实时探测并显示已关联虚拟环境

fix: 按系统 ANSI 代码页解码子进程输出,修复中文日志乱码
This commit is contained in:
2026-10-24 22:30:10 +08:00
parent 23565b40a4
commit dd7612bc74
20 changed files with 1508 additions and 48 deletions
+102
View File
@@ -719,3 +719,105 @@ async fn scheduler_restores_enabled_tasks_after_restart() {
second_state.shutdown();
second_pool.close().await;
}
async fn run_command_task(app: &TestApp, id: &str, args: Vec<&str>) -> i64 {
let working_dir = app._temp_dir.path().to_string_lossy().into_owned();
let task = script_task(id, &working_dir, "cmd.exe", "/C", args, "0 0 2 * * *", 10);
let create_response = send_json(&app.router, Method::POST, "/api/v1/tasks", Some(task)).await;
assert_eq!(create_response.status(), StatusCode::CREATED);
let run_response = send_json(
&app.router,
Method::POST,
&format!("/api/v1/tasks/{id}/run"),
None,
)
.await;
assert_eq!(run_response.status(), StatusCode::ACCEPTED);
response_json(run_response).await["id"]
.as_i64()
.expect("run id should be an integer")
}
async fn read_log_chunk(app: &TestApp, run_id: i64, offset: u64) -> Value {
let response = send_json(
&app.router,
Method::GET,
&format!("/api/v1/runs/{run_id}/logs?offset={offset}"),
None,
)
.await;
assert_eq!(response.status(), StatusCode::OK);
response_json(response).await
}
#[tokio::test]
async fn gbk_encoded_output_is_decoded_to_utf8() {
let app = test_app().await;
// 固定的 GBK 字节只在中日韩等使用该代码页的系统上才有意义。
let encoding = task_pilot_backend::runs::executor::OutputEncoding::from_env();
if encoding.name() != "GBK" {
return;
}
let payload = app._temp_dir.path().join("gbk.txt");
// "中文" 的 GBK 编码
std::fs::write(&payload, [0xD6, 0xD0, 0xCE, 0xC4]).expect("GBK fixture should be written");
let run_id = run_command_task(&app, "gbk_task", vec!["type", "gbk.txt"]).await;
let completed = wait_for_run(&app.router, run_id).await;
assert_eq!(completed["status"], "succeeded");
let log = read_log_chunk(&app, run_id, 0).await;
let content = log["content"].as_str().expect("log content should be text");
assert!(
content.contains("中文"),
"unexpected log content: {content}"
);
assert!(!content.contains('\u{fffd}'));
app.state.shutdown();
}
#[tokio::test]
async fn long_utf8_log_is_reassembled_across_chunk_boundaries() {
let app = test_app().await;
let payload = app._temp_dir.path().join("long.txt");
let body = format!("{}\n尾巴\n", "中".repeat(30_000));
std::fs::write(&payload, body.as_bytes()).expect("UTF-8 fixture should be written");
let run_id = run_command_task(&app, "long_log_task", vec!["type", "long.txt"]).await;
let completed = wait_for_run(&app.router, run_id).await;
assert_eq!(completed["status"], "succeeded");
let first = read_log_chunk(&app, run_id, 0).await;
assert_eq!(
first["complete"], false,
"a terminal run must not report completion before the whole log is read"
);
let mut content = first["content"].as_str().unwrap_or_default().to_owned();
let mut offset = first["next_offset"]
.as_u64()
.expect("next_offset should be an integer");
for _ in 0..20 {
let chunk = read_log_chunk(&app, run_id, offset).await;
content.push_str(chunk["content"].as_str().unwrap_or_default());
offset = chunk["next_offset"]
.as_u64()
.expect("next_offset should be an integer");
if chunk["complete"].as_bool().unwrap_or(false) {
break;
}
}
assert!(
!content.contains('\u{fffd}'),
"log lost a character boundary"
);
assert!(content.contains("尾巴"));
assert_eq!(
content.len(),
usize::try_from(offset).expect("offset fits usize")
);
app.state.shutdown();
}