//! Integration tests for Execution repository //! //! These tests verify CRUD operations, queries, and constraints //! for the Execution repository. mod helpers; use attune_common::{ models::enums::ExecutionStatus, repositories::{ execution::{CreateExecutionInput, ExecutionRepository, UpdateExecutionInput}, Create, Delete, FindById, List, Update, }, }; use helpers::*; use serde_json::json; // ============================================================================ // CREATE Tests // ============================================================================ #[tokio::test] async fn test_create_execution_basic() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("exec_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "test_action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: Some(json!({"param1": "value1"})), parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let execution = ExecutionRepository::create(&pool, input).await.unwrap(); assert_eq!(execution.action, Some(action.id)); assert_eq!(execution.action_ref, action.r#ref); assert_eq!(execution.config, Some(json!({"param1": "value1"}))); assert_eq!(execution.parent, None); assert_eq!(execution.enforcement, None); assert_eq!(execution.executor, None); assert_eq!(execution.status, ExecutionStatus::Requested); assert_eq!(execution.result, None); assert!(execution.created.timestamp() > 0); assert!(execution.updated.timestamp() > 0); } #[tokio::test] async fn test_create_execution_without_action() { let pool = create_test_pool().await.unwrap(); let action_ref = format!("core.{}", unique_execution_ref("deleted_action")); let input = CreateExecutionInput { action: None, action_ref: action_ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let execution = ExecutionRepository::create(&pool, input).await.unwrap(); assert_eq!(execution.action, None); assert_eq!(execution.action_ref, action_ref); } #[tokio::test] async fn test_create_execution_with_all_fields() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("full_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: Some(json!({"timeout": 300, "retry": true})), parent: None, enforcement: None, executor: None, // Don't reference non-existent identity status: ExecutionStatus::Scheduled, result: Some(json!({"status": "ok"})), workflow_task: None, }; let execution = ExecutionRepository::create(&pool, input).await.unwrap(); assert_eq!(execution.executor, None); assert_eq!(execution.status, ExecutionStatus::Scheduled); assert_eq!(execution.result, Some(json!({"status": "ok"}))); } #[tokio::test] async fn test_create_execution_with_parent() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("parent_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); // Create parent execution let parent_input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Running, result: None, workflow_task: None, }; let parent = ExecutionRepository::create(&pool, parent_input) .await .unwrap(); // Create child execution let child_input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: Some(parent.id), enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let child = ExecutionRepository::create(&pool, child_input) .await .unwrap(); assert_eq!(child.parent, Some(parent.id)); } // ============================================================================ // READ Tests // ============================================================================ #[tokio::test] async fn test_find_execution_by_id() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("find_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let created = ExecutionRepository::create(&pool, input).await.unwrap(); let found = ExecutionRepository::find_by_id(&pool, created.id) .await .unwrap() .expect("Execution should exist"); assert_eq!(found.id, created.id); assert_eq!(found.action_ref, created.action_ref); assert_eq!(found.status, created.status); } #[tokio::test] async fn test_find_execution_by_id_not_found() { let pool = create_test_pool().await.unwrap(); let result = ExecutionRepository::find_by_id(&pool, 999999) .await .unwrap(); assert!(result.is_none()); } #[tokio::test] async fn test_list_executions() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("list_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); // Create multiple executions for i in 1..=3 { let input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}_{}", action.r#ref, i), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; ExecutionRepository::create(&pool, input).await.unwrap(); } let executions = ExecutionRepository::list(&pool).await.unwrap(); // Should have at least our 3 executions (may have more from parallel tests) let our_executions: Vec<_> = executions .iter() .filter(|e| e.action_ref.starts_with(&action.r#ref)) .collect(); assert_eq!(our_executions.len(), 3); } #[tokio::test] async fn test_list_executions_ordered_by_created_desc() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("order_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let mut created_ids = vec![]; // Create executions in sequence for i in 1..=3 { let input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}_{}", action.r#ref, i), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let exec = ExecutionRepository::create(&pool, input).await.unwrap(); created_ids.push(exec.id); // Small delay to ensure different timestamps tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; } let executions = ExecutionRepository::list(&pool).await.unwrap(); let our_executions: Vec<_> = executions .iter() .filter(|e| e.action_ref.starts_with(&action.r#ref)) .collect(); // Should be in reverse order (newest first) assert_eq!(our_executions[0].id, created_ids[2]); assert_eq!(our_executions[1].id, created_ids[1]); assert_eq!(our_executions[2].id, created_ids[0]); } // ============================================================================ // UPDATE Tests // ============================================================================ #[tokio::test] async fn test_update_execution_status() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("update_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let created = ExecutionRepository::create(&pool, input).await.unwrap(); let update = UpdateExecutionInput { status: Some(ExecutionStatus::Running), result: None, executor: None, workflow_task: None, }; let updated = ExecutionRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.status, ExecutionStatus::Running); assert!(updated.updated > created.updated); } #[tokio::test] async fn test_update_execution_result() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("result_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Running, result: None, workflow_task: None, }; let created = ExecutionRepository::create(&pool, input).await.unwrap(); let result_data = json!({"output": "success", "data": {"count": 42}}); let update = UpdateExecutionInput { status: Some(ExecutionStatus::Completed), result: Some(result_data.clone()), executor: None, workflow_task: None, }; let updated = ExecutionRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.status, ExecutionStatus::Completed); assert_eq!(updated.result, Some(result_data)); } #[tokio::test] async fn test_update_execution_executor() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("executor_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let created = ExecutionRepository::create(&pool, input).await.unwrap(); let update = UpdateExecutionInput { status: Some(ExecutionStatus::Scheduled), result: None, executor: None, workflow_task: None, }; let updated = ExecutionRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.status, ExecutionStatus::Scheduled); } #[tokio::test] async fn test_update_execution_status_transitions() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("status_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let exec = ExecutionRepository::create(&pool, input).await.unwrap(); // Transition: Requested -> Scheduling let exec = ExecutionRepository::update( &pool, exec.id, UpdateExecutionInput { status: Some(ExecutionStatus::Scheduling), result: None, executor: None, workflow_task: None, }, ) .await .unwrap(); assert_eq!(exec.status, ExecutionStatus::Scheduling); // Transition: Scheduling -> Scheduled let exec = ExecutionRepository::update( &pool, exec.id, UpdateExecutionInput { status: Some(ExecutionStatus::Scheduled), result: None, executor: None, workflow_task: None, }, ) .await .unwrap(); assert_eq!(exec.status, ExecutionStatus::Scheduled); // Transition: Scheduled -> Running let exec = ExecutionRepository::update( &pool, exec.id, UpdateExecutionInput { status: Some(ExecutionStatus::Running), result: None, executor: None, workflow_task: None, }, ) .await .unwrap(); assert_eq!(exec.status, ExecutionStatus::Running); // Transition: Running -> Completed let exec = ExecutionRepository::update( &pool, exec.id, UpdateExecutionInput { status: Some(ExecutionStatus::Completed), result: Some(json!({"success": true})), executor: None, workflow_task: None, }, ) .await .unwrap(); assert_eq!(exec.status, ExecutionStatus::Completed); } #[tokio::test] async fn test_update_execution_failed_status() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("failed_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Running, result: None, workflow_task: None, }; let created = ExecutionRepository::create(&pool, input).await.unwrap(); let update = UpdateExecutionInput { status: Some(ExecutionStatus::Failed), result: Some(json!({"error": "Connection timeout"})), executor: None, workflow_task: None, }; let updated = ExecutionRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.status, ExecutionStatus::Failed); assert!(updated.result.is_some()); } #[tokio::test] async fn test_update_execution_no_changes() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("nochange_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let created = ExecutionRepository::create(&pool, input).await.unwrap(); let update = UpdateExecutionInput::default(); let updated = ExecutionRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.status, created.status); assert_eq!(updated.result, created.result); } // ============================================================================ // DELETE Tests // ============================================================================ #[tokio::test] async fn test_delete_execution() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("delete_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Completed, result: None, workflow_task: None, }; let created = ExecutionRepository::create(&pool, input).await.unwrap(); let deleted = ExecutionRepository::delete(&pool, created.id) .await .unwrap(); assert!(deleted); let found = ExecutionRepository::find_by_id(&pool, created.id) .await .unwrap(); assert!(found.is_none()); } #[tokio::test] async fn test_delete_execution_not_found() { let pool = create_test_pool().await.unwrap(); let deleted = ExecutionRepository::delete(&pool, 999999).await.unwrap(); assert!(!deleted); } // ============================================================================ // SPECIALIZED QUERY Tests // ============================================================================ #[tokio::test] async fn test_find_executions_by_status() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("status_filter_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); // Create executions with different statuses for (i, status) in [ ExecutionStatus::Requested, ExecutionStatus::Running, ExecutionStatus::Running, ExecutionStatus::Completed, ] .iter() .enumerate() { let input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}_{}", action.r#ref, i), config: None, parent: None, enforcement: None, executor: None, status: *status, result: None, workflow_task: None, }; ExecutionRepository::create(&pool, input).await.unwrap(); } let running = ExecutionRepository::find_by_status(&pool, ExecutionStatus::Running) .await .unwrap(); let our_running: Vec<_> = running .iter() .filter(|e| e.action_ref.starts_with(&action.r#ref)) .collect(); assert_eq!(our_running.len(), 2); assert!(our_running .iter() .all(|e| e.status == ExecutionStatus::Running)); } #[tokio::test] async fn test_find_executions_by_enforcement() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("enforcement_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); // Create first execution with enforcement placeholder let exec1_input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}_1", action.r#ref), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let _exec1 = ExecutionRepository::create(&pool, exec1_input) .await .unwrap(); // Create executions with enforcement reference for i in 2..=3 { let input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}_{}", action.r#ref, i), config: None, parent: None, enforcement: if i == 2 { None } else { None }, // Can't reference non-existent enforcement executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; ExecutionRepository::create(&pool, input).await.unwrap(); } // Test find_by_enforcement with non-existent ID returns empty let by_enforcement = ExecutionRepository::find_by_enforcement(&pool, 999999) .await .unwrap(); assert_eq!(by_enforcement.len(), 0); } // ============================================================================ // PARENT-CHILD RELATIONSHIP Tests // ============================================================================ #[tokio::test] async fn test_parent_child_execution_hierarchy() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("hierarchy_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); // Create parent let parent_input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}.parent", action.r#ref), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Running, result: None, workflow_task: None, }; let parent = ExecutionRepository::create(&pool, parent_input) .await .unwrap(); // Create children let mut children = vec![]; for i in 1..=3 { let child_input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}.child_{}", action.r#ref, i), config: None, parent: Some(parent.id), enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let child = ExecutionRepository::create(&pool, child_input) .await .unwrap(); children.push(child); } // Verify all children have correct parent for child in children { assert_eq!(child.parent, Some(parent.id)); } // Verify parent has no parent assert_eq!(parent.parent, None); } #[tokio::test] async fn test_nested_execution_hierarchy() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("nested_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); // Create grandparent let grandparent_input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}.grandparent", action.r#ref), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Running, result: None, workflow_task: None, }; let grandparent = ExecutionRepository::create(&pool, grandparent_input) .await .unwrap(); // Create parent let parent_input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}.parent", action.r#ref), config: None, parent: Some(grandparent.id), enforcement: None, executor: None, status: ExecutionStatus::Running, result: None, workflow_task: None, }; let parent = ExecutionRepository::create(&pool, parent_input) .await .unwrap(); // Create child let child_input = CreateExecutionInput { action: Some(action.id), action_ref: format!("{}.child", action.r#ref), config: None, parent: Some(parent.id), enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let child = ExecutionRepository::create(&pool, child_input) .await .unwrap(); // Verify hierarchy assert_eq!(grandparent.parent, None); assert_eq!(parent.parent, Some(grandparent.id)); assert_eq!(child.parent, Some(parent.id)); } // ============================================================================ // TIMESTAMP Tests // ============================================================================ #[tokio::test] async fn test_execution_timestamps() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("timestamp_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let created = ExecutionRepository::create(&pool, input).await.unwrap(); assert!(created.created.timestamp() > 0); assert!(created.updated.timestamp() > 0); assert_eq!(created.created, created.updated); // Sleep briefly to ensure timestamp difference tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let update = UpdateExecutionInput { status: Some(ExecutionStatus::Running), result: None, executor: None, workflow_task: None, }; let updated = ExecutionRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.created, created.created); // created unchanged assert!(updated.updated > created.updated); // updated changed } // ============================================================================ // JSON FIELD Tests // ============================================================================ #[tokio::test] async fn test_execution_config_json() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("config_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let complex_config = json!({ "parameters": { "timeout": 300, "retry_count": 3, "retry_delay": 1000 }, "environment": { "NODE_ENV": "production" }, "metadata": { "triggered_by": "webhook", "source": "github" } }); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: Some(complex_config.clone()), parent: None, enforcement: None, executor: None, status: ExecutionStatus::Requested, result: None, workflow_task: None, }; let execution = ExecutionRepository::create(&pool, input).await.unwrap(); assert_eq!(execution.config, Some(complex_config)); } #[tokio::test] async fn test_execution_result_json() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("result_json_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); let input = CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, parent: None, enforcement: None, executor: None, status: ExecutionStatus::Running, result: None, workflow_task: None, }; let created = ExecutionRepository::create(&pool, input).await.unwrap(); let complex_result = json!({ "output": { "stdout": "Process completed successfully", "stderr": "" }, "metrics": { "duration_ms": 1234, "memory_mb": 128, "cpu_percent": 45.2 }, "artifacts": [ {"name": "report.pdf", "size": 1024000}, {"name": "data.json", "size": 512} ] }); let update = UpdateExecutionInput { status: Some(ExecutionStatus::Completed), result: Some(complex_result.clone()), executor: None, workflow_task: None, }; let updated = ExecutionRepository::update(&pool, created.id, update) .await .unwrap(); assert_eq!(updated.result, Some(complex_result)); }