//! Integration tests for the hindsight CLI commands. //! //! These tests require a running hindsight API server. //! Set HINDSIGHT_API_URL environment variable to point to the server. //! Tests will be skipped if the server is not available. use std::env; use std::process::Command; /// Check if the API server is available fn server_available() -> bool { let api_url = env::var("HINDSIGHT_API_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); let health_url = format!("{}/health", api_url); match reqwest::blocking::get(&health_url) { Ok(resp) => resp.status().is_success(), Err(_) => false, } } /// Helper macro to skip tests when server is not available macro_rules! skip_if_no_server { () => { if !server_available() { eprintln!("Skipping test: API server not available"); return; } }; } /// Get the path to the hindsight binary fn hindsight_binary() -> String { env::var("CARGO_BIN_EXE_hindsight") .unwrap_or_else(|_| { // Try common locations let target_debug = "./target/debug/hindsight"; let target_release = "./target/release/hindsight"; if std::path::Path::new(target_debug).exists() { target_debug.to_string() } else if std::path::Path::new(target_release).exists() { target_release.to_string() } else { "hindsight".to_string() } }) } /// Test bank ID for integration tests - each test needs a unique bank ID /// to avoid parallel test interference fn test_bank_id(test_name: &str) -> String { format!("cli-test-{}-{}", test_name, std::process::id()) } /// Run a hindsight CLI command fn run_hindsight(args: &[&str]) -> std::process::Output { let api_url = env::var("HINDSIGHT_API_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); Command::new(hindsight_binary()) .env("HINDSIGHT_API_URL", &api_url) .args(args) .output() .expect("Failed to execute hindsight command") } #[test] fn test_health_check() { skip_if_no_server!(); let output = run_hindsight(&["health"]); // Should succeed or fail gracefully let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Either succeeded with "healthy" output or has a reasonable error if output.status.success() { // Note: output may contain ANSI color codes, so check for key text assert!( stdout.contains("healthy") || stdout.contains("Health") || stdout.contains("status"), "Expected health check output, got: {} / {}", stdout, stderr ); } } #[test] fn test_health_check_json_output() { skip_if_no_server!(); let output = run_hindsight(&["health", "-o", "json"]); if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); // Should be valid JSON let result: serde_json::Value = serde_json::from_str(&stdout) .expect(&format!("Expected valid JSON output, got: {}", stdout)); // Should have status field assert!(result.get("status").is_some(), "Expected status field in health response"); } } #[test] fn test_bank_list() { skip_if_no_server!(); let output = run_hindsight(&["bank", "list"]); // Should succeed (even if no banks exist) let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "Bank list command failed: {} / {}", stdout, stderr ); } #[test] fn test_bank_list_json_output() { skip_if_no_server!(); let output = run_hindsight(&["bank", "list", "-o", "json"]); if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); // Should be valid JSON array let _result: serde_json::Value = serde_json::from_str(&stdout) .expect(&format!("Expected valid JSON output, got: {}", stdout)); } } #[test] fn test_bank_create_and_delete() { skip_if_no_server!(); let bank_id = test_bank_id("create-delete"); // Create a bank let output = run_hindsight(&[ "bank", "create", &bank_id, "--name", "Test Bank", "--mission", "A test bank for CLI integration tests", ]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Bank might already exist, which is OK let created = output.status.success(); // Get bank disposition let output = run_hindsight(&["bank", "disposition", &bank_id]); if created { assert!( output.status.success(), "Bank disposition command failed: {} / {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } // Clean up: delete the bank let output = run_hindsight(&["bank", "delete", &bank_id, "-y"]); // Deletion should succeed if created { assert!( output.status.success(), "Bank delete command failed: {} / {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } } #[test] fn test_memory_list() { skip_if_no_server!(); let bank_id = test_bank_id("memory-list"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // List memories (should be empty for new bank) let output = run_hindsight(&["memory", "list", &bank_id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed (even if empty) assert!( output.status.success(), "Memory list command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_mental_model_list() { skip_if_no_server!(); let bank_id = test_bank_id("mm-list"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // List mental models let output = run_hindsight(&["mental-model", "list", &bank_id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed assert!( output.status.success(), "Mental model list command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_mental_model_create_and_delete() { skip_if_no_server!(); let bank_id = test_bank_id("mm-create"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // Create a mental model let output = run_hindsight(&[ "mental-model", "create", &bank_id, "Test Model", "A test mental model", ]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // The create command should succeed assert!( output.status.success(), "Mental model create failed: stdout={}, stderr={}", stdout, stderr ); // Verify it's in the list let output = run_hindsight(&["mental-model", "list", &bank_id, "-o", "json"]); let stdout = String::from_utf8_lossy(&output.stdout); assert!( output.status.success(), "Mental model list failed: {}", stdout ); // Parse JSON and verify model exists if let Ok(result) = serde_json::from_str::(&stdout) { if let Some(items) = result.get("items").and_then(|v| v.as_array()) { // Check if any model has the name "Test Model" let found = items.iter().any(|item| { item.get("name").and_then(|v| v.as_str()) == Some("Test Model") }); assert!(found, "Expected to find 'Test Model' in mental models list: {}", stdout); } } // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_tag_list() { skip_if_no_server!(); let bank_id = test_bank_id("tag-list"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // List tags let output = run_hindsight(&["tag", "list", &bank_id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed (even if no tags) assert!( output.status.success(), "Tag list command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_entity_list() { skip_if_no_server!(); let bank_id = test_bank_id("entity-list"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // List entities let output = run_hindsight(&["entity", "list", &bank_id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed (even if no entities) assert!( output.status.success(), "Entity list command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_operation_list() { skip_if_no_server!(); let bank_id = test_bank_id("op-list"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // List operations let output = run_hindsight(&["operation", "list", &bank_id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed (even if no operations) assert!( output.status.success(), "Operation list command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_bank_stats() { skip_if_no_server!(); let bank_id = test_bank_id("bank-stats"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // Get stats let output = run_hindsight(&["bank", "stats", &bank_id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed assert!( output.status.success(), "Bank stats command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_bank_graph() { skip_if_no_server!(); let bank_id = test_bank_id("bank-graph"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // Get graph let output = run_hindsight(&["bank", "graph", &bank_id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed (even if empty graph) assert!( output.status.success(), "Bank graph command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_bank_update() { skip_if_no_server!(); let bank_id = test_bank_id("bank-update"); // Create the bank first let output = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); if output.status.success() { // Update the bank let output = run_hindsight(&[ "bank", "update", &bank_id, "--name", "Updated Test Bank", ]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "Bank update command failed: {} / {}", stdout, stderr ); // Verify the update let output = run_hindsight(&["bank", "disposition", &bank_id, "-o", "json"]); if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let result: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!( result.get("name").and_then(|v| v.as_str()), Some("Updated Test Bank") ); } } // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_json_yaml_output_formats() { skip_if_no_server!(); // Test JSON output for bank list let output = run_hindsight(&["bank", "list", "-o", "json"]); if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let _: serde_json::Value = serde_json::from_str(&stdout) .expect("Expected valid JSON for bank list"); } // Test YAML output for bank list let output = run_hindsight(&["bank", "list", "-o", "yaml"]); if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let _: serde_yaml::Value = serde_yaml::from_str(&stdout) .expect("Expected valid YAML for bank list"); } } // ============================================================================ // Directive Tests // ============================================================================ #[test] fn test_directive_list() { skip_if_no_server!(); let bank_id = test_bank_id("dir-list"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // List directives let output = run_hindsight(&["directive", "list", &bank_id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed (even if empty) assert!( output.status.success(), "Directive list command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_directive_create_get_update_delete() { skip_if_no_server!(); let bank_id = test_bank_id("dir-crud"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // Create a directive let output = run_hindsight(&[ "directive", "create", &bank_id, "Test Directive", "Always respond politely", ]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "Directive create failed: stdout={}, stderr={}", stdout, stderr ); // List directives and get the ID let output = run_hindsight(&["directive", "list", &bank_id, "-o", "json"]); let stdout = String::from_utf8_lossy(&output.stdout); assert!( output.status.success(), "Directive list failed: {}", stdout ); // Parse JSON and get directive ID let directive_id: Option = if let Ok(result) = serde_json::from_str::(&stdout) { result.get("items") .and_then(|v| v.as_array()) .and_then(|items| items.first()) .and_then(|item| item.get("id")) .and_then(|v| v.as_str()) .map(|s| s.to_string()) } else { None }; if let Some(id) = directive_id { // Get the directive let output = run_hindsight(&["directive", "get", &bank_id, &id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "Directive get failed: stdout={}, stderr={}", stdout, stderr ); // Update the directive let output = run_hindsight(&[ "directive", "update", &bank_id, &id, "--name", "Updated Directive", "--content", "Always respond very politely", ]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "Directive update failed: stdout={}, stderr={}", stdout, stderr ); // Verify update in JSON let output = run_hindsight(&["directive", "get", &bank_id, &id, "-o", "json"]); if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let result: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!( result.get("name").and_then(|v| v.as_str()), Some("Updated Directive") ); } // Delete the directive let output = run_hindsight(&["directive", "delete", &bank_id, &id, "-y"]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "Directive delete failed: stdout={}, stderr={}", stdout, stderr ); } // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } // ============================================================================ // Mental Model Extended Tests // ============================================================================ #[test] fn test_mental_model_get() { skip_if_no_server!(); let bank_id = test_bank_id("mm-get"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // Create a mental model let output = run_hindsight(&[ "mental-model", "create", &bank_id, "Test Get Model", "What are the key facts?", ]); if output.status.success() { // List to get the ID let output = run_hindsight(&["mental-model", "list", &bank_id, "-o", "json"]); let stdout = String::from_utf8_lossy(&output.stdout); if let Ok(result) = serde_json::from_str::(&stdout) { if let Some(id) = result.get("items") .and_then(|v| v.as_array()) .and_then(|items| items.iter().find(|item| { item.get("name").and_then(|v| v.as_str()) == Some("Test Get Model") })) .and_then(|item| item.get("id")) .and_then(|v| v.as_str()) { // Get the mental model let output = run_hindsight(&["mental-model", "get", &bank_id, id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "Mental model get failed: stdout={}, stderr={}", stdout, stderr ); } } } // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_mental_model_update() { skip_if_no_server!(); let bank_id = test_bank_id("mm-update"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // Create a mental model let output = run_hindsight(&[ "mental-model", "create", &bank_id, "Test Update Model", "What are the key facts?", ]); if output.status.success() { // List to get the ID let output = run_hindsight(&["mental-model", "list", &bank_id, "-o", "json"]); let stdout = String::from_utf8_lossy(&output.stdout); if let Ok(result) = serde_json::from_str::(&stdout) { if let Some(id) = result.get("items") .and_then(|v| v.as_array()) .and_then(|items| items.iter().find(|item| { item.get("name").and_then(|v| v.as_str()) == Some("Test Update Model") })) .and_then(|item| item.get("id")) .and_then(|v| v.as_str()) { // Update the mental model let output = run_hindsight(&[ "mental-model", "update", &bank_id, id, "--name", "Updated Model Name", ]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "Mental model update failed: stdout={}, stderr={}", stdout, stderr ); // Verify update let output = run_hindsight(&["mental-model", "get", &bank_id, id, "-o", "json"]); if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let result: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!( result.get("name").and_then(|v| v.as_str()), Some("Updated Model Name") ); } } } } // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_mental_model_refresh() { skip_if_no_server!(); let bank_id = test_bank_id("mm-refresh"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // Create a mental model let output = run_hindsight(&[ "mental-model", "create", &bank_id, "Test Refresh Model", "What are the key facts?", ]); if output.status.success() { // List to get the ID let output = run_hindsight(&["mental-model", "list", &bank_id, "-o", "json"]); let stdout = String::from_utf8_lossy(&output.stdout); if let Ok(result) = serde_json::from_str::(&stdout) { if let Some(id) = result.get("items") .and_then(|v| v.as_array()) .and_then(|items| items.iter().find(|item| { item.get("name").and_then(|v| v.as_str()) == Some("Test Refresh Model") })) .and_then(|item| item.get("id")) .and_then(|v| v.as_str()) { // Refresh the mental model let output = run_hindsight(&["mental-model", "refresh", &bank_id, id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!( output.status.success(), "Mental model refresh failed: stdout={}, stderr={}", stdout, stderr ); } } } // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } // ============================================================================ // Bank Consolidation Tests // ============================================================================ #[test] fn test_bank_consolidate() { skip_if_no_server!(); let bank_id = test_bank_id("bank-consolidate"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // Trigger consolidation let output = run_hindsight(&["bank", "consolidate", &bank_id]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed assert!( output.status.success(), "Bank consolidate command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } #[test] fn test_bank_clear_observations() { skip_if_no_server!(); let bank_id = test_bank_id("bank-clear-obs"); // Create the bank first let _ = run_hindsight(&["bank", "create", &bank_id, "--name", "Test Bank"]); // Clear observations let output = run_hindsight(&["bank", "clear-observations", &bank_id, "-y"]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed assert!( output.status.success(), "Bank clear-observations command failed: {} / {}", stdout, stderr ); // Clean up let _ = run_hindsight(&["bank", "delete", &bank_id, "-y"]); } // ============================================================================ // Version Test // ============================================================================ #[test] fn test_version() { skip_if_no_server!(); let output = run_hindsight(&["version"]); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Should succeed assert!( output.status.success(), "Version command failed: {} / {}", stdout, stderr ); } #[test] fn test_version_json() { skip_if_no_server!(); let output = run_hindsight(&["version", "-o", "json"]); if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let result: serde_json::Value = serde_json::from_str(&stdout) .expect(&format!("Expected valid JSON output, got: {}", stdout)); // Should have api_version and features assert!(result.get("api_version").is_some(), "Expected api_version field"); assert!(result.get("features").is_some(), "Expected features field"); } }