添加qiming-rcoder模块
This commit is contained in:
648
qiming-rcoder/crates/rcoder/src/handler/agent_cancel_handler.rs
Normal file
648
qiming-rcoder/crates/rcoder/src/handler/agent_cancel_handler.rs
Normal file
@@ -0,0 +1,648 @@
|
||||
//! Agent任务取消处理器
|
||||
//!
|
||||
//! 转发取消请求到容器内的 agent_runner 服务
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::http::HeaderMap;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::router::AppState;
|
||||
use docker_manager::ContainerBasicInfo;
|
||||
use shared_types::{AppError, AgentCancelRequest, AgentCancelResponse, ComputerAgentCancelRequest, HttpResult};
|
||||
|
||||
use super::utils::{I18nJsonOrQuery, extract_grpc_addr, get_locale_from_headers};
|
||||
|
||||
/// Computer Agent 取消任务的查询参数(仅用于测试)
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct ComputerCancelQuery {
|
||||
/// 用户ID,用于标识特定的用户容器(ComputerAgentRunner模式,可与 pod_id 二选一)
|
||||
#[schema(example = "user_123")]
|
||||
pub user_id: Option<String>,
|
||||
/// 项目ID,必填,用于标识要取消的特定项目的 agent
|
||||
#[schema(example = "project456")]
|
||||
pub project_id: String,
|
||||
/// 会话ID,用于标识要取消的会话(可选)
|
||||
#[serde(default)]
|
||||
#[schema(example = "session789")]
|
||||
pub session_id: Option<String>,
|
||||
/// Pod ID,用于共享容器模式下的容器定位(可选)
|
||||
#[serde(default)]
|
||||
pub pod_id: Option<String>,
|
||||
/// 租户ID(可选)
|
||||
#[serde(default, deserialize_with = "shared_types::flexible_string::flexible_string")]
|
||||
pub tenant_id: Option<String>,
|
||||
/// 空间ID(可选)
|
||||
#[serde(default, deserialize_with = "shared_types::flexible_string::flexible_string")]
|
||||
pub space_id: Option<String>,
|
||||
/// 隔离类型(可选),如 "project", "tenant", "space"
|
||||
#[serde(default)]
|
||||
pub isolation_type: Option<String>,
|
||||
}
|
||||
|
||||
/// 取消操作的标识符
|
||||
#[derive(Debug, Clone)]
|
||||
enum CancelIdentifier {
|
||||
/// RCoder 模式:使用 project_id
|
||||
Project(String),
|
||||
/// ComputerAgentRunner 模式:使用 user_id
|
||||
User(String),
|
||||
/// 共享容器模式:使用 pod_id
|
||||
Pod(String),
|
||||
}
|
||||
|
||||
/// 统一的容器查询函数 - 通过 DuckDB 查询
|
||||
async fn get_container_for_cancel_duckdb(
|
||||
state: &AppState,
|
||||
identifier: &CancelIdentifier,
|
||||
) -> Result<Option<ContainerBasicInfo>, AppError> {
|
||||
let identifier_display = match identifier {
|
||||
CancelIdentifier::Project(pid) => format!("project_id={}", pid),
|
||||
CancelIdentifier::User(uid) => format!("user_id={}", uid),
|
||||
CancelIdentifier::Pod(pod_id) => format!("pod_id={}", pod_id),
|
||||
};
|
||||
|
||||
info!(
|
||||
"🔍 [CANCEL_CONTAINER_DUCKDB] Looking up container: {}",
|
||||
identifier_display
|
||||
);
|
||||
|
||||
let container_info = match identifier {
|
||||
CancelIdentifier::Project(project_id) => {
|
||||
// RCoder 模式:直接通过 project_id 查询
|
||||
// ProjectAdapter.get() 内部会调用 get_container_for_project
|
||||
state
|
||||
.get_project(project_id)
|
||||
.and_then(|info| info.container().cloned())
|
||||
}
|
||||
CancelIdentifier::User(user_id) => {
|
||||
// ComputerAgentRunner 模式:通过 user_id 查询容器
|
||||
// 使用新添加的 get_container_by_user_id 方法
|
||||
state.projects.get_container_by_user_id(user_id)
|
||||
}
|
||||
CancelIdentifier::Pod(pod_id) => {
|
||||
// 共享容器模式:通过 pod_id 查询容器
|
||||
// 目前暂时使用 get_container_by_user_id 作为占位,后续需要实现 get_container_by_pod_id
|
||||
state.projects.get_container_by_pod_id(pod_id)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref info) = container_info {
|
||||
info!(
|
||||
"✅ [CANCEL_CONTAINER_DUCKDB] Container found: {}, container_id={}, service_url={}",
|
||||
identifier_display, info.container_id, info.service_url
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"ℹ️ [CANCEL_CONTAINER_DUCKDB] Container not found: {}, no need to cancel",
|
||||
identifier_display
|
||||
);
|
||||
}
|
||||
|
||||
Ok(container_info)
|
||||
}
|
||||
|
||||
/// 转发取消请求到容器内的 agent_runner 服务
|
||||
///
|
||||
/// 🎯 使用 gRPC CancelSession RPC 替代 HTTP 转发
|
||||
async fn forward_cancel_request_to_container_service(
|
||||
project_id: &str,
|
||||
session_id: Option<&str>,
|
||||
container_info: &ContainerBasicInfo,
|
||||
grpc_pool: &Arc<crate::grpc::GrpcChannelPool>,
|
||||
locale: &'static str,
|
||||
rcoder_prefix: &str,
|
||||
computer_prefix: &str,
|
||||
) -> Result<HttpResult<AgentCancelResponse>, AppError> {
|
||||
let session_id_display = session_id
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "None".to_string());
|
||||
info!(
|
||||
"📤 [CANCEL_FORWARD] Forwarding cancel request to container (gRPC): project_id={}, session_id={}, container_id={}",
|
||||
project_id, session_id_display, container_info.container_id
|
||||
);
|
||||
|
||||
// 🎯 使用 gRPC 替代 HTTP
|
||||
// 从 service_url 提取 gRPC 地址
|
||||
let grpc_addr = extract_grpc_addr(&container_info.service_url)?;
|
||||
|
||||
info!(
|
||||
"📡 [CANCEL_FORWARD] Sending gRPC cancel request to: {}, session_id={}",
|
||||
grpc_addr, session_id_display
|
||||
);
|
||||
|
||||
// 构建 session_id(如果未提供则使用空字符串,由 Agent Runner 根据 project_id 查找)
|
||||
let session_id_str = session_id.unwrap_or("").to_string();
|
||||
let reason = "User requested cancellation".to_string();
|
||||
|
||||
// 调用 gRPC CancelSession
|
||||
match crate::grpc::grpc_cancel_session_with_pool(
|
||||
grpc_pool,
|
||||
&grpc_addr,
|
||||
session_id_str.clone(),
|
||||
reason,
|
||||
project_id.to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(grpc_response) => {
|
||||
if grpc_response.success {
|
||||
info!(
|
||||
"✅ [CANCEL_FORWARD] gRPC cancel succeeded: session_id={}",
|
||||
session_id_str
|
||||
);
|
||||
Ok(HttpResult::success(AgentCancelResponse {
|
||||
success: true,
|
||||
session_id: session_id_str,
|
||||
}))
|
||||
} else {
|
||||
let error_msg = grpc_response
|
||||
.message
|
||||
.unwrap_or_else(|| "Unknown error".to_string());
|
||||
error!("[CANCEL_FORWARD] gRPC cancelfailed: {}", error_msg);
|
||||
Ok(HttpResult::error_with_locale(
|
||||
shared_types::error_codes::ERR_CANCEL_FAILED,
|
||||
locale,
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("[CANCEL_FORWARD] gRPC call failed: {}", e);
|
||||
|
||||
// 检查特定的 gRPC 错误码并分类处理
|
||||
if let Some(status) = crate::grpc::extract_grpc_status(&e) {
|
||||
use tonic::Code;
|
||||
match status.code() {
|
||||
Code::NotFound => {
|
||||
// 会话或 Agent 不存在,返回成功(幂等设计)
|
||||
info!("[CANCEL_FORWARD] Session not found, cancel succeeded");
|
||||
return Ok(HttpResult::success(AgentCancelResponse {
|
||||
success: true,
|
||||
session_id: session_id.unwrap_or("").to_string(),
|
||||
}));
|
||||
}
|
||||
Code::Unavailable => {
|
||||
// Agent Worker 不可用,需要判断是容器已销毁还是临时故障
|
||||
// 通过 Docker API 检查容器是否真的存在
|
||||
let container_exists = check_container_exists_by_info(container_info, rcoder_prefix, computer_prefix).await;
|
||||
|
||||
if !container_exists {
|
||||
// 容器已销毁,取消目标已达成(幂等设计)
|
||||
info!(
|
||||
"[CANCEL_FORWARD] container already destroyed, cancel request already completed"
|
||||
);
|
||||
return Ok(HttpResult::success(AgentCancelResponse {
|
||||
success: true,
|
||||
session_id: session_id.unwrap_or("").to_string(),
|
||||
}));
|
||||
} else {
|
||||
// 容器存在但服务不可用(可能是临时故障),返回错误
|
||||
warn!(
|
||||
"[CANCEL_FORWARD] Agent Worker unavailable (container exists, may be temporary failure)"
|
||||
);
|
||||
return Ok(HttpResult::error_with_locale(
|
||||
shared_types::error_codes::ERR_SERVICE_UNAVAILABLE,
|
||||
locale,
|
||||
));
|
||||
}
|
||||
}
|
||||
other_code => {
|
||||
// 其他 gRPC 状态码
|
||||
error!("[CANCEL_FORWARD] gRPC error code: {:?}", other_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 其他 gRPC 通信失败(网络错误等)
|
||||
return Ok(HttpResult::error_with_locale(
|
||||
shared_types::error_codes::ERR_GRPC_ERROR,
|
||||
locale,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查容器是否真实存在(通过 Docker API)
|
||||
///
|
||||
/// 用于区分 Unavailable 错误的原因:
|
||||
/// - 容器已销毁 → 返回 false(取消目标已达成)
|
||||
/// - 容器存在但服务不可用 → 返回 true(临时故障)
|
||||
///
|
||||
/// 使用容器名称而非 ID,因为容器重启后 ID 会变,但名称不变
|
||||
async fn check_container_exists_by_info(
|
||||
container_info: &ContainerBasicInfo,
|
||||
rcoder_prefix: &str,
|
||||
computer_prefix: &str,
|
||||
) -> bool {
|
||||
match docker_manager::runtime::RuntimeManager::get().await {
|
||||
Ok(runtime) => {
|
||||
// 使用配置化的前缀,而不是硬编码的 ServiceType::container_prefix()
|
||||
|
||||
let query = if let Some(identifier) = container_info
|
||||
.container_name
|
||||
.strip_prefix(&format!("{}-", computer_prefix))
|
||||
{
|
||||
runtime
|
||||
.get_container_info_by_identifier(
|
||||
identifier,
|
||||
&shared_types::ServiceType::ComputerAgentRunner,
|
||||
)
|
||||
.await
|
||||
} else if let Some(identifier) = container_info
|
||||
.container_name
|
||||
.strip_prefix(&format!("{}-", rcoder_prefix))
|
||||
{
|
||||
runtime
|
||||
.get_container_info_by_identifier(identifier, &shared_types::ServiceType::RCoder)
|
||||
.await
|
||||
} else {
|
||||
return true;
|
||||
};
|
||||
|
||||
match query {
|
||||
Ok(Some(info)) => {
|
||||
debug!(
|
||||
"🔍 [CANCEL_FORWARD] Runtime container exists: name={}, id={}",
|
||||
info.container_name, info.container_id
|
||||
);
|
||||
true
|
||||
}
|
||||
Ok(None) => {
|
||||
info!(
|
||||
"🔍 [CANCEL_FORWARD] Runtime container not found (already destroyed): {}",
|
||||
container_info.container_name
|
||||
);
|
||||
false
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"⚠️ [CANCEL_FORWARD] Failed to query runtime container status: {}, conservatively assuming container exists",
|
||||
e
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// 无法获取 runtime,保守地认为容器存在
|
||||
warn!(
|
||||
"[CANCEL_FORWARD] Failed to get runtime: {}, conservatively assuming container exists",
|
||||
e
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 内部核心处理函数 v2:处理会话取消请求(支持多种服务类型)
|
||||
///
|
||||
/// 使用 DuckDB 统一查询,支持 RCoder 和 ComputerAgentRunner 两种模式
|
||||
async fn handle_session_cancel_internal_v2(
|
||||
state: &AppState,
|
||||
identifier: CancelIdentifier,
|
||||
project_id: String, // 必填:传递给 agent_runner 的项目ID
|
||||
session_id: Option<String>, // 可选:会话ID
|
||||
locale: &'static str,
|
||||
) -> Result<HttpResult<AgentCancelResponse>, AppError> {
|
||||
let session_id_display = session_id
|
||||
.as_deref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "None".to_string());
|
||||
|
||||
let identifier_display = match &identifier {
|
||||
CancelIdentifier::Project(pid) => format!("project_id={}", pid),
|
||||
CancelIdentifier::User(uid) => format!("user_id={}", uid),
|
||||
CancelIdentifier::Pod(pod_id) => format!("pod_id={}", pod_id),
|
||||
};
|
||||
|
||||
info!(
|
||||
"🛑 [CANCEL_FORWARD_V2] Received cancel task request: session_id={}, project_id={}, {}",
|
||||
session_id_display, project_id, identifier_display
|
||||
);
|
||||
|
||||
// 获取容器(不创建)
|
||||
let container_info = get_container_for_cancel_duckdb(state, &identifier).await?;
|
||||
|
||||
// 如果容器不存在,说明任务已经结束或从未启动,直接返回成功
|
||||
let Some(container_info) = container_info else {
|
||||
info!(
|
||||
"✅ [CANCEL_FORWARD_V2] Container not found, cancel target already achieved: {}",
|
||||
identifier_display
|
||||
);
|
||||
return Ok(HttpResult::success(AgentCancelResponse {
|
||||
success: true,
|
||||
session_id: session_id.unwrap_or_else(|| "all".to_string()),
|
||||
}));
|
||||
};
|
||||
|
||||
// 转发取消请求到容器服务
|
||||
let result = forward_cancel_request_to_container_service(
|
||||
&project_id, // 使用传入的 project_id
|
||||
session_id.as_deref(),
|
||||
&container_info,
|
||||
&state.grpc_pool,
|
||||
locale,
|
||||
&state.container_prefix_rcoder,
|
||||
&state.container_prefix_computer,
|
||||
)
|
||||
.await;
|
||||
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"✅ [CANCEL_FORWARD_V2] Cancel request handled successfully: project_id={}, {}",
|
||||
project_id, identifier_display
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"❌ [CANCEL_FORWARD_V2] Cancel request handling failed: project_id={}, {}, error={}",
|
||||
project_id, identifier_display, e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// 处理agent任务取消请求
|
||||
///
|
||||
/// 转发取消请求到容器内的 agent_runner 服务(使用 gRPC)
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/session/cancel",
|
||||
request_body = AgentCancelRequest,
|
||||
responses(
|
||||
(
|
||||
status = 200,
|
||||
description = "成功转发取消请求到容器",
|
||||
body = HttpResult<AgentCancelResponse>,
|
||||
example = json!({
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true,
|
||||
"session_id": "session456"
|
||||
},
|
||||
"error": null
|
||||
})
|
||||
),
|
||||
(
|
||||
status = 400,
|
||||
description = "请求参数错误",
|
||||
body = HttpResult<String>,
|
||||
example = json!({
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "INVALID_PARAMS",
|
||||
"message": "Invalid project_id or session_id"
|
||||
}
|
||||
})
|
||||
),
|
||||
(
|
||||
status = 401,
|
||||
description = "API Key 鉴权失败",
|
||||
body = HttpResult<String>
|
||||
),
|
||||
(
|
||||
status = 404,
|
||||
description = "未找到对应的项目或会话",
|
||||
body = HttpResult<String>,
|
||||
example = json!({
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "PROJECT_NOT_FOUND",
|
||||
"message": "Project or session not found"
|
||||
}
|
||||
})
|
||||
),
|
||||
(
|
||||
status = 500,
|
||||
description = "转发取消请求失败",
|
||||
body = HttpResult<String>,
|
||||
example = json!({
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "CANCEL_FAILED",
|
||||
"message": "Failed to forward cancel request to container"
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
tag = "agent",
|
||||
operation_id = "agent_session_cancel",
|
||||
summary = "转发Agent任务取消请求(gRPC)",
|
||||
description = "将取消请求通过 gRPC 转发到容器内的 agent_runner 服务"
|
||||
)]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn agent_session_cancel(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
I18nJsonOrQuery(request): I18nJsonOrQuery<AgentCancelRequest>,
|
||||
) -> Result<HttpResult<AgentCancelResponse>, AppError> {
|
||||
let locale = get_locale_from_headers(&headers);
|
||||
|
||||
// 使用 garde 进行字段校验
|
||||
let I18nJsonOrQuery(request) = I18nJsonOrQuery(request).validate_into_app_error()?;
|
||||
let project_id = request.project_id.as_ref().expect("validated: project_id is required and non-empty");
|
||||
|
||||
info!(
|
||||
"🚫 [CANCEL] Agent cancel request: project_id={}, session_id={:?}",
|
||||
project_id, request.session_id
|
||||
);
|
||||
|
||||
handle_session_cancel_internal_v2(
|
||||
&state,
|
||||
CancelIdentifier::Project(project_id.to_string()),
|
||||
project_id.to_string(),
|
||||
request.session_id,
|
||||
locale,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 处理 Computer Agent 任务取消请求
|
||||
///
|
||||
/// 转发取消请求到容器内的 agent_runner 服务(使用 gRPC)
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/computer/agent/session/cancel",
|
||||
request_body = ComputerAgentCancelRequest,
|
||||
responses(
|
||||
(
|
||||
status = 200,
|
||||
description = "成功转发取消请求到容器",
|
||||
body = HttpResult<AgentCancelResponse>,
|
||||
example = json!({
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true,
|
||||
"session_id": "session456"
|
||||
},
|
||||
"error": null
|
||||
})
|
||||
),
|
||||
(
|
||||
status = 400,
|
||||
description = "请求参数错误",
|
||||
body = HttpResult<String>,
|
||||
example = json!({
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "ERR_VALIDATION",
|
||||
"message": "user_id 或 pod_id is required"
|
||||
}
|
||||
})
|
||||
),
|
||||
(
|
||||
status = 401,
|
||||
description = "API Key 鉴权失败",
|
||||
body = HttpResult<String>
|
||||
),
|
||||
(
|
||||
status = 404,
|
||||
description = "未找到对应的用户容器或会话",
|
||||
body = HttpResult<String>,
|
||||
example = json!({
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "CONTAINER_NOT_FOUND",
|
||||
"message": "User container not found"
|
||||
}
|
||||
})
|
||||
),
|
||||
(
|
||||
status = 500,
|
||||
description = "转发取消请求失败",
|
||||
body = HttpResult<String>,
|
||||
example = json!({
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "CANCEL_FAILED",
|
||||
"message": "Failed to forward cancel request to container"
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
tag = "computer",
|
||||
operation_id = "computer_agent_session_cancel",
|
||||
summary = "转发 Computer Agent 任务取消请求(支持 user_id)",
|
||||
description = "将 Computer Agent 取消请求通过 gRPC 转发到容器内的 agent_runner 服务,支持通过 user_id 或 pod_id 定位用户容器"
|
||||
)]
|
||||
#[instrument(skip(state), fields(user_id = ?request.user_id.as_ref().map(|s| s.as_str()), project_id = %request.project_id, pod_id = ?request.pod_id.as_ref().map(|s| s.as_str())))]
|
||||
pub async fn computer_agent_session_cancel(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
I18nJsonOrQuery(request): I18nJsonOrQuery<ComputerAgentCancelRequest>,
|
||||
) -> Result<HttpResult<AgentCancelResponse>, AppError> {
|
||||
let locale = get_locale_from_headers(&headers);
|
||||
|
||||
// 验证 user_id 或 pod_id 至少有一个
|
||||
let has_user_id = request.user_id.as_ref().map(|s| !s.trim().is_empty()).unwrap_or(false);
|
||||
let has_pod_id = request.pod_id.as_ref().map(|s| !s.trim().is_empty()).unwrap_or(false);
|
||||
if !has_user_id && !has_pod_id {
|
||||
error!("[COMPUTER_CANCEL] user_id or pod_id is required");
|
||||
return Ok(HttpResult::error_with_locale(
|
||||
shared_types::error_codes::ERR_VALIDATION,
|
||||
locale,
|
||||
));
|
||||
}
|
||||
|
||||
// 验证 project_id 不为空
|
||||
if request.project_id.trim().is_empty() {
|
||||
error!("[COMPUTER_CANCEL] project_id is required");
|
||||
return Ok(HttpResult::error_with_locale(
|
||||
shared_types::error_codes::ERR_VALIDATION,
|
||||
locale,
|
||||
));
|
||||
}
|
||||
|
||||
info!(
|
||||
"🚀 [COMPUTER_CANCEL] Starting to process cancel request: user_id={:?}, pod_id={:?}, project_id={}, session_id={:?}",
|
||||
request.user_id, request.pod_id, request.project_id, request.session_id
|
||||
);
|
||||
|
||||
// 使用 user_id 或 pod_id 来构建 CancelIdentifier
|
||||
let identifier = if has_user_id {
|
||||
CancelIdentifier::User(request.user_id.clone().unwrap())
|
||||
} else {
|
||||
CancelIdentifier::Pod(request.pod_id.clone().unwrap())
|
||||
};
|
||||
|
||||
handle_session_cancel_internal_v2(
|
||||
&state,
|
||||
identifier,
|
||||
request.project_id, // 必填的 project_id
|
||||
request.session_id,
|
||||
locale,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_computer_cancel_query_deserialization() {
|
||||
// 测试 ComputerCancelQuery 反序列化
|
||||
let query_json = json!({
|
||||
"user_id": "user_123",
|
||||
"project_id": "project_456",
|
||||
"session_id": "session_789"
|
||||
});
|
||||
|
||||
let query: ComputerCancelQuery = serde_json::from_value(query_json).unwrap();
|
||||
assert_eq!(query.user_id, Some("user_123".to_string()));
|
||||
assert_eq!(query.project_id, "project_456");
|
||||
assert_eq!(query.session_id, Some("session_789".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_computer_cancel_query_optional_session() {
|
||||
// 测试不带 session_id 的情况
|
||||
let query_json = json!({
|
||||
"user_id": "user_123",
|
||||
"project_id": "project_456"
|
||||
});
|
||||
|
||||
let query: ComputerCancelQuery = serde_json::from_value(query_json).unwrap();
|
||||
assert_eq!(query.user_id, Some("user_123".to_string()));
|
||||
assert_eq!(query.project_id, "project_456");
|
||||
assert_eq!(query.session_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cancel_identifier_display() {
|
||||
// 测试 CancelIdentifier 的显示格式
|
||||
let project_id = CancelIdentifier::Project("test_project".to_string());
|
||||
let user_id = CancelIdentifier::User("test_user".to_string());
|
||||
let pod_id = CancelIdentifier::Pod("test_pod".to_string());
|
||||
|
||||
let display = match &project_id {
|
||||
CancelIdentifier::Project(pid) => format!("project_id={}", pid),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(display, "project_id=test_project");
|
||||
|
||||
let display = match &user_id {
|
||||
CancelIdentifier::User(uid) => format!("user_id={}", uid),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(display, "user_id=test_user");
|
||||
|
||||
let display = match &pod_id {
|
||||
CancelIdentifier::Pod(pod_id) => format!("pod_id={}", pod_id),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(display, "pod_id=test_pod");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user