use std::{fs, path::Path}; use axum::{ Router, body::Body, http::{Request, StatusCode, header}, }; use http_body_util::BodyExt; use serde_json::{Value, json}; use tempfile::TempDir; use tower::ServiceExt; async fn test_app() -> (Router, TempDir) { let temp_dir = TempDir::new().expect("temporary directory should be created"); let pool = task_pilot_backend::database::connect("sqlite::memory:") .await .expect("test database should initialize"); let app = task_pilot_backend::app(pool, temp_dir.path().join("logs")) .await .expect("test app should start"); (app, temp_dir) } async fn fetch_token(app: &Router) -> String { let response = app .clone() .oneshot( Request::builder() .uri("/api/v1/session") .body(Body::empty()) .expect("request should build"), ) .await .expect("router should respond"); let body = response .into_body() .collect() .await .expect("session body should be readable") .to_bytes(); let json: Value = serde_json::from_slice(&body).expect("session should be JSON"); json["token"] .as_str() .expect("session token should be a string") .to_owned() } async fn post_probe(app: &Router, token: Option<&str>, payload: Value) -> (StatusCode, Value) { post_json(app, "/api/v1/python/venv", token, payload).await } async fn post_create(app: &Router, token: Option<&str>, payload: Value) -> (StatusCode, Value) { post_json(app, "/api/v1/python/venv/create", token, payload).await } async fn post_json( app: &Router, uri: &str, token: Option<&str>, payload: Value, ) -> (StatusCode, Value) { let mut builder = Request::builder() .method("POST") .uri(uri) .header(header::CONTENT_TYPE, "application/json"); if let Some(token) = token { builder = builder.header("x-taskpilot-token", token); } let response = app .clone() .oneshot( builder .body(Body::from(payload.to_string())) .expect("request should build"), ) .await .expect("router should respond"); let status = response.status(); let body = response .into_body() .collect() .await .expect("response body should be readable") .to_bytes(); (status, serde_json::from_slice(&body).unwrap_or(Value::Null)) } fn create_fake_venv(project: &Path) { let venv = project.join(".venv"); fs::create_dir_all(venv.join("Scripts")).expect("venv directory should be created"); fs::write(venv.join("pyvenv.cfg"), "home = C:\\Python\n") .expect("pyvenv.cfg should be written"); fs::write(venv.join("Scripts").join("python.exe"), "") .expect("python.exe placeholder should be written"); } #[tokio::test] async fn venv_probe_requires_management_token() { let (app, _temp_dir) = test_app().await; let (status, body) = post_probe( &app, None, json!({ "working_dir": ".", "interpreter": "python.exe" }), ) .await; assert_eq!(status, StatusCode::FORBIDDEN); assert_eq!(body["code"], "FORBIDDEN"); } #[tokio::test] async fn venv_probe_reports_linked_environment() { let (app, temp_dir) = test_app().await; let project = temp_dir.path().join("project"); fs::create_dir_all(&project).expect("project directory should be created"); create_fake_venv(&project); let token = fetch_token(&app).await; let (status, body) = post_probe( &app, Some(&token), json!({ "working_dir": project.to_str().expect("project path should be UTF-8"), "interpreter": "python.exe", "venv": null }), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(body["status"], "linked"); assert_eq!(body["interpreter_kind"], "python"); assert_eq!(body["source"], "working_directory"); let expected_python = Path::new(".venv") .join("Scripts") .join("python.exe") .to_string_lossy() .into_owned(); assert_eq!(body["display_path"], expected_python); assert_eq!( body["venv_path"], project.join(".venv").to_string_lossy().into_owned() ); } #[tokio::test] async fn venv_probe_skips_non_python_interpreters() { let (app, temp_dir) = test_app().await; let token = fetch_token(&app).await; let (status, body) = post_probe( &app, Some(&token), json!({ "working_dir": temp_dir.path().to_str().expect("temp path should be UTF-8"), "interpreter": "node.exe" }), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(body["status"], "not_applicable"); assert_eq!(body["interpreter_kind"], "node"); assert_eq!(body["display_path"], "node.exe"); } #[tokio::test] async fn venv_probe_reports_missing_working_directory() { let (app, temp_dir) = test_app().await; let token = fetch_token(&app).await; let absent = temp_dir.path().join("absent"); let (status, body) = post_probe( &app, Some(&token), json!({ "working_dir": absent.to_str().expect("absent path should be UTF-8"), "interpreter": "python.exe" }), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(body["status"], "incomplete"); assert!( body["message"] .as_str() .expect("message should be a string") .contains("工作目录") ); } #[tokio::test] async fn venv_probe_reports_project_dependency_files() { let (app, temp_dir) = test_app().await; let token = fetch_token(&app).await; let project = temp_dir.path().join("deps-project"); fs::create_dir_all(&project).expect("project directory should be created"); fs::write( project.join("pyproject.toml"), "[project]\nname = \"demo\"\n", ) .expect("pyproject.toml should be written"); fs::write(project.join("requirements.txt"), "").expect("requirements.txt should be written"); let (status, body) = post_probe( &app, Some(&token), json!({ "working_dir": project.to_str().expect("project path should be UTF-8"), "interpreter": "python.exe" }), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(body["status"], "missing"); assert_eq!(body["has_pyproject"], true); assert_eq!(body["has_requirements"], true); assert_eq!(body["requirements_files"], json!(["requirements.txt"])); assert_eq!(body["can_create"], body["uv_available"]); } #[tokio::test] async fn venv_create_rejects_missing_working_directory() { let (app, temp_dir) = test_app().await; let token = fetch_token(&app).await; let absent = temp_dir.path().join("absent-project"); let (status, body) = post_create( &app, Some(&token), json!({ "working_dir": absent.to_str().expect("absent path should be UTF-8") }), ) .await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!(body["code"], "VENV_CREATE_FAILED"); assert!( body["message"] .as_str() .expect("message should be a string") .contains("工作目录") ); } #[tokio::test] async fn venv_create_requires_management_token() { let (app, temp_dir) = test_app().await; let (status, body) = post_create( &app, None, json!({ "working_dir": temp_dir.path().to_str().expect("temp path should be UTF-8") }), ) .await; assert_eq!(status, StatusCode::FORBIDDEN); assert_eq!(body["code"], "FORBIDDEN"); }