diff --git a/backend/.sqlx/query-ffd6ac9bbe361cdb172198be10ecfbcc10970bba5d9f41fd5018c858fd784cb2.json b/backend/.sqlx/query-35c2c111465a13b4a9d45ef505512796b6e6970ffd35ee4095d9c5caf28502e4.json similarity index 51% rename from backend/.sqlx/query-ffd6ac9bbe361cdb172198be10ecfbcc10970bba5d9f41fd5018c858fd784cb2.json rename to backend/.sqlx/query-35c2c111465a13b4a9d45ef505512796b6e6970ffd35ee4095d9c5caf28502e4.json index d10e46b1..0d4ab12c 100644 --- a/backend/.sqlx/query-ffd6ac9bbe361cdb172198be10ecfbcc10970bba5d9f41fd5018c858fd784cb2.json +++ b/backend/.sqlx/query-35c2c111465a13b4a9d45ef505512796b6e6970ffd35ee4095d9c5caf28502e4.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT \n t.id as \"id!: Uuid\", \n t.project_id as \"project_id!: Uuid\", \n t.title, \n t.description, \n t.status as \"status!: TaskStatus\", \n t.created_at as \"created_at!: DateTime\", \n t.updated_at as \"updated_at!: DateTime\",\n CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as \"has_in_progress_attempt!\",\n CASE WHEN merged_attempts.task_id IS NOT NULL THEN true ELSE false END as \"has_merged_attempt!\"\n FROM tasks t\n LEFT JOIN (\n SELECT DISTINCT ta.task_id \n FROM task_attempts ta\n INNER JOIN (\n SELECT task_attempt_id, MAX(created_at) as latest_created_at\n FROM task_attempt_activities\n GROUP BY task_attempt_id\n ) latest_activity ON ta.id = latest_activity.task_attempt_id\n INNER JOIN task_attempt_activities taa ON ta.id = taa.task_attempt_id \n AND taa.created_at = latest_activity.latest_created_at\n WHERE taa.status IN ('setuprunning', 'executorrunning')\n ) in_progress_attempts ON t.id = in_progress_attempts.task_id\n LEFT JOIN (\n SELECT DISTINCT ta.task_id \n FROM task_attempts ta\n WHERE ta.merge_commit IS NOT NULL\n ) merged_attempts ON t.id = merged_attempts.task_id\n WHERE t.project_id = $1 \n ORDER BY t.created_at DESC", + "query": "SELECT \n t.id as \"id!: Uuid\", \n t.project_id as \"project_id!: Uuid\", \n t.title, \n t.description, \n t.status as \"status!: TaskStatus\", \n t.created_at as \"created_at!: DateTime\", \n t.updated_at as \"updated_at!: DateTime\",\n CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as \"has_in_progress_attempt!: i64\",\n CASE WHEN merged_attempts.task_id IS NOT NULL THEN true ELSE false END as \"has_merged_attempt!\"\n FROM tasks t\n LEFT JOIN (\n SELECT DISTINCT ta.task_id \n FROM task_attempts ta\n INNER JOIN (\n SELECT task_attempt_id, MAX(created_at) as latest_created_at\n FROM task_attempt_activities\n GROUP BY task_attempt_id\n ) latest_activity ON ta.id = latest_activity.task_attempt_id\n INNER JOIN task_attempt_activities taa ON ta.id = taa.task_attempt_id \n AND taa.created_at = latest_activity.latest_created_at\n WHERE taa.status IN ('setuprunning', 'executorrunning')\n ) in_progress_attempts ON t.id = in_progress_attempts.task_id\n LEFT JOIN (\n SELECT DISTINCT ta.task_id \n FROM task_attempts ta\n WHERE ta.merge_commit IS NOT NULL\n ) merged_attempts ON t.id = merged_attempts.task_id\n WHERE t.project_id = $1 \n ORDER BY t.created_at DESC", "describe": { "columns": [ { @@ -39,14 +39,14 @@ "type_info": "Text" }, { - "name": "has_in_progress_attempt!", + "name": "has_in_progress_attempt!: i64", "ordinal": 7, - "type_info": "Int" + "type_info": "Integer" }, { "name": "has_merged_attempt!", "ordinal": 8, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { @@ -64,5 +64,5 @@ false ] }, - "hash": "ffd6ac9bbe361cdb172198be10ecfbcc10970bba5d9f41fd5018c858fd784cb2" + "hash": "35c2c111465a13b4a9d45ef505512796b6e6970ffd35ee4095d9c5caf28502e4" } diff --git a/backend/.sqlx/query-b4efb71365b75edd3dfc843f774f7cdb06dc756328ed5929d6b13ef3a956be0e.json b/backend/.sqlx/query-b4efb71365b75edd3dfc843f774f7cdb06dc756328ed5929d6b13ef3a956be0e.json deleted file mode 100644 index 59b826b4..00000000 --- a/backend/.sqlx/query-b4efb71365b75edd3dfc843f774f7cdb06dc756328ed5929d6b13ef3a956be0e.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT COUNT(*) as count FROM projects WHERE id = $1", - "describe": { - "columns": [ - { - "name": "count", - "ordinal": 0, - "type_info": "Int" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "b4efb71365b75edd3dfc843f774f7cdb06dc756328ed5929d6b13ef3a956be0e" -} diff --git a/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json b/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json new file mode 100644 index 00000000..8d640cca --- /dev/null +++ b/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) as \"count!: i64\"\n FROM projects\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "name": "count!: i64", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2" +} diff --git a/backend/Cargo.toml b/backend/Cargo.toml index ac1714f0..f597a6c0 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -18,7 +18,7 @@ serde_json = { workspace = true } anyhow = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] } +sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] } chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1.0", features = ["v4", "serde"] } bcrypt = "0.15" diff --git a/backend/src/execution_monitor.rs b/backend/src/execution_monitor.rs index 814297ab..4aa10a37 100644 --- a/backend/src/execution_monitor.rs +++ b/backend/src/execution_monitor.rs @@ -12,9 +12,17 @@ use crate::models::{ task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity}, }; +// #[derive(Debug)] +// pub enum ExecutionType { +// SetupScript, +// CodingAgent, +// DevServer, +// } + #[derive(Debug)] pub struct RunningExecution { pub task_attempt_id: Uuid, + // pub execution_type: ExecutionType, pub child: tokio::process::Child, pub started_at: DateTime, } diff --git a/backend/src/models/project.rs b/backend/src/models/project.rs index a116037b..b3173ee0 100644 --- a/backend/src/models/project.rs +++ b/backend/src/models/project.rs @@ -143,9 +143,17 @@ impl Project { } pub async fn exists(pool: &SqlitePool, id: Uuid) -> Result { - let result = sqlx::query!("SELECT COUNT(*) as count FROM projects WHERE id = $1", id) - .fetch_one(pool) - .await?; + let result = sqlx::query!( + r#" + SELECT COUNT(*) as "count!: i64" + FROM projects + WHERE id = $1 + "#, + id + ) + .fetch_one(pool) + .await?; + Ok(result.count > 0) } } diff --git a/backend/src/models/task.rs b/backend/src/models/task.rs index 5b26f557..b107e788 100644 --- a/backend/src/models/task.rs +++ b/backend/src/models/task.rs @@ -88,7 +88,7 @@ impl Task { t.status as "status!: TaskStatus", t.created_at as "created_at!: DateTime", t.updated_at as "updated_at!: DateTime", - CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as "has_in_progress_attempt!", + CASE WHEN in_progress_attempts.task_id IS NOT NULL THEN true ELSE false END as "has_in_progress_attempt!: i64", CASE WHEN merged_attempts.task_id IS NOT NULL THEN true ELSE false END as "has_merged_attempt!" FROM tasks t LEFT JOIN ( diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 2a4fe087..886e31e7 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -135,8 +135,15 @@ pub async fn create_task_and_start( // Create task attempt let attempt_id = Uuid::new_v4(); - let worktree_path = format!("/tmp/task-{}-attempt-{}", task_id, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis()); - + let worktree_path = format!( + "/tmp/task-{}-attempt-{}", + task_id, + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() + ); + let attempt_payload = CreateTaskAttempt { task_id, worktree_path, @@ -756,399 +763,3 @@ pub fn tasks_router() -> Router { post(delete_task_attempt_file), ) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::auth::hash_password; - use crate::models::{ - project::Project, - task::{CreateTask, TaskStatus, UpdateTask}, - user::User, - }; - use axum::extract::Extension; - use chrono::Utc; - use sqlx::SqlitePool; - use uuid::Uuid; - - async fn create_test_user( - pool: &SqlitePool, - email: &str, - password: &str, - is_admin: bool, - ) -> User { - let id = Uuid::new_v4(); - let now = Utc::now(); - let password_hash = hash_password(password).unwrap(); - - sqlx::query_as!( - User, - "INSERT INTO users (id, email, password_hash, is_admin, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, email, password_hash, is_admin, created_at, updated_at", - id, - email, - password_hash, - is_admin, - now, - now - ) - .fetch_one(pool) - .await - .unwrap() - } - - async fn create_test_project(pool: &SqlitePool, name: &str, owner_id: Uuid) -> Project { - let id = Uuid::new_v4(); - let now = Utc::now(); - let git_repo_path = format!("/tmp/test-repo-{}", id); - - sqlx::query_as!( - Project, - "INSERT INTO projects (id, name, git_repo_path, owner_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, git_repo_path, owner_id, created_at, updated_at", - id, - name, - git_repo_path, - owner_id, - now, - now - ) - .fetch_one(pool) - .await - .unwrap() - } - - async fn create_test_task( - pool: &SqlitePool, - project_id: Uuid, - title: &str, - description: Option, - status: TaskStatus, - ) -> Task { - let id = Uuid::new_v4(); - let now = Utc::now(); - - sqlx::query_as!( - Task, - r#"INSERT INTO tasks (id, project_id, title, description, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at"#, - id, - project_id, - title, - description, - status as TaskStatus, - now, - now - ) - .fetch_one(pool) - .await - .unwrap() - } - - #[sqlx::test] - async fn test_get_project_tasks_success(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Test Project", user.id).await; - - // Create multiple tasks - create_test_task( - &pool, - project.id, - "Task 1", - Some("Description 1".to_string()), - TaskStatus::Todo, - ) - .await; - create_test_task(&pool, project.id, "Task 2", None, TaskStatus::InProgress).await; - create_test_task( - &pool, - project.id, - "Task 3", - Some("Description 3".to_string()), - TaskStatus::Done, - ) - .await; - - let result = get_project_tasks(Path(project.id), Extension(pool)).await; - assert!(result.is_ok()); - - let response = result.unwrap().0; - assert!(response.success); - assert!(response.data.is_some()); - assert_eq!(response.data.unwrap().len(), 3); - } - - #[sqlx::test] - async fn test_get_project_tasks_empty_project(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Empty Project", user.id).await; - - let result = get_project_tasks(Path(project.id), Extension(pool)).await; - assert!(result.is_ok()); - - let response = result.unwrap().0; - assert!(response.success); - assert!(response.data.is_some()); - assert_eq!(response.data.unwrap().len(), 0); - } - - #[sqlx::test] - async fn test_get_task_success(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Test Project", user.id).await; - let task = create_test_task( - &pool, - project.id, - "Test Task", - Some("Test Description".to_string()), - TaskStatus::Todo, - ) - .await; - - let result = get_task(Path((project.id, task.id)), Extension(pool)).await; - assert!(result.is_ok()); - - let response = result.unwrap().0; - assert!(response.success); - assert!(response.data.is_some()); - let returned_task = response.data.unwrap(); - assert_eq!(returned_task.id, task.id); - assert_eq!(returned_task.title, task.title); - assert_eq!(returned_task.description, task.description); - assert_eq!(returned_task.status, task.status); - } - - #[sqlx::test] - async fn test_get_task_not_found(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Test Project", user.id).await; - let nonexistent_task_id = Uuid::new_v4(); - - let result = get_task(Path((project.id, nonexistent_task_id)), Extension(pool)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); - } - - #[sqlx::test] - async fn test_get_task_wrong_project(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project1 = create_test_project(&pool, "Project 1", user.id).await; - let project2 = create_test_project(&pool, "Project 2", user.id).await; - let task = create_test_task(&pool, project1.id, "Test Task", None, TaskStatus::Todo).await; - - // Try to get task from wrong project - let result = get_task(Path((project2.id, task.id)), Extension(pool)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); - } - - #[sqlx::test] - async fn test_create_task_success(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Test Project", user.id).await; - - let create_request = CreateTask { - project_id: project.id, // This will be overridden by the path parameter - title: "New Task".to_string(), - description: Some("Task description".to_string()), - }; - - let result = create_task(Path(project.id), Extension(pool), Json(create_request)).await; - assert!(result.is_ok()); - - let response = result.unwrap().0; - assert!(response.success); - assert!(response.data.is_some()); - let created_task = response.data.unwrap(); - assert_eq!(created_task.title, "New Task"); - assert_eq!( - created_task.description, - Some("Task description".to_string()) - ); - assert_eq!(created_task.status, TaskStatus::Todo); - assert_eq!(created_task.project_id, project.id); - } - - #[sqlx::test] - async fn test_create_task_project_not_found(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let nonexistent_project_id = Uuid::new_v4(); - - let create_request = CreateTask { - project_id: nonexistent_project_id, - title: "New Task".to_string(), - description: None, - }; - - let result = create_task( - Path(nonexistent_project_id), - Extension(pool), - Json(create_request), - ) - .await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); - } - - #[sqlx::test] - async fn test_update_task_success(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Test Project", user.id).await; - let task = create_test_task( - &pool, - project.id, - "Original Title", - Some("Original Description".to_string()), - TaskStatus::Todo, - ) - .await; - - let update_request = UpdateTask { - title: Some("Updated Title".to_string()), - description: Some("Updated Description".to_string()), - status: Some(TaskStatus::InProgress), - }; - - let result = update_task( - Path((project.id, task.id)), - Extension(pool), - Json(update_request), - ) - .await; - assert!(result.is_ok()); - - let response = result.unwrap().0; - assert!(response.success); - assert!(response.data.is_some()); - let updated_task = response.data.unwrap(); - assert_eq!(updated_task.title, "Updated Title"); - assert_eq!( - updated_task.description, - Some("Updated Description".to_string()) - ); - assert_eq!(updated_task.status, TaskStatus::InProgress); - } - - #[sqlx::test] - async fn test_update_task_partial(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Test Project", user.id).await; - let task = create_test_task( - &pool, - project.id, - "Original Title", - Some("Original Description".to_string()), - TaskStatus::Todo, - ) - .await; - - // Only update status - let update_request = UpdateTask { - title: None, - description: None, - status: Some(TaskStatus::Done), - }; - - let result = update_task( - Path((project.id, task.id)), - Extension(pool), - Json(update_request), - ) - .await; - assert!(result.is_ok()); - - let response = result.unwrap().0; - assert!(response.success); - assert!(response.data.is_some()); - let updated_task = response.data.unwrap(); - assert_eq!(updated_task.title, "Original Title"); // Should remain unchanged - assert_eq!( - updated_task.description, - Some("Original Description".to_string()) - ); // Should remain unchanged - assert_eq!(updated_task.status, TaskStatus::Done); // Should be updated - } - - #[sqlx::test] - async fn test_update_task_not_found(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Test Project", user.id).await; - let nonexistent_task_id = Uuid::new_v4(); - - let update_request = UpdateTask { - title: Some("Updated Title".to_string()), - description: None, - status: None, - }; - - let result = update_task( - Path((project.id, nonexistent_task_id)), - Extension(pool), - Json(update_request), - ) - .await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); - } - - #[sqlx::test] - async fn test_update_task_wrong_project(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project1 = create_test_project(&pool, "Project 1", user.id).await; - let project2 = create_test_project(&pool, "Project 2", user.id).await; - let task = create_test_task(&pool, project1.id, "Test Task", None, TaskStatus::Todo).await; - - let update_request = UpdateTask { - title: Some("Updated Title".to_string()), - description: None, - status: None, - }; - - // Try to update task in wrong project - let result = update_task( - Path((project2.id, task.id)), - Extension(pool), - Json(update_request), - ) - .await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); - } - - #[sqlx::test] - async fn test_delete_task_success(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Test Project", user.id).await; - let task = - create_test_task(&pool, project.id, "Task to Delete", None, TaskStatus::Todo).await; - - let result = delete_task(Path((project.id, task.id)), Extension(pool)).await; - assert!(result.is_ok()); - - let response = result.unwrap().0; - assert!(response.success); - assert_eq!(response.message.unwrap(), "Task deleted successfully"); - } - - #[sqlx::test] - async fn test_delete_task_not_found(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Test Project", user.id).await; - let nonexistent_task_id = Uuid::new_v4(); - - let result = delete_task(Path((project.id, nonexistent_task_id)), Extension(pool)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); - } - - #[sqlx::test] - async fn test_delete_task_wrong_project(pool: SqlitePool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project1 = create_test_project(&pool, "Project 1", user.id).await; - let project2 = create_test_project(&pool, "Project 2", user.id).await; - let task = - create_test_task(&pool, project1.id, "Task to Delete", None, TaskStatus::Todo).await; - - // Try to delete task from wrong project - let result = delete_task(Path((project2.id, task.id)), Extension(pool)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); - } -}