//! Integration tests for Inquiry repository //! //! These tests verify CRUD operations, queries, and constraints //! for the Inquiry repository. mod helpers; use attune_common::{ models::enums::InquiryStatus, repositories::{ inquiry::{CreateInquiryInput, InquiryRepository, UpdateInquiryInput}, Create, Delete, FindById, List, Update, }, Error, }; use chrono::{Duration, Utc}; use helpers::*; use serde_json::json; // ============================================================================ // CREATE Tests // ============================================================================ #[tokio::test] async fn test_create_inquiry_minimal() { let pool = create_test_pool().await.unwrap(); // Create pack, action, and execution let pack = PackFixture::new_unique("inquiry_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); // Create execution for inquiry use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); // Create inquiry with minimal fields let input = CreateInquiryInput { execution: execution.id, prompt: "Approve deployment?".to_string(), response_schema: None, assigned_to: None, status: InquiryStatus::Pending, response: None, timeout_at: None, }; let inquiry = InquiryRepository::create(&pool, input).await.unwrap(); assert!(inquiry.id > 0); assert_eq!(inquiry.execution, execution.id); assert_eq!(inquiry.prompt, "Approve deployment?"); assert_eq!(inquiry.response_schema, None); assert_eq!(inquiry.assigned_to, None); assert_eq!(inquiry.status, InquiryStatus::Pending); assert_eq!(inquiry.response, None); assert_eq!(inquiry.timeout_at, None); assert_eq!(inquiry.responded_at, None); assert!(inquiry.created.timestamp() > 0); assert!(inquiry.updated.timestamp() > 0); } #[tokio::test] async fn test_create_inquiry_with_response_schema() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("schema_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let response_schema = json!({ "type": "object", "properties": { "approved": {"type": "boolean"}, "reason": {"type": "string"} }, "required": ["approved"] }); let input = CreateInquiryInput { execution: execution.id, prompt: "Approve this action?".to_string(), response_schema: Some(response_schema.clone()), assigned_to: None, status: InquiryStatus::Pending, response: None, timeout_at: None, }; let inquiry = InquiryRepository::create(&pool, input).await.unwrap(); assert_eq!(inquiry.response_schema, Some(response_schema)); } #[tokio::test] async fn test_create_inquiry_with_timeout() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("timeout_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let timeout_at = Utc::now() + Duration::hours(1); let input = CreateInquiryInput { execution: execution.id, prompt: "Time-sensitive approval".to_string(), response_schema: None, assigned_to: None, status: InquiryStatus::Pending, response: None, timeout_at: Some(timeout_at), }; let inquiry = InquiryRepository::create(&pool, input).await.unwrap(); assert!(inquiry.timeout_at.is_some()); let saved_timeout = inquiry.timeout_at.unwrap(); // Allow for small timestamp differences (within 1 second) assert!((saved_timeout.timestamp() - timeout_at.timestamp()).abs() < 1); } #[tokio::test] async fn test_create_inquiry_with_assigned_user() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("assigned_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); // Create an identity to assign to use attune_common::repositories::identity::{CreateIdentityInput, IdentityRepository}; let identity = IdentityRepository::create( &pool, CreateIdentityInput { login: format!("approver_{}", unique_test_id()), display_name: Some("Approver User".to_string()), attributes: json!({"email": format!("approver_{}@example.com", unique_test_id())}), password_hash: None, }, ) .await .unwrap(); let input = CreateInquiryInput { execution: execution.id, prompt: "Review and approve".to_string(), response_schema: None, assigned_to: Some(identity.id), status: InquiryStatus::Pending, response: None, timeout_at: None, }; let inquiry = InquiryRepository::create(&pool, input).await.unwrap(); assert_eq!(inquiry.assigned_to, Some(identity.id)); } #[tokio::test] async fn test_create_inquiry_with_invalid_execution_fails() { let pool = create_test_pool().await.unwrap(); // Try to create inquiry with non-existent execution ID let input = CreateInquiryInput { execution: 99999, prompt: "Test prompt".to_string(), response_schema: None, assigned_to: None, status: InquiryStatus::Pending, response: None, timeout_at: None, }; let result = InquiryRepository::create(&pool, input).await; assert!(result.is_err()); // Foreign key constraint violation } // ============================================================================ // READ Tests // ============================================================================ #[tokio::test] async fn test_find_inquiry_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(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let created_inquiry = InquiryFixture::new_unique(execution.id, "Find me") .with_response_schema(json!({"type": "boolean"})) .create(&pool) .await .unwrap(); let found = InquiryRepository::find_by_id(&pool, created_inquiry.id) .await .unwrap(); assert!(found.is_some()); let inquiry = found.unwrap(); assert_eq!(inquiry.id, created_inquiry.id); assert_eq!(inquiry.execution, created_inquiry.execution); assert_eq!(inquiry.prompt, created_inquiry.prompt); assert_eq!(inquiry.status, created_inquiry.status); } #[tokio::test] async fn test_find_inquiry_by_id_not_found() { let pool = create_test_pool().await.unwrap(); let result = InquiryRepository::find_by_id(&pool, 99999).await.unwrap(); assert!(result.is_none()); } #[tokio::test] async fn test_get_inquiry_by_id() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("get_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let created_inquiry = InquiryFixture::new_unique(execution.id, "Get me") .create(&pool) .await .unwrap(); let inquiry = InquiryRepository::get_by_id(&pool, created_inquiry.id) .await .unwrap(); assert_eq!(inquiry.id, created_inquiry.id); } #[tokio::test] async fn test_get_inquiry_by_id_not_found() { let pool = create_test_pool().await.unwrap(); let result = InquiryRepository::get_by_id(&pool, 99999).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), Error::NotFound { .. })); } // ============================================================================ // LIST Tests // ============================================================================ #[tokio::test] async fn test_list_inquiries_empty() { let pool = create_test_pool().await.unwrap(); let inquiries = InquiryRepository::list(&pool).await.unwrap(); // May have inquiries from other tests, just verify we can list without error drop(inquiries); } #[tokio::test] async fn test_list_inquiries() { 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(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let before_count = InquiryRepository::list(&pool).await.unwrap().len(); // Create multiple inquiries let mut created_ids = vec![]; for i in 0..3 { let inquiry = InquiryFixture::new_unique(execution.id, &format!("Inquiry {}", i)) .create(&pool) .await .unwrap(); created_ids.push(inquiry.id); } let inquiries = InquiryRepository::list(&pool).await.unwrap(); assert!(inquiries.len() >= before_count + 3); // Verify our inquiries are in the list let our_inquiries: Vec<_> = inquiries .iter() .filter(|i| created_ids.contains(&i.id)) .collect(); assert_eq!(our_inquiries.len(), 3); } // ============================================================================ // UPDATE Tests // ============================================================================ #[tokio::test] async fn test_update_inquiry_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(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let inquiry = InquiryFixture::new_unique(execution.id, "Update status") .with_status(InquiryStatus::Pending) .create(&pool) .await .unwrap(); let input = UpdateInquiryInput { status: Some(InquiryStatus::Responded), response: None, responded_at: None, assigned_to: None, }; let updated = InquiryRepository::update(&pool, inquiry.id, input) .await .unwrap(); assert_eq!(updated.id, inquiry.id); assert_eq!(updated.status, InquiryStatus::Responded); assert!(updated.updated > inquiry.updated); } #[tokio::test] async fn test_update_inquiry_status_transitions() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("transitions_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let inquiry = InquiryFixture::new_unique(execution.id, "Transitions") .create(&pool) .await .unwrap(); // Test status transitions: Pending -> Responded let updated = InquiryRepository::update( &pool, inquiry.id, UpdateInquiryInput { status: Some(InquiryStatus::Responded), response: None, responded_at: None, assigned_to: None, }, ) .await .unwrap(); assert_eq!(updated.status, InquiryStatus::Responded); // Test status transition: Responded -> Cancelled (although unusual) let updated = InquiryRepository::update( &pool, inquiry.id, UpdateInquiryInput { status: Some(InquiryStatus::Cancelled), response: None, responded_at: None, assigned_to: None, }, ) .await .unwrap(); assert_eq!(updated.status, InquiryStatus::Cancelled); // Test Timeout status let updated = InquiryRepository::update( &pool, inquiry.id, UpdateInquiryInput { status: Some(InquiryStatus::Timeout), response: None, responded_at: None, assigned_to: None, }, ) .await .unwrap(); assert_eq!(updated.status, InquiryStatus::Timeout); } #[tokio::test] async fn test_update_inquiry_response() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("response_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let inquiry = InquiryFixture::new_unique(execution.id, "Get response") .create(&pool) .await .unwrap(); let response = json!({ "approved": true, "reason": "Looks good to me" }); let input = UpdateInquiryInput { status: None, response: Some(response.clone()), responded_at: None, assigned_to: None, }; let updated = InquiryRepository::update(&pool, inquiry.id, input) .await .unwrap(); assert_eq!(updated.response, Some(response)); } #[tokio::test] async fn test_update_inquiry_with_response_and_status() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("both_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let inquiry = InquiryFixture::new_unique(execution.id, "Complete") .create(&pool) .await .unwrap(); let response = json!({"decision": "approved"}); let responded_at = Utc::now(); let input = UpdateInquiryInput { status: Some(InquiryStatus::Responded), response: Some(response.clone()), responded_at: Some(responded_at), assigned_to: None, }; let updated = InquiryRepository::update(&pool, inquiry.id, input) .await .unwrap(); assert_eq!(updated.status, InquiryStatus::Responded); assert_eq!(updated.response, Some(response)); assert!(updated.responded_at.is_some()); } #[tokio::test] async fn test_update_inquiry_assignment() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("assign_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let inquiry = InquiryFixture::new_unique(execution.id, "Reassign") .create(&pool) .await .unwrap(); // Create an identity to assign to use attune_common::repositories::identity::{CreateIdentityInput, IdentityRepository}; let identity = IdentityRepository::create( &pool, CreateIdentityInput { login: format!("new_approver_{}", unique_test_id()), display_name: Some("New Approver".to_string()), password_hash: None, attributes: json!({"email": format!("new_approver_{}@example.com", unique_test_id())}), }, ) .await .unwrap(); let input = UpdateInquiryInput { status: None, response: None, responded_at: None, assigned_to: Some(identity.id), }; let updated = InquiryRepository::update(&pool, inquiry.id, input) .await .unwrap(); assert_eq!(updated.assigned_to, Some(identity.id)); } #[tokio::test] async fn test_update_inquiry_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(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let inquiry = InquiryFixture::new_unique(execution.id, "No change") .create(&pool) .await .unwrap(); let input = UpdateInquiryInput { status: None, response: None, responded_at: None, assigned_to: None, }; let result = InquiryRepository::update(&pool, inquiry.id, input) .await .unwrap(); // Should return existing inquiry without updating assert_eq!(result.id, inquiry.id); assert_eq!(result.status, inquiry.status); } #[tokio::test] async fn test_update_inquiry_not_found() { let pool = create_test_pool().await.unwrap(); let input = UpdateInquiryInput { status: Some(InquiryStatus::Responded), response: None, responded_at: None, assigned_to: None, }; let result = InquiryRepository::update(&pool, 99999, input).await; // When updating non-existent entity with changes, SQLx returns RowNotFound error assert!(result.is_err()); } // ============================================================================ // DELETE Tests // ============================================================================ #[tokio::test] async fn test_delete_inquiry() { 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(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let inquiry = InquiryFixture::new_unique(execution.id, "Delete me") .create(&pool) .await .unwrap(); let deleted = InquiryRepository::delete(&pool, inquiry.id).await.unwrap(); assert!(deleted); // Verify it's gone let found = InquiryRepository::find_by_id(&pool, inquiry.id) .await .unwrap(); assert!(found.is_none()); } #[tokio::test] async fn test_delete_inquiry_not_found() { let pool = create_test_pool().await.unwrap(); let deleted = InquiryRepository::delete(&pool, 99999).await.unwrap(); assert!(!deleted); } #[tokio::test] async fn test_delete_execution_cascades_to_inquiries() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("cascade_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); // Create inquiries for this execution let inquiry1 = InquiryFixture::new_unique(execution.id, "First") .create(&pool) .await .unwrap(); let inquiry2 = InquiryFixture::new_unique(execution.id, "Second") .create(&pool) .await .unwrap(); // Delete the execution - should cascade to inquiries use attune_common::repositories::Delete; ExecutionRepository::delete(&pool, execution.id) .await .unwrap(); // Verify inquiries are deleted let found1 = InquiryRepository::find_by_id(&pool, inquiry1.id) .await .unwrap(); assert!(found1.is_none()); let found2 = InquiryRepository::find_by_id(&pool, inquiry2.id) .await .unwrap(); assert!(found2.is_none()); } // ============================================================================ // SPECIALIZED QUERY Tests // ============================================================================ #[tokio::test] async fn test_find_inquiries_by_status() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("status_query_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); // Create inquiries with different statuses let inq1 = InquiryFixture::new_unique(execution.id, "Pending 1") .with_status(InquiryStatus::Pending) .create(&pool) .await .unwrap(); let inq2 = InquiryFixture::new_unique(execution.id, "Responded") .with_status(InquiryStatus::Responded) .create(&pool) .await .unwrap(); let inq3 = InquiryFixture::new_unique(execution.id, "Pending 2") .with_status(InquiryStatus::Pending) .create(&pool) .await .unwrap(); let pending_inquiries = InquiryRepository::find_by_status(&pool, InquiryStatus::Pending) .await .unwrap(); // Filter to only our test inquiries let our_pending: Vec<_> = pending_inquiries .iter() .filter(|i| i.id == inq1.id || i.id == inq3.id) .collect(); assert_eq!(our_pending.len(), 2); for inquiry in &our_pending { assert_eq!(inquiry.status, InquiryStatus::Pending); } let responded_inquiries = InquiryRepository::find_by_status(&pool, InquiryStatus::Responded) .await .unwrap(); // Verify our responded inquiry is in the list let our_responded: Vec<_> = responded_inquiries .iter() .filter(|i| i.id == inq2.id) .collect(); assert_eq!(our_responded.len(), 1); } #[tokio::test] async fn test_find_inquiries_by_execution() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("exec_query_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution1 = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let execution2 = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); // Create inquiries for execution1 for i in 0..3 { InquiryFixture::new_unique(execution1.id, &format!("Exec1 inquiry {}", i)) .create(&pool) .await .unwrap(); } // Create inquiries for execution2 for i in 0..2 { InquiryFixture::new_unique(execution2.id, &format!("Exec2 inquiry {}", i)) .create(&pool) .await .unwrap(); } let inquiries = InquiryRepository::find_by_execution(&pool, execution1.id) .await .unwrap(); assert_eq!(inquiries.len(), 3); for inquiry in &inquiries { assert_eq!(inquiry.execution, execution1.id); } } // ============================================================================ // TIMESTAMP Tests // ============================================================================ #[tokio::test] async fn test_inquiry_timestamps_auto_managed() { 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(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let inquiry = InquiryFixture::new_unique(execution.id, "Timestamps") .create(&pool) .await .unwrap(); let created_time = inquiry.created; let updated_time = inquiry.updated; assert!(created_time.timestamp() > 0); assert_eq!(created_time, updated_time); // Update and verify timestamp changed tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let input = UpdateInquiryInput { status: Some(InquiryStatus::Responded), response: None, responded_at: None, assigned_to: None, }; let updated = InquiryRepository::update(&pool, inquiry.id, input) .await .unwrap(); assert_eq!(updated.created, created_time); // created unchanged assert!(updated.updated > updated_time); // updated changed } // ============================================================================ // JSON SCHEMA Tests // ============================================================================ #[tokio::test] async fn test_inquiry_complex_response_schema() { let pool = create_test_pool().await.unwrap(); let pack = PackFixture::new_unique("schema_complex_pack") .create(&pool) .await .unwrap(); let action = ActionFixture::new_unique(pack.id, &pack.r#ref, "action") .create(&pool) .await .unwrap(); use attune_common::repositories::execution::{CreateExecutionInput, ExecutionRepository}; let execution = ExecutionRepository::create( &pool, CreateExecutionInput { action: Some(action.id), action_ref: action.r#ref.clone(), config: None, env_vars: None, parent: None, enforcement: None, executor: None, status: attune_common::models::enums::ExecutionStatus::Requested, result: None, workflow_task: None, }, ) .await .unwrap(); let complex_schema = json!({ "type": "object", "properties": { "severity": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, "impact_analysis": { "type": "object", "properties": { "affected_systems": { "type": "array", "items": {"type": "string"} }, "estimated_downtime": {"type": "number"} } }, "approval": {"type": "boolean"} }, "required": ["severity", "approval"] }); let inquiry = InquiryFixture::new_unique(execution.id, "Complex schema") .with_response_schema(complex_schema.clone()) .create(&pool) .await .unwrap(); assert_eq!(inquiry.response_schema, Some(complex_schema)); }