use super::{ any, build_router, build_router_with_execution_runtime_override, json, start_server, Arc, Body, Bytes, HeaderName, HeaderValue, Infallible, Json, Mutex, Request, Response, Router, StatusCode, CONTROL_EXECUTED_HEADER, CONTROL_EXECUTE_FALLBACK_HEADER, DEPENDENCY_REASON_HEADER, EXECUTION_PATH_HEADER, EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS, TRACE_ID_HEADER, }; #[tokio::test] async fn gateway_locally_denies_sync_ai_control_execute_when_opted_in_and_execution_runtime_missing( ) { let execute_hits = Arc::new(Mutex::new(0usize)); let execute_hits_clone = Arc::clone(&execute_hits); let public_hits = Arc::new(Mutex::new(0usize)); let public_hits_clone = Arc::clone(&public_hits); let public_execution_path = Arc::new(Mutex::new(None::)); let public_execution_path_clone = Arc::clone(&public_execution_path); let upstream = Router::new() .route( "/api/internal/gateway/resolve", any(|_request: Request| async move { Json(json!({ "action": "proxy_public", "route_class": "ai_public", "route_family": "openai", "route_kind": "chat", "auth_endpoint_signature": "openai:chat", "execution_runtime_candidate": true, "auth_context": { "user_id": "user-sync-123", "api_key_id": "key-sync-123", "access_allowed": true }, "public_path": "/v1/chat/completions" })) }), ) .route( "/api/internal/gateway/execute-sync", any(move |_request: Request| { let execute_hits_inner = Arc::clone(&execute_hits_clone); async move { *execute_hits_inner.lock().expect("mutex should lock") += 1; let mut response = Response::builder() .status(StatusCode::CREATED) .body(Body::from("{\"ok\":true}")) .expect("response should build"); response.headers_mut().insert( http::header::CONTENT_TYPE, HeaderValue::from_static("application/json"), ); response.headers_mut().insert( HeaderName::from_static(CONTROL_EXECUTED_HEADER), HeaderValue::from_static("true"), ); response } }), ) .route( "/v1/chat/completions", any(move |request: Request| { let public_hits_inner = Arc::clone(&public_hits_clone); let public_execution_path_inner = Arc::clone(&public_execution_path_clone); async move { *public_hits_inner.lock().expect("mutex should lock") += 1; *public_execution_path_inner .lock() .expect("mutex should lock") = Some( request .headers() .get(EXECUTION_PATH_HEADER) .and_then(|value| value.to_str().ok()) .unwrap_or_default() .to_string(), ); let mut response = Response::builder() .status(StatusCode::OK) .body(Body::from("{\"public\":true}")) .expect("response should build"); response.headers_mut().insert( http::header::CONTENT_TYPE, HeaderValue::from_static("application/json"), ); response } }), ); let (upstream_url, upstream_handle) = start_server(upstream).await; let gateway = build_router().expect("gateway should build"); let (gateway_url, gateway_handle) = start_server(gateway).await; let response = reqwest::Client::new() .post(format!("{gateway_url}/v1/chat/completions")) .header(http::header::CONTENT_TYPE, "application/json") .header(CONTROL_EXECUTE_FALLBACK_HEADER, "true") .header(TRACE_ID_HEADER, "trace-sync-123") .body("{\"model\":\"gpt-5\",\"messages\":[]}") .send() .await .expect("request should succeed"); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); let execution_path = response .headers() .get(EXECUTION_PATH_HEADER) .and_then(|value| value.to_str().ok()) .map(ToOwned::to_owned); let dependency_reason = response .headers() .get(DEPENDENCY_REASON_HEADER) .and_then(|value| value.to_str().ok()) .map(ToOwned::to_owned); assert_eq!(dependency_reason.as_deref(), None); let payload: serde_json::Value = response.json().await.expect("body should parse"); assert_eq!( execution_path.as_deref(), Some(EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS) ); assert_eq!( payload["error"]["message"], "请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商" ); assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0); assert_eq!(*public_hits.lock().expect("mutex should lock"), 0); assert_eq!( public_execution_path .lock() .expect("mutex should lock") .as_deref(), None ); gateway_handle.abort(); upstream_handle.abort(); } #[tokio::test] async fn gateway_locally_denies_stream_ai_control_execute_when_opted_in_and_execution_runtime_missing( ) { let execute_hits = Arc::new(Mutex::new(0usize)); let execute_hits_clone = Arc::clone(&execute_hits); let public_hits = Arc::new(Mutex::new(0usize)); let public_hits_clone = Arc::clone(&public_hits); let public_execution_path = Arc::new(Mutex::new(None::)); let public_execution_path_clone = Arc::clone(&public_execution_path); let upstream = Router::new() .route( "/api/internal/gateway/resolve", any(|_request: Request| async move { Json(json!({ "action": "proxy_public", "route_class": "ai_public", "route_family": "openai", "route_kind": "chat", "auth_endpoint_signature": "openai:chat", "execution_runtime_candidate": true, "auth_context": { "user_id": "user-stream-123", "api_key_id": "key-stream-123", "access_allowed": true }, "public_path": "/v1/chat/completions" })) }), ) .route( "/api/internal/gateway/execute-stream", any(move |_request: Request| { let execute_hits_inner = Arc::clone(&execute_hits_clone); async move { *execute_hits_inner.lock().expect("mutex should lock") += 1; let stream = futures_util::stream::iter([ Ok::<_, Infallible>(Bytes::from_static(b"data: one\n\n")), Ok::<_, Infallible>(Bytes::from_static(b"data: [DONE]\n\n")), ]); let mut response = Response::builder() .status(StatusCode::OK) .body(Body::from_stream(stream)) .expect("response should build"); response.headers_mut().insert( http::header::CONTENT_TYPE, HeaderValue::from_static("text/event-stream"), ); response.headers_mut().insert( HeaderName::from_static(CONTROL_EXECUTED_HEADER), HeaderValue::from_static("true"), ); response } }), ) .route( "/v1/chat/completions", any(move |request: Request| { let public_hits_inner = Arc::clone(&public_hits_clone); let public_execution_path_inner = Arc::clone(&public_execution_path_clone); async move { *public_hits_inner.lock().expect("mutex should lock") += 1; *public_execution_path_inner .lock() .expect("mutex should lock") = Some( request .headers() .get(EXECUTION_PATH_HEADER) .and_then(|value| value.to_str().ok()) .unwrap_or_default() .to_string(), ); let mut response = Response::builder() .status(StatusCode::OK) .body(Body::from("data: {\"public\":true}\n\ndata: [DONE]\n\n")) .expect("response should build"); response.headers_mut().insert( http::header::CONTENT_TYPE, HeaderValue::from_static("text/event-stream"), ); response } }), ); let (upstream_url, upstream_handle) = start_server(upstream).await; let gateway = build_router().expect("gateway should build"); let (gateway_url, gateway_handle) = start_server(gateway).await; let response = reqwest::Client::new() .post(format!("{gateway_url}/v1/chat/completions")) .header(http::header::CONTENT_TYPE, "application/json") .header(CONTROL_EXECUTE_FALLBACK_HEADER, "true") .header(TRACE_ID_HEADER, "trace-stream-123") .body("{\"model\":\"gpt-5\",\"messages\":[],\"stream\":true}") .send() .await .expect("request should succeed"); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); let execution_path = response .headers() .get(EXECUTION_PATH_HEADER) .and_then(|value| value.to_str().ok()) .map(ToOwned::to_owned); let dependency_reason = response .headers() .get(DEPENDENCY_REASON_HEADER) .and_then(|value| value.to_str().ok()) .map(ToOwned::to_owned); assert_eq!(dependency_reason.as_deref(), None); let payload: serde_json::Value = response.json().await.expect("body should parse"); assert_eq!( execution_path.as_deref(), Some(EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS) ); assert_eq!( payload["error"]["message"], "请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商" ); assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0); assert_eq!(*public_hits.lock().expect("mutex should lock"), 0); assert_eq!( public_execution_path .lock() .expect("mutex should lock") .as_deref(), None ); gateway_handle.abort(); upstream_handle.abort(); } #[tokio::test] async fn gateway_does_not_proxy_control_execute_over_http_when_opted_in_and_execution_runtime_misses_sync_ai_routes( ) { let plan_hits = Arc::new(Mutex::new(0usize)); let plan_hits_clone = Arc::clone(&plan_hits); let execute_hits = Arc::new(Mutex::new(0usize)); let execute_hits_clone = Arc::clone(&execute_hits); let public_hits = Arc::new(Mutex::new(0usize)); let public_hits_clone = Arc::clone(&public_hits); let upstream = Router::new() .route( "/api/internal/gateway/resolve", any(|_request: Request| async move { Json(json!({ "action": "proxy_public", "route_class": "ai_public", "route_family": "openai", "route_kind": "chat", "auth_endpoint_signature": "openai:chat", "execution_runtime_candidate": true, "public_path": "/v1/chat/completions" })) }), ) .route( "/api/internal/gateway/plan-sync", any(move |_request: Request| { let plan_hits_inner = Arc::clone(&plan_hits_clone); async move { *plan_hits_inner.lock().expect("mutex should lock") += 1; let mut response = Response::builder() .status(StatusCode::ACCEPTED) .body(Body::from("{\"ok\":true,\"via\":\"plan-sync\"}")) .expect("response should build"); response.headers_mut().insert( http::header::CONTENT_TYPE, HeaderValue::from_static("application/json"), ); response.headers_mut().insert( HeaderName::from_static(CONTROL_EXECUTED_HEADER), HeaderValue::from_static("true"), ); response } }), ) .route( "/api/internal/gateway/execute-sync", any(move |_request: Request| { let execute_hits_inner = Arc::clone(&execute_hits_clone); async move { *execute_hits_inner.lock().expect("mutex should lock") += 1; let mut response = Response::builder() .status(StatusCode::OK) .body(Body::from("{\"ok\":false}")) .expect("response should build"); response.headers_mut().insert( http::header::CONTENT_TYPE, HeaderValue::from_static("application/json"), ); response.headers_mut().insert( HeaderName::from_static(CONTROL_EXECUTED_HEADER), HeaderValue::from_static("true"), ); response } }), ) .route( "/v1/chat/completions", any(move |_request: Request| { let public_hits_inner = Arc::clone(&public_hits_clone); async move { *public_hits_inner.lock().expect("mutex should lock") += 1; let mut response = Response::builder() .status(StatusCode::OK) .body(Body::from("{\"public\":true}")) .expect("response should build"); response.headers_mut().insert( http::header::CONTENT_TYPE, HeaderValue::from_static("application/json"), ); response } }), ); let execution_runtime = Router::new(); let (upstream_url, upstream_handle) = start_server(upstream).await; let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await; let gateway = build_router_with_execution_runtime_override(execution_runtime_url); let (gateway_url, gateway_handle) = start_server(gateway).await; let response = reqwest::Client::new() .post(format!("{gateway_url}/v1/chat/completions")) .header(http::header::CONTENT_TYPE, "application/json") .header(CONTROL_EXECUTE_FALLBACK_HEADER, "true") .body("{\"model\":\"gpt-5\",\"messages\":[]}") .send() .await .expect("request should succeed"); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); assert_eq!( response .headers() .get(EXECUTION_PATH_HEADER) .and_then(|value| value.to_str().ok()), Some(EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS) ); assert_eq!( response .headers() .get(DEPENDENCY_REASON_HEADER) .and_then(|value| value.to_str().ok()), None ); let payload: serde_json::Value = response.json().await.expect("body should parse"); assert_eq!( payload["error"]["message"], "请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商" ); assert_eq!(*plan_hits.lock().expect("mutex should lock"), 0); assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0); assert_eq!(*public_hits.lock().expect("mutex should lock"), 0); gateway_handle.abort(); execution_runtime_handle.abort(); upstream_handle.abort(); } #[tokio::test] async fn gateway_does_not_proxy_control_execute_over_http_when_opted_in_and_execution_runtime_misses_stream_ai_routes( ) { let plan_hits = Arc::new(Mutex::new(0usize)); let plan_hits_clone = Arc::clone(&plan_hits); let execute_hits = Arc::new(Mutex::new(0usize)); let execute_hits_clone = Arc::clone(&execute_hits); let public_hits = Arc::new(Mutex::new(0usize)); let public_hits_clone = Arc::clone(&public_hits); let upstream = Router::new() .route( "/api/internal/gateway/resolve", any(|_request: Request| async move { Json(json!({ "action": "proxy_public", "route_class": "ai_public", "route_family": "openai", "route_kind": "chat", "auth_endpoint_signature": "openai:chat", "execution_runtime_candidate": true, "public_path": "/v1/chat/completions" })) }), ) .route( "/api/internal/gateway/plan-stream", any(move |_request: Request| { let plan_hits_inner = Arc::clone(&plan_hits_clone); async move { *plan_hits_inner.lock().expect("mutex should lock") += 1; let stream = futures_util::stream::iter([ Ok::<_, Infallible>(Bytes::from_static(b"data: one\n\n")), Ok::<_, Infallible>(Bytes::from_static(b"data: [DONE]\n\n")), ]); let mut response = Response::builder() .status(StatusCode::OK) .body(Body::from_stream(stream)) .expect("response should build"); response.headers_mut().insert( http::header::CONTENT_TYPE, HeaderValue::from_static("text/event-stream"), ); response.headers_mut().insert( HeaderName::from_static(CONTROL_EXECUTED_HEADER), HeaderValue::from_static("true"), ); response } }), ) .route( "/api/internal/gateway/execute-stream", any(move |_request: Request| { let execute_hits_inner = Arc::clone(&execute_hits_clone); async move { *execute_hits_inner.lock().expect("mutex should lock") += 1; let mut response = Response::builder() .status(StatusCode::OK) .body(Body::from("data: fallback\n\n")) .expect("response should build"); response.headers_mut().insert( HeaderName::from_static(CONTROL_EXECUTED_HEADER), HeaderValue::from_static("true"), ); response } }), ) .route( "/v1/chat/completions", any(move |_request: Request| { let public_hits_inner = Arc::clone(&public_hits_clone); async move { *public_hits_inner.lock().expect("mutex should lock") += 1; let mut response = Response::builder() .status(StatusCode::OK) .body(Body::from("data: {\"public\":true}\n\ndata: [DONE]\n\n")) .expect("response should build"); response.headers_mut().insert( http::header::CONTENT_TYPE, HeaderValue::from_static("text/event-stream"), ); response } }), ); let execution_runtime = Router::new(); let (upstream_url, upstream_handle) = start_server(upstream).await; let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await; let gateway = build_router_with_execution_runtime_override(execution_runtime_url); let (gateway_url, gateway_handle) = start_server(gateway).await; let response = reqwest::Client::new() .post(format!("{gateway_url}/v1/chat/completions")) .header(http::header::CONTENT_TYPE, "application/json") .header(CONTROL_EXECUTE_FALLBACK_HEADER, "true") .body("{\"model\":\"gpt-5\",\"messages\":[],\"stream\":true}") .send() .await .expect("request should succeed"); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); assert_eq!( response .headers() .get(EXECUTION_PATH_HEADER) .and_then(|value| value.to_str().ok()), Some(EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS) ); assert_eq!( response .headers() .get(DEPENDENCY_REASON_HEADER) .and_then(|value| value.to_str().ok()), None ); let payload: serde_json::Value = response.json().await.expect("body should parse"); assert_eq!( payload["error"]["message"], "请求缺少有效的用户或 API Key 认证上下文,无法选择上游提供商" ); assert_eq!(*plan_hits.lock().expect("mutex should lock"), 0); assert_eq!(*execute_hits.lock().expect("mutex should lock"), 0); assert_eq!(*public_hits.lock().expect("mutex should lock"), 0); gateway_handle.abort(); execution_runtime_handle.abort(); upstream_handle.abort(); }