chore: turn clippy on for test code (#4817)

This commit is contained in:
Jack Amadeo
2025-09-26 00:06:07 -04:00
committed by GitHub
parent f5877a9996
commit 757ceb6109
38 changed files with 660 additions and 849 deletions
+1 -1
View File
@@ -192,7 +192,7 @@ response:
let url = result.unwrap(); let url = result.unwrap();
assert!(url.starts_with("goose://recipe?config=")); assert!(url.starts_with("goose://recipe?config="));
let encoded_part = url.strip_prefix("goose://recipe?config=").unwrap(); let encoded_part = url.strip_prefix("goose://recipe?config=").unwrap();
assert!(encoded_part.len() > 0); assert!(!encoded_part.is_empty());
} }
#[test] #[test]
+6 -6
View File
@@ -248,7 +248,7 @@ mod tests {
); );
// Create a proper temporary directory that will be automatically cleaned up // Create a proper temporary directory that will be automatically cleaned up
let _temp_dir = TempDir::with_prefix(&format!("goose_test_{}_", test_id)).unwrap(); let _temp_dir = TempDir::with_prefix(format!("goose_test_{}_", test_id)).unwrap();
let test_dir = _temp_dir.path(); let test_dir = _temp_dir.path();
// Set up environment // Set up environment
@@ -275,7 +275,7 @@ mod tests {
if log_dir.exists() { if log_dir.exists() {
for entry in std::fs::read_dir(&log_dir).unwrap() { for entry in std::fs::read_dir(&log_dir).unwrap() {
let entry = entry.unwrap(); let entry = entry.unwrap();
if entry.path().extension().map_or(false, |ext| ext == "log") { if entry.path().extension().is_some_and(|ext| ext == "log") {
std::fs::remove_file(entry.path()).unwrap(); std::fs::remove_file(entry.path()).unwrap();
} }
} }
@@ -315,7 +315,7 @@ mod tests {
let log_count = all_files let log_count = all_files
.iter() .iter()
.filter(|e| e.path().extension().map_or(false, |ext| ext == "log")) .filter(|e| e.path().extension().is_some_and(|ext| ext == "log"))
.count(); .count();
println!("Found {} log files in directory", log_count); println!("Found {} log files in directory", log_count);
@@ -358,8 +358,8 @@ mod tests {
.iter() .iter()
.filter(|e| { .filter(|e| {
let path = e.path(); let path = e.path();
let matches = path.extension().map_or(false, |ext| ext == "log") let matches = path.extension().is_some_and(|ext| ext == "log")
&& path.file_name().map_or(false, |name| { && path.file_name().is_some_and(|name| {
let name = name.to_string_lossy(); let name = name.to_string_lossy();
if let Some(session) = session_name { if let Some(session) = session_name {
name.ends_with(&format!("{}.log", session)) name.ends_with(&format!("{}.log", session))
@@ -411,7 +411,7 @@ mod tests {
}) })
.collect(); .collect();
log_files.sort_by(|a, b| a.file_name().cmp(&b.file_name())); log_files.sort_by_key(|a| a.file_name());
assert_eq!(log_files.len(), 1, "Expected exactly one matching log file"); assert_eq!(log_files.len(), 1, "Expected exactly one matching log file");
let log_file_name = log_files[0].file_name().to_string_lossy().into_owned(); let log_file_name = log_files[0].file_name().to_string_lossy().into_owned();
@@ -14,9 +14,11 @@ use std::collections::HashMap;
use tokio::sync::mpsc::{self, Receiver}; use tokio::sync::mpsc::{self, Receiver};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
type Handler = Box<dyn Fn(&Value) -> Result<Vec<Content>, ErrorData> + Send + Sync>;
pub struct MockClient { pub struct MockClient {
tools: HashMap<String, Tool>, tools: HashMap<String, Tool>,
handlers: HashMap<String, Box<dyn Fn(&Value) -> Result<Vec<Content>, ErrorData> + Send + Sync>>, handlers: HashMap<String, Handler>,
} }
impl MockClient { impl MockClient {
@@ -180,7 +180,7 @@ where
let original_env = setup_environment(config)?; let original_env = setup_environment(config)?;
let inner_provider = create(&factory_name, ModelConfig::new(&config.model_name)?)?; let inner_provider = create(&factory_name, ModelConfig::new(config.model_name)?)?;
let test_provider = Arc::new(TestProvider::new_recording(inner_provider, &file_path)); let test_provider = Arc::new(TestProvider::new_recording(inner_provider, &file_path));
( (
+6 -10
View File
@@ -443,18 +443,14 @@ mod tests {
let mut cache = CompletionCache::new(); let mut cache = CompletionCache::new();
// Add some test prompts // Add some test prompts
let mut extension1_prompts = Vec::new(); cache.prompts.insert(
extension1_prompts.push("test_prompt1".to_string()); "extension1".to_string(),
extension1_prompts.push("test_prompt2".to_string()); vec!["test_prompt1".to_string(), "test_prompt2".to_string()],
cache );
.prompts
.insert("extension1".to_string(), extension1_prompts);
let mut extension2_prompts = Vec::new();
extension2_prompts.push("other_prompt".to_string());
cache cache
.prompts .prompts
.insert("extension2".to_string(), extension2_prompts); .insert("extension2".to_string(), vec!["other_prompt".to_string()]);
// Add prompt info with arguments // Add prompt info with arguments
let test_prompt1_args = vec![ let test_prompt1_args = vec![
@@ -519,7 +515,7 @@ mod tests {
let (pos, candidates) = completer.complete_slash_commands("/e").unwrap(); let (pos, candidates) = completer.complete_slash_commands("/e").unwrap();
assert_eq!(pos, 0); assert_eq!(pos, 0);
// There might be multiple commands starting with "e" like "/exit" and "/extension" // There might be multiple commands starting with "e" like "/exit" and "/extension"
assert!(candidates.len() >= 1); assert!(!candidates.is_empty());
// Test multiple matches // Test multiple matches
let (pos, candidates) = completer.complete_slash_commands("/").unwrap(); let (pos, candidates) = completer.complete_slash_commands("/").unwrap();
@@ -34,7 +34,7 @@ fn test_format_result_data_for_display() {
assert_eq!(format_result_data_for_display(&json!(true)), "true"); assert_eq!(format_result_data_for_display(&json!(true)), "true");
assert_eq!(format_result_data_for_display(&json!(false)), "false"); assert_eq!(format_result_data_for_display(&json!(false)), "false");
assert_eq!(format_result_data_for_display(&json!(42)), "42"); assert_eq!(format_result_data_for_display(&json!(42)), "42");
assert_eq!(format_result_data_for_display(&json!(3.14)), "3.14"); assert_eq!(format_result_data_for_display(&json!(3.41)), "3.41");
assert_eq!(format_result_data_for_display(&json!(null)), "null"); assert_eq!(format_result_data_for_display(&json!(null)), "null");
let partial_obj = json!({ let partial_obj = json!({
@@ -134,7 +134,7 @@ fn test_symlink_handling() {
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
let _ = symlink(&dir_path.join("target.rs"), dir_path.join("link.rs")); let _ = symlink(dir_path.join("target.rs"), dir_path.join("link.rs"));
let _ = symlink(&target_dir, dir_path.join("link_dir")); let _ = symlink(&target_dir, dir_path.join("link_dir"));
} }
@@ -2970,7 +2970,7 @@ mod tests {
let temp_path = temp_dir.path(); let temp_path = temp_dir.path();
// Set the current directory before creating the server // Set the current directory before creating the server
std::env::set_current_dir(&temp_path).unwrap(); std::env::set_current_dir(temp_path).unwrap();
// Create some test files and directories // Create some test files and directories
fs::create_dir(temp_path.join("subdir1")).unwrap(); fs::create_dir(temp_path.join("subdir1")).unwrap();
@@ -3029,7 +3029,7 @@ mod tests {
let temp_path = temp_dir.path(); let temp_path = temp_dir.path();
// Set the current directory before creating the server // Set the current directory before creating the server
std::env::set_current_dir(&temp_path).unwrap(); std::env::set_current_dir(temp_path).unwrap();
// Create more than 50 files to test the limit // Create more than 50 files to test the limit
for i in 0..60 { for i in 0..60 {
@@ -3085,7 +3085,7 @@ mod tests {
let temp_path = temp_dir.path(); let temp_path = temp_dir.path();
// Set the current directory before creating the server // Set the current directory before creating the server
std::env::set_current_dir(&temp_path).unwrap(); std::env::set_current_dir(temp_path).unwrap();
let server = create_test_server(); let server = create_test_server();
@@ -3666,7 +3666,7 @@ Additional instructions here.
// Cancel the command // Cancel the command
let cancel_params = CancelledNotificationParam { let cancel_params = CancelledNotificationParam {
request_id: request_id, request_id,
reason: Some("test cancellation".to_string()), reason: Some("test cancellation".to_string()),
}; };
@@ -3748,7 +3748,7 @@ Additional instructions here.
// Cancel the command // Cancel the command
let cancel_params = CancelledNotificationParam { let cancel_params = CancelledNotificationParam {
request_id: request_id, request_id,
reason: Some("test cancellation".to_string()), reason: Some("test cancellation".to_string()),
}; };
-2
View File
@@ -391,7 +391,6 @@ pub fn routes(state: Arc<AppState>) -> Router {
mod tests { mod tests {
use super::*; use super::*;
use axum::{body::Body, http::Request}; use axum::{body::Body, http::Request};
use serde_json::json;
use tower::ServiceExt; use tower::ServiceExt;
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@@ -424,7 +423,6 @@ mod tests {
let state = AppState::new().await.unwrap(); let state = AppState::new().await.unwrap();
let app = routes(state); let app = routes(state);
let large_data = "a".repeat(30 * 1024 * 1024); // 30MB
let request = Request::builder() let request = Request::builder()
.uri("/audio/transcribe") .uri("/audio/transcribe")
.method("POST") .method("POST")
+6 -12
View File
@@ -631,25 +631,19 @@ mod tests {
#[test] #[test]
fn test_make_full() { fn test_make_full() {
assert_eq!( assert_eq!(
make_full_cmd("uvx", &vec!["mcp_slack".to_string()]), make_full_cmd("uvx", &["mcp_slack".to_string()]),
"uvx mcp_slack" "uvx mcp_slack"
); );
assert_eq!( assert_eq!(
make_full_cmd("uvx", &vec!["mcp_slack ".to_string()]), make_full_cmd("uvx", &["mcp_slack ".to_string()]),
"uvx mcp_slack" "uvx mcp_slack"
); );
assert_eq!( assert_eq!(
make_full_cmd( make_full_cmd("uvx", &["mcp_slack".to_string(), "--verbose".to_string()]),
"uvx",
&vec!["mcp_slack".to_string(), "--verbose".to_string()]
),
"uvx mcp_slack --verbose" "uvx mcp_slack --verbose"
); );
assert_eq!( assert_eq!(
make_full_cmd( make_full_cmd("uvx", &["mcp_slack".to_string(), " --verbose".to_string()]),
"uvx",
&vec!["mcp_slack".to_string(), " --verbose".to_string()]
),
"uvx mcp_slack --verbose" "uvx mcp_slack --verbose"
); );
} }
@@ -1093,14 +1087,14 @@ mod tests {
// With bypass enabled, any command should be allowed regardless of allowlist // With bypass enabled, any command should be allowed regardless of allowlist
assert!(is_command_allowed( assert!(is_command_allowed(
"uvx", "uvx",
&vec!["unauthorized_command".to_string()] &["unauthorized_command".to_string()]
)); ));
// Test case insensitivity // Test case insensitivity
env::set_var("GOOSE_ALLOWLIST_BYPASS", "TRUE"); env::set_var("GOOSE_ALLOWLIST_BYPASS", "TRUE");
assert!(is_command_allowed( assert!(is_command_allowed(
"uvx", "uvx",
&vec!["unauthorized_command".to_string()] &["unauthorized_command".to_string()]
)); ));
// Clean up // Clean up
@@ -129,6 +129,6 @@ recipe: recipe_content
let result = RecipeManifestMetadata::from_yaml_file(&file_path).unwrap(); let result = RecipeManifestMetadata::from_yaml_file(&file_path).unwrap();
assert_eq!(result.name, "Test Recipe"); assert_eq!(result.name, "Test Recipe");
assert_eq!(result.is_global, true); assert!(result.is_global);
} }
} }
-43
View File
@@ -557,58 +557,15 @@ pub fn routes(state: Arc<AppState>) -> Router {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use goose::conversation::message::Message;
use goose::{
model::ModelConfig,
providers::{
base::{Provider, ProviderUsage, Usage},
errors::ProviderError,
},
};
#[derive(Clone)]
struct MockProvider {
model_config: ModelConfig,
}
#[async_trait::async_trait]
impl Provider for MockProvider {
fn metadata() -> goose::providers::base::ProviderMetadata {
goose::providers::base::ProviderMetadata::empty()
}
async fn complete_with_model(
&self,
_model_config: &ModelConfig,
_system: &str,
_messages: &[Message],
_tools: &[rmcp::model::Tool],
) -> anyhow::Result<(Message, ProviderUsage), ProviderError> {
Ok((
Message::assistant().with_text("Mock response"),
ProviderUsage::new("mock".to_string(), Usage::default()),
))
}
fn get_model_config(&self) -> ModelConfig {
self.model_config.clone()
}
}
mod integration_tests { mod integration_tests {
use super::*; use super::*;
use axum::{body::Body, http::Request}; use axum::{body::Body, http::Request};
use goose::conversation::message::Message; use goose::conversation::message::Message;
use serde_json::json;
use tower::ServiceExt; use tower::ServiceExt;
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_reply_endpoint() { async fn test_reply_endpoint() {
let mock_model_config = ModelConfig::new("test-model").unwrap();
let mock_provider = MockProvider {
model_config: mock_model_config,
};
let state = AppState::new().await.unwrap(); let state = AppState::new().await.unwrap();
let app = routes(state); let app = routes(state);
+2 -2
View File
@@ -31,9 +31,9 @@ async fn main() -> Result<()> {
let mut usage = Usage::default(); let mut usage = Usage::default();
while let Some(Ok((msg, usage_part))) = stream.next().await { while let Some(Ok((msg, usage_part))) = stream.next().await {
dbg!(msg); dbg!(msg);
usage_part.map(|u| { if let Some(u) = usage_part {
usage += u.usage; usage += u.usage;
}); }
} }
println!("\nToken Usage:"); println!("\nToken Usage:");
println!("------------"); println!("------------");
@@ -6,14 +6,13 @@ use serde_json::json;
use crate::agents::recipe_tools::param_utils::prepare_command_params; use crate::agents::recipe_tools::param_utils::prepare_command_params;
fn setup_default_sub_recipe() -> SubRecipe { fn setup_default_sub_recipe() -> SubRecipe {
let sub_recipe = SubRecipe { SubRecipe {
name: "test_sub_recipe".to_string(), name: "test_sub_recipe".to_string(),
path: "test_sub_recipe.yaml".to_string(), path: "test_sub_recipe.yaml".to_string(),
values: Some(HashMap::from([("key1".to_string(), "value1".to_string())])), values: Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
sequential_when_repeated: true, sequential_when_repeated: true,
description: Some("Test subrecipe".to_string()), description: Some("Test subrecipe".to_string()),
}; }
sub_recipe
} }
mod prepare_command_params_tests { mod prepare_command_params_tests {
@@ -1,51 +1,48 @@
#[cfg(test)] use std::collections::HashMap;
mod tests {
use std::collections::HashMap;
use crate::recipe::SubRecipe; use crate::recipe::SubRecipe;
use serde_json::json; use serde_json::json;
use serde_json::Value; use serde_json::Value;
use tempfile::TempDir; use tempfile::TempDir;
fn setup_default_sub_recipe() -> SubRecipe { fn setup_default_sub_recipe() -> SubRecipe {
let sub_recipe = SubRecipe { SubRecipe {
name: "test_sub_recipe".to_string(), name: "test_sub_recipe".to_string(),
path: "test_sub_recipe.yaml".to_string(), path: "test_sub_recipe.yaml".to_string(),
values: Some(HashMap::from([("key1".to_string(), "value1".to_string())])), values: Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
sequential_when_repeated: true, sequential_when_repeated: true,
description: Some("Test subrecipe".to_string()), description: Some("Test subrecipe".to_string()),
}; }
sub_recipe }
mod get_input_schema {
use super::*;
use crate::agents::recipe_tools::sub_recipe_tools::get_input_schema;
fn prepare_sub_recipe(sub_recipe_file_content: &str) -> (SubRecipe, TempDir) {
let mut sub_recipe = setup_default_sub_recipe();
let temp_dir = tempfile::tempdir().unwrap();
let temp_file = temp_dir.path().join(sub_recipe.path.clone());
std::fs::write(&temp_file, sub_recipe_file_content).unwrap();
sub_recipe.path = temp_file.to_string_lossy().to_string();
(sub_recipe, temp_dir)
} }
mod get_input_schema { fn verify_task_parameters(result: Value, expected_task_parameters_items: Value) {
use super::*; let task_parameters = result
use crate::agents::recipe_tools::sub_recipe_tools::get_input_schema; .get("properties")
.unwrap()
.as_object()
.unwrap()
.get("task_parameters")
.unwrap()
.as_object()
.unwrap();
let task_parameters_items = task_parameters.get("items").unwrap();
assert_eq!(&expected_task_parameters_items, task_parameters_items);
}
fn prepare_sub_recipe(sub_recipe_file_content: &str) -> (SubRecipe, TempDir) { const SUB_RECIPE_FILE_CONTENT_WITH_TWO_PARAMS: &str = r#"{
let mut sub_recipe = setup_default_sub_recipe();
let temp_dir = tempfile::tempdir().unwrap();
let temp_file = temp_dir.path().join(sub_recipe.path.clone());
std::fs::write(&temp_file, sub_recipe_file_content).unwrap();
sub_recipe.path = temp_file.to_string_lossy().to_string();
(sub_recipe, temp_dir)
}
fn verify_task_parameters(result: Value, expected_task_parameters_items: Value) {
let task_parameters = result
.get("properties")
.unwrap()
.as_object()
.unwrap()
.get("task_parameters")
.unwrap()
.as_object()
.unwrap();
let task_parameters_items = task_parameters.get("items").unwrap();
assert_eq!(&expected_task_parameters_items, task_parameters_items);
}
const SUB_RECIPE_FILE_CONTENT_WITH_TWO_PARAMS: &str = r#"{
"version": "1.0.0", "version": "1.0.0",
"title": "Test Recipe", "title": "Test Recipe",
"description": "A test recipe", "description": "A test recipe",
@@ -66,67 +63,66 @@ mod tests {
] ]
}"#; }"#;
#[test] #[test]
fn test_with_one_param_in_tool_input() { fn test_with_one_param_in_tool_input() {
let (mut sub_recipe, _temp_dir) = let (mut sub_recipe, _temp_dir) =
prepare_sub_recipe(SUB_RECIPE_FILE_CONTENT_WITH_TWO_PARAMS); prepare_sub_recipe(SUB_RECIPE_FILE_CONTENT_WITH_TWO_PARAMS);
sub_recipe.values = Some(HashMap::from([("key1".to_string(), "value1".to_string())])); sub_recipe.values = Some(HashMap::from([("key1".to_string(), "value1".to_string())]));
let result = get_input_schema(&sub_recipe).unwrap(); let result = get_input_schema(&sub_recipe).unwrap();
verify_task_parameters( verify_task_parameters(
result, result,
json!({ json!({
"type": "object", "type": "object",
"properties": { "properties": {
"key2": { "type": "number", "description": "An optional parameter" } "key2": { "type": "number", "description": "An optional parameter" }
}, },
"required": [] "required": []
}), }),
); );
} }
#[test] #[test]
fn test_without_param_in_tool_input() { fn test_without_param_in_tool_input() {
let (mut sub_recipe, _temp_dir) = let (mut sub_recipe, _temp_dir) =
prepare_sub_recipe(SUB_RECIPE_FILE_CONTENT_WITH_TWO_PARAMS); prepare_sub_recipe(SUB_RECIPE_FILE_CONTENT_WITH_TWO_PARAMS);
sub_recipe.values = Some(HashMap::from([ sub_recipe.values = Some(HashMap::from([
("key1".to_string(), "value1".to_string()), ("key1".to_string(), "value1".to_string()),
("key2".to_string(), "value2".to_string()), ("key2".to_string(), "value2".to_string()),
])); ]));
let result = get_input_schema(&sub_recipe).unwrap(); let result = get_input_schema(&sub_recipe).unwrap();
assert_eq!( assert_eq!(
None, None,
result result
.get("properties") .get("properties")
.unwrap() .unwrap()
.as_object() .as_object()
.unwrap() .unwrap()
.get("task_parameters") .get("task_parameters")
); );
} }
#[test] #[test]
fn test_with_all_params_in_tool_input() { fn test_with_all_params_in_tool_input() {
let (mut sub_recipe, _temp_dir) = let (mut sub_recipe, _temp_dir) =
prepare_sub_recipe(SUB_RECIPE_FILE_CONTENT_WITH_TWO_PARAMS); prepare_sub_recipe(SUB_RECIPE_FILE_CONTENT_WITH_TWO_PARAMS);
sub_recipe.values = None; sub_recipe.values = None;
let result = get_input_schema(&sub_recipe).unwrap(); let result = get_input_schema(&sub_recipe).unwrap();
verify_task_parameters( verify_task_parameters(
result, result,
json!({ json!({
"type": "object", "type": "object",
"properties": { "properties": {
"key1": { "type": "string", "description": "A test parameter" }, "key1": { "type": "string", "description": "A test parameter" },
"key2": { "type": "number", "description": "An optional parameter" } "key2": { "type": "number", "description": "An optional parameter" }
}, },
"required": ["key1"] "required": ["key1"]
}), }),
); );
}
} }
} }
+7 -7
View File
@@ -1024,7 +1024,7 @@ mod tests {
let key = format!("key{}", i); let key = format!("key{}", i);
let value = format!("value{}", i); let value = format!("value{}", i);
assert!( assert!(
final_values.get(&key).is_some(), final_values.contains_key(&key),
"Missing key {} in final values", "Missing key {} in final values",
key key
); );
@@ -1075,7 +1075,7 @@ mod tests {
// Should have recovered the data // Should have recovered the data
assert!( assert!(
recovered_values.len() >= 1, !recovered_values.is_empty(),
"Should have recovered at least one key" "Should have recovered at least one key"
); );
@@ -1169,7 +1169,7 @@ mod tests {
// Should have recovered the data from backup // Should have recovered the data from backup
assert!( assert!(
recovered_values.len() >= 1, !recovered_values.is_empty(),
"Should have recovered data from backup" "Should have recovered data from backup"
); );
@@ -1260,10 +1260,10 @@ mod tests {
assert_eq!(value, Value::Number((-123).into())); assert_eq!(value, Value::Number((-123).into()));
// Test floats // Test floats
let value = Config::parse_env_value("3.14")?; let value = Config::parse_env_value("3.41")?;
assert!(matches!(value, Value::Number(_))); assert!(matches!(value, Value::Number(_)));
if let Value::Number(n) = value { if let Value::Number(n) = value {
assert_eq!(n.as_f64().unwrap(), 3.14); assert_eq!(n.as_f64().unwrap(), 3.41);
} }
let value = Config::parse_env_value("0.01")?; let value = Config::parse_env_value("0.01")?;
@@ -1421,7 +1421,7 @@ mod tests {
// Test boolean environment variable // Test boolean environment variable
std::env::set_var("ENABLED", "true"); std::env::set_var("ENABLED", "true");
let value: bool = config.get_param("enabled")?; let value: bool = config.get_param("enabled")?;
assert_eq!(value, true); assert!(value);
// Test JSON object environment variable // Test JSON object environment variable
std::env::set_var("CONFIG", "{\"debug\": true, \"level\": 5}"); std::env::set_var("CONFIG", "{\"debug\": true, \"level\": 5}");
@@ -1431,7 +1431,7 @@ mod tests {
level: i32, level: i32,
} }
let value: TestConfig = config.get_param("config")?; let value: TestConfig = config.get_param("config")?;
assert_eq!(value.debug, true); assert!(value.debug);
assert_eq!(value.level, 5); assert_eq!(value.level, 5);
// Clean up // Clean up
@@ -1,68 +1,65 @@
#[cfg(test)] use crate::config::signup_openrouter::PkceAuthFlow;
mod tests { use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use crate::config::signup_openrouter::PkceAuthFlow; use sha2::{Digest, Sha256};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use sha2::{Digest, Sha256};
#[test] #[test]
fn test_pkce_flow_creation() { fn test_pkce_flow_creation() {
let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow"); let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow");
// Verify code_verifier is 128 characters // Verify code_verifier is 128 characters
assert_eq!(flow.code_verifier.len(), 128); assert_eq!(flow.code_verifier.len(), 128);
// Verify code_challenge is base64url encoded (no padding) // Verify code_challenge is base64url encoded (no padding)
assert!(!flow.code_challenge.contains('=')); assert!(!flow.code_challenge.contains('='));
assert!(!flow.code_challenge.contains('+')); assert!(!flow.code_challenge.contains('+'));
assert!(!flow.code_challenge.contains('/')); assert!(!flow.code_challenge.contains('/'));
// Verify auth URL is properly formatted // Verify auth URL is properly formatted
let auth_url = flow.get_auth_url(); let auth_url = flow.get_auth_url();
assert!(auth_url.starts_with("https://openrouter.ai/auth")); assert!(auth_url.starts_with("https://openrouter.ai/auth"));
assert!(auth_url.contains("callback_url=http%3A%2F%2Flocalhost%3A3000")); assert!(auth_url.contains("callback_url=http%3A%2F%2Flocalhost%3A3000"));
assert!(auth_url.contains(&format!("code_challenge={}", flow.code_challenge))); assert!(auth_url.contains(&format!("code_challenge={}", flow.code_challenge)));
assert!(auth_url.contains("code_challenge_method=S256")); assert!(auth_url.contains("code_challenge_method=S256"));
} }
#[test] #[test]
fn test_different_flows_have_different_verifiers() { fn test_different_flows_have_different_verifiers() {
let flow1 = PkceAuthFlow::new().expect("Failed to create PKCE flow 1"); let flow1 = PkceAuthFlow::new().expect("Failed to create PKCE flow 1");
let flow2 = PkceAuthFlow::new().expect("Failed to create PKCE flow 2"); let flow2 = PkceAuthFlow::new().expect("Failed to create PKCE flow 2");
// Verify that different flows have different verifiers and challenges // Verify that different flows have different verifiers and challenges
assert_ne!(flow1.code_verifier, flow2.code_verifier); assert_ne!(flow1.code_verifier, flow2.code_verifier);
assert_ne!(flow1.code_challenge, flow2.code_challenge); assert_ne!(flow1.code_challenge, flow2.code_challenge);
} }
#[test] #[test]
fn test_code_verifier_is_alphanumeric() { fn test_code_verifier_is_alphanumeric() {
let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow"); let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow");
// Verify all characters in code_verifier are alphanumeric // Verify all characters in code_verifier are alphanumeric
assert!(flow.code_verifier.chars().all(|c| c.is_alphanumeric())); assert!(flow.code_verifier.chars().all(|c| c.is_alphanumeric()));
} }
#[test] #[test]
fn test_code_challenge_matches_verifier() { fn test_code_challenge_matches_verifier() {
let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow"); let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow");
// Manually compute the expected challenge // Manually compute the expected challenge
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(&flow.code_verifier); hasher.update(&flow.code_verifier);
let hash = hasher.finalize(); let hash = hasher.finalize();
let expected_challenge = URL_SAFE_NO_PAD.encode(hash); let expected_challenge = URL_SAFE_NO_PAD.encode(hash);
// Verify the challenge matches // Verify the challenge matches
assert_eq!(flow.code_challenge, expected_challenge); assert_eq!(flow.code_challenge, expected_challenge);
} }
#[test] #[test]
fn test_pkce_verifier_length_bounds() { fn test_pkce_verifier_length_bounds() {
// PKCE spec requires verifier to be 43-128 characters // PKCE spec requires verifier to be 43-128 characters
// Our implementation uses 128 characters // Our implementation uses 128 characters
let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow"); let flow = PkceAuthFlow::new().expect("Failed to create PKCE flow");
assert!(flow.code_verifier.len() >= 43); assert!(flow.code_verifier.len() >= 43);
assert!(flow.code_verifier.len() <= 128); assert!(flow.code_verifier.len() <= 128);
}
} }
+15 -6
View File
@@ -558,11 +558,14 @@ mod tests {
]; ];
// Create session metadata with specific token counts // Create session metadata with specific token counts
#[allow(clippy::field_reassign_with_default)]
let mut session_metadata = SessionMetadata::default(); let mut session_metadata = SessionMetadata::default();
session_metadata.total_tokens = Some(8000); // High token count to trigger compaction {
session_metadata.accumulated_total_tokens = Some(15000); // Even higher accumulated count session_metadata.total_tokens = Some(8000); // High token count to trigger compaction
session_metadata.input_tokens = Some(5000); session_metadata.accumulated_total_tokens = Some(15000); // Even higher accumulated count
session_metadata.output_tokens = Some(3000); session_metadata.input_tokens = Some(5000);
session_metadata.output_tokens = Some(3000);
}
// Test with session metadata - should use total_tokens for compaction (not accumulated) // Test with session metadata - should use total_tokens for compaction (not accumulated)
let result_with_metadata = check_compaction_needed( let result_with_metadata = check_compaction_needed(
@@ -594,7 +597,10 @@ mod tests {
// Test with metadata that has only accumulated tokens (no total_tokens) // Test with metadata that has only accumulated tokens (no total_tokens)
let mut session_metadata_no_total = SessionMetadata::default(); let mut session_metadata_no_total = SessionMetadata::default();
session_metadata_no_total.accumulated_total_tokens = Some(7500); #[allow(clippy::field_reassign_with_default)]
{
session_metadata_no_total.accumulated_total_tokens = Some(7500);
}
let result_with_no_total = check_compaction_needed( let result_with_no_total = check_compaction_needed(
&agent, &agent,
@@ -650,7 +656,10 @@ mod tests {
// Create session metadata with high token count to trigger compaction // Create session metadata with high token count to trigger compaction
let mut session_metadata = SessionMetadata::default(); let mut session_metadata = SessionMetadata::default();
session_metadata.total_tokens = Some(9000); // High enough to trigger compaction #[allow(clippy::field_reassign_with_default)]
{
session_metadata.total_tokens = Some(9000); // High enough to trigger compaction
}
// Test full compaction flow with session metadata // Test full compaction flow with session metadata
let result = check_and_compact_messages( let result = check_and_compact_messages(
+5 -5
View File
@@ -506,7 +506,7 @@ mod tests {
let context_limit = 25; let context_limit = 25;
let result = truncate_messages( let result = truncate_messages(
&messages.messages(), messages.messages(),
&token_counts, &token_counts,
context_limit, context_limit,
&OldestFirstTruncation, &OldestFirstTruncation,
@@ -590,7 +590,7 @@ mod tests {
let context_limit = 100; // Exactly matches total tokens let context_limit = 100; // Exactly matches total tokens
let result = truncate_messages( let result = truncate_messages(
&messages.messages(), messages.messages(),
&token_counts, &token_counts,
context_limit, context_limit,
&OldestFirstTruncation, &OldestFirstTruncation,
@@ -605,7 +605,7 @@ mod tests {
token_counts.push(1); token_counts.push(1);
let result = truncate_messages( let result = truncate_messages(
&messages.messages(), messages.messages(),
&token_counts, &token_counts,
context_limit, context_limit,
&OldestFirstTruncation, &OldestFirstTruncation,
@@ -698,7 +698,7 @@ mod tests {
let (messages, token_counts) = result; let (messages, token_counts) = result;
// Verify the conversation still makes sense // Verify the conversation still makes sense
assert!(messages.len() >= 1); assert!(!messages.is_empty());
assert!(messages.last().unwrap().role == Role::User); assert!(messages.last().unwrap().role == Role::User);
assert!(token_counts.iter().sum::<usize>() <= context_limit); assert!(token_counts.iter().sum::<usize>() <= context_limit);
@@ -710,7 +710,7 @@ mod tests {
// Test impossibly small context window // Test impossibly small context window
let (messages, token_counts) = create_messages_with_counts(1, 10, false); let (messages, token_counts) = create_messages_with_counts(1, 10, false);
let result = truncate_messages( let result = truncate_messages(
&messages.messages(), messages.messages(),
&token_counts, &token_counts,
5, // Impossibly small context 5, // Impossibly small context
&OldestFirstTruncation, &OldestFirstTruncation,
+1 -1
View File
@@ -379,7 +379,7 @@ mod tests {
0, 0,
"Fixed conversation should have no issues, but found: {:?}\n\n{}", "Fixed conversation should have no issues, but found: {:?}\n\n{}",
issues_with_fixed, issues_with_fixed,
debug_conversation_fix(&messages, &fixed.messages(), &issues) debug_conversation_fix(&messages, fixed.messages(), &issues)
); );
(fixed.messages().clone(), issues) (fixed.messages().clone(), issues)
} }
@@ -337,7 +337,7 @@ mod tests {
assert!(tool assert!(tool
.description .description
.as_ref() .as_ref()
.map_or(false, |desc| desc.contains("read-only operation"))); .is_some_and(|desc| desc.contains("read-only operation")));
} }
#[test] #[test]
+1 -66
View File
@@ -30,11 +30,6 @@ use crate::model::ModelConfig;
use anyhow::Result; use anyhow::Result;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
#[cfg(test)]
use super::errors::ProviderError;
#[cfg(test)]
use rmcp::model::Tool;
const DEFAULT_LEAD_TURNS: usize = 3; const DEFAULT_LEAD_TURNS: usize = 3;
const DEFAULT_FAILURE_THRESHOLD: usize = 2; const DEFAULT_FAILURE_THRESHOLD: usize = 2;
const DEFAULT_FALLBACK_TURNS: usize = 2; const DEFAULT_FALLBACK_TURNS: usize = 2;
@@ -174,63 +169,8 @@ fn create_worker_model_config(default_model: &ModelConfig) -> Result<ModelConfig
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::conversation::message::{Message, MessageContent};
use crate::providers::base::{ProviderMetadata, ProviderUsage, Usage};
use chrono::Utc;
use rmcp::model::{AnnotateAble, RawTextContent, Role};
use std::env; use std::env;
#[derive(Clone)]
struct MockTestProvider {
name: String,
model_config: ModelConfig,
}
#[async_trait::async_trait]
impl Provider for MockTestProvider {
fn metadata() -> ProviderMetadata {
ProviderMetadata::new(
"mock_test",
"Mock Test Provider",
"A mock provider for testing",
"mock-model",
vec!["mock-model"],
"",
vec![],
)
}
fn get_model_config(&self) -> ModelConfig {
self.model_config.clone()
}
async fn complete_with_model(
&self,
_model_config: &ModelConfig,
_system: &str,
_messages: &[Message],
_tools: &[Tool],
) -> Result<(Message, ProviderUsage), ProviderError> {
Ok((
Message::new(
Role::Assistant,
Utc::now().timestamp(),
vec![MessageContent::Text(
RawTextContent {
text: format!(
"Response from {} with model {}",
self.name, self.model_config.model_name
),
meta: None,
}
.no_annotation(),
)],
),
ProviderUsage::new(self.model_config.model_name.clone(), Usage::default()),
))
}
}
struct EnvVarGuard { struct EnvVarGuard {
vars: Vec<(String, Option<String>)>, vars: Vec<(String, Option<String>)>,
} }
@@ -355,17 +295,12 @@ mod tests {
let default_model = let default_model =
ModelConfig::new_or_fail("gpt-3.5-turbo").with_context_limit(Some(16_000)); ModelConfig::new_or_fail("gpt-3.5-turbo").with_context_limit(Some(16_000));
let result = create_lead_worker_from_env("openai", &default_model, "gpt-4o"); let _result = create_lead_worker_from_env("openai", &default_model, "gpt-4o");
_guard.set("GOOSE_WORKER_CONTEXT_LIMIT", "32000"); _guard.set("GOOSE_WORKER_CONTEXT_LIMIT", "32000");
let _result = create_lead_worker_from_env("openai", &default_model, "gpt-4o"); let _result = create_lead_worker_from_env("openai", &default_model, "gpt-4o");
_guard.set("GOOSE_CONTEXT_LIMIT", "64000"); _guard.set("GOOSE_CONTEXT_LIMIT", "64000");
let _result = create_lead_worker_from_env("openai", &default_model, "gpt-4o"); let _result = create_lead_worker_from_env("openai", &default_model, "gpt-4o");
match result {
Ok(_) => {}
Err(_) => {}
}
} }
} }
+4 -4
View File
@@ -545,7 +545,7 @@ mod tests {
0x0D, 0x0A, 0x1A, 0x0A, // PNG header 0x0D, 0x0A, 0x1A, 0x0A, // PNG header
0x00, 0x00, 0x00, 0x0D, // Rest of fake PNG data 0x00, 0x00, 0x00, 0x0D, // Rest of fake PNG data
]; ];
std::fs::write(&png_path, &png_data).unwrap(); std::fs::write(&png_path, png_data).unwrap();
let png_path_str = png_path.to_str().unwrap(); let png_path_str = png_path.to_str().unwrap();
// Create a fake PNG (wrong magic numbers) // Create a fake PNG (wrong magic numbers)
@@ -583,7 +583,7 @@ mod tests {
0x0D, 0x0A, 0x1A, 0x0A, // PNG header 0x0D, 0x0A, 0x1A, 0x0A, // PNG header
0x00, 0x00, 0x00, 0x0D, // Rest of fake PNG data 0x00, 0x00, 0x00, 0x0D, // Rest of fake PNG data
]; ];
std::fs::write(&png_path, &png_data).unwrap(); std::fs::write(&png_path, png_data).unwrap();
let png_path_str = png_path.to_str().unwrap(); let png_path_str = png_path.to_str().unwrap();
// Create a fake PNG (wrong magic numbers) // Create a fake PNG (wrong magic numbers)
@@ -613,7 +613,7 @@ mod tests {
let gif_path = temp_dir.path().join("test.gif"); let gif_path = temp_dir.path().join("test.gif");
// Minimal GIF89a header // Minimal GIF89a header
let gif_data = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; let gif_data = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
std::fs::write(&gif_path, &gif_data).unwrap(); std::fs::write(&gif_path, gif_data).unwrap();
let gif_path_str = gif_path.to_str().unwrap(); let gif_path_str = gif_path.to_str().unwrap();
// Test loading unsupported GIF format // Test loading unsupported GIF format
@@ -965,7 +965,7 @@ mod tests {
// Make request to mock server // Make request to mock server
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let response = client let response = client
.get(&format!("{}/test", &mock_server.uri())) .get(format!("{}/test", &mock_server.uri()))
.send() .send()
.await .await
.unwrap(); .unwrap();
+337 -343
View File
@@ -1,97 +1,96 @@
#[cfg(test)] use crate::recipe::build_recipe::{
mod tests { build_recipe_from_template, resolve_sub_recipe_path, RecipeError,
use crate::recipe::build_recipe::{ };
build_recipe_from_template, resolve_sub_recipe_path, RecipeError, use crate::recipe::read_recipe_file_content::RecipeFile;
}; use crate::recipe::{RecipeParameterInputType, RecipeParameterRequirement};
use crate::recipe::read_recipe_file_content::RecipeFile; use tempfile::TempDir;
use crate::recipe::{RecipeParameterInputType, RecipeParameterRequirement};
use tempfile::TempDir;
const NO_USER_PROMPT: Option<fn(&str, &str) -> Result<String, anyhow::Error>> = None; #[allow(clippy::type_complexity)]
const NO_USER_PROMPT: Option<fn(&str, &str) -> Result<String, anyhow::Error>> = None;
fn setup_recipe_file(instructions_and_parameters: &str) -> (TempDir, RecipeFile) { fn setup_recipe_file(instructions_and_parameters: &str) -> (TempDir, RecipeFile) {
let recipe_content = format!( let recipe_content = format!(
r#"{{ r#"{{
"version": "1.0.0", "version": "1.0.0",
"title": "Test Recipe", "title": "Test Recipe",
"description": "A test recipe", "description": "A test recipe",
{} {}
}}"#, }}"#,
instructions_and_parameters instructions_and_parameters
); );
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let recipe_path = temp_dir.path().join("test_recipe.json"); let recipe_path = temp_dir.path().join("test_recipe.json");
std::fs::write(&recipe_path, recipe_content).unwrap(); std::fs::write(&recipe_path, recipe_content).unwrap();
let recipe_file = RecipeFile { let recipe_file = RecipeFile {
content: std::fs::read_to_string(&recipe_path).unwrap(), content: std::fs::read_to_string(&recipe_path).unwrap(),
parent_dir: temp_dir.path().to_path_buf(), parent_dir: temp_dir.path().to_path_buf(),
file_path: recipe_path, file_path: recipe_path,
}; };
(temp_dir, recipe_file) (temp_dir, recipe_file)
} }
fn setup_test_file(temp_dir: &TempDir, filename: &str, content: &str) -> std::path::PathBuf { fn setup_test_file(temp_dir: &TempDir, filename: &str, content: &str) -> std::path::PathBuf {
let file_path = temp_dir.path().join(filename); let file_path = temp_dir.path().join(filename);
std::fs::write(&file_path, content).unwrap(); std::fs::write(&file_path, content).unwrap();
file_path file_path
} }
fn setup_yaml_recipe_file(instructions_and_parameters: &str) -> (TempDir, RecipeFile) { fn setup_yaml_recipe_file(instructions_and_parameters: &str) -> (TempDir, RecipeFile) {
let recipe_content = format!( let recipe_content = format!(
r#"version: "1.0.0" r#"version: "1.0.0"
title: "Test Recipe" title: "Test Recipe"
description: "A test recipe" description: "A test recipe"
{}"#, {}"#,
instructions_and_parameters instructions_and_parameters
); );
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let recipe_path = temp_dir.path().join("test_recipe.yaml"); let recipe_path = temp_dir.path().join("test_recipe.yaml");
std::fs::write(&recipe_path, recipe_content).unwrap(); std::fs::write(&recipe_path, recipe_content).unwrap();
let recipe_file = RecipeFile { let recipe_file = RecipeFile {
content: std::fs::read_to_string(&recipe_path).unwrap(), content: std::fs::read_to_string(&recipe_path).unwrap(),
parent_dir: temp_dir.path().to_path_buf(), parent_dir: temp_dir.path().to_path_buf(),
file_path: recipe_path, file_path: recipe_path,
}; };
(temp_dir, recipe_file) (temp_dir, recipe_file)
} }
fn setup_yaml_recipe_files( fn setup_yaml_recipe_files(
parent_content: &str, parent_content: &str,
child_content: &str, child_content: &str,
) -> (TempDir, RecipeFile, RecipeFile) { ) -> (TempDir, RecipeFile, RecipeFile) {
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.path(); let temp_path = temp_dir.path();
let parent_path = temp_path.join("parent.yaml"); let parent_path = temp_path.join("parent.yaml");
std::fs::write(&parent_path, parent_content).unwrap(); std::fs::write(&parent_path, parent_content).unwrap();
let child_path = temp_path.join("child.yaml"); let child_path = temp_path.join("child.yaml");
std::fs::write(&child_path, child_content).unwrap(); std::fs::write(&child_path, child_content).unwrap();
let parent_recipe_file = RecipeFile { let parent_recipe_file = RecipeFile {
content: std::fs::read_to_string(&parent_path).unwrap(), content: std::fs::read_to_string(&parent_path).unwrap(),
parent_dir: temp_path.to_path_buf(), parent_dir: temp_path.to_path_buf(),
file_path: parent_path, file_path: parent_path,
}; };
let child_recipe_file = RecipeFile { let child_recipe_file = RecipeFile {
content: std::fs::read_to_string(&child_path).unwrap(), content: std::fs::read_to_string(&child_path).unwrap(),
parent_dir: temp_path.to_path_buf(), parent_dir: temp_path.to_path_buf(),
file_path: child_path, file_path: child_path,
}; };
(temp_dir, parent_recipe_file, child_recipe_file) (temp_dir, parent_recipe_file, child_recipe_file)
} }
#[test] #[test]
fn test_build_recipe_from_template_success() { fn test_build_recipe_from_template_success() {
let instructions_and_parameters = r#" let instructions_and_parameters = r#"
"instructions": "Test instructions with {{ my_name }}", "instructions": "Test instructions with {{ my_name }}",
"parameters": [ "parameters": [
{ {
@@ -102,28 +101,28 @@ description: "A test recipe"
} }
]"#; ]"#;
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters); let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
let params = vec![("my_name".to_string(), "value".to_string())]; let params = vec![("my_name".to_string(), "value".to_string())];
let recipe = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT).unwrap(); let recipe = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT).unwrap();
assert_eq!(recipe.title, "Test Recipe"); assert_eq!(recipe.title, "Test Recipe");
assert_eq!(recipe.description, "A test recipe"); assert_eq!(recipe.description, "A test recipe");
assert_eq!(recipe.instructions.unwrap(), "Test instructions with value"); assert_eq!(recipe.instructions.unwrap(), "Test instructions with value");
assert_eq!(recipe.parameters.as_ref().unwrap().len(), 1); assert_eq!(recipe.parameters.as_ref().unwrap().len(), 1);
let param = &recipe.parameters.as_ref().unwrap()[0]; let param = &recipe.parameters.as_ref().unwrap()[0];
assert_eq!(param.key, "my_name"); assert_eq!(param.key, "my_name");
assert!(matches!(param.input_type, RecipeParameterInputType::String)); assert!(matches!(param.input_type, RecipeParameterInputType::String));
assert!(matches!( assert!(matches!(
param.requirement, param.requirement,
RecipeParameterRequirement::Required RecipeParameterRequirement::Required
)); ));
assert_eq!(param.description, "A test parameter"); assert_eq!(param.description, "A test parameter");
} }
#[test] #[test]
fn test_build_recipe_from_template_success_variable_in_prompt() { fn test_build_recipe_from_template_success_variable_in_prompt() {
let instructions_and_parameters = r#" let instructions_and_parameters = r#"
"instructions": "Test instructions", "instructions": "Test instructions",
"prompt": "My prompt {{ my_name }}", "prompt": "My prompt {{ my_name }}",
"parameters": [ "parameters": [
@@ -135,28 +134,28 @@ description: "A test recipe"
} }
]"#; ]"#;
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters); let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
let params = vec![("my_name".to_string(), "value".to_string())]; let params = vec![("my_name".to_string(), "value".to_string())];
let recipe = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT).unwrap(); let recipe = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT).unwrap();
assert_eq!(recipe.title, "Test Recipe"); assert_eq!(recipe.title, "Test Recipe");
assert_eq!(recipe.description, "A test recipe"); assert_eq!(recipe.description, "A test recipe");
assert_eq!(recipe.instructions.unwrap(), "Test instructions"); assert_eq!(recipe.instructions.unwrap(), "Test instructions");
assert_eq!(recipe.prompt.unwrap(), "My prompt value"); assert_eq!(recipe.prompt.unwrap(), "My prompt value");
let param = &recipe.parameters.as_ref().unwrap()[0]; let param = &recipe.parameters.as_ref().unwrap()[0];
assert_eq!(param.key, "my_name"); assert_eq!(param.key, "my_name");
assert!(matches!(param.input_type, RecipeParameterInputType::String)); assert!(matches!(param.input_type, RecipeParameterInputType::String));
assert!(matches!( assert!(matches!(
param.requirement, param.requirement,
RecipeParameterRequirement::Required RecipeParameterRequirement::Required
)); ));
assert_eq!(param.description, "A test parameter"); assert_eq!(param.description, "A test parameter");
} }
#[test] #[test]
fn test_build_recipe_from_template_wrong_parameters_in_recipe_file() { fn test_build_recipe_from_template_wrong_parameters_in_recipe_file() {
let instructions_and_parameters = r#" let instructions_and_parameters = r#"
"instructions": "Test instructions with {{ expected_param1 }} {{ expected_param2 }}", "instructions": "Test instructions with {{ expected_param1 }} {{ expected_param2 }}",
"parameters": [ "parameters": [
{ {
@@ -166,29 +165,28 @@ description: "A test recipe"
"description": "A test parameter" "description": "A test parameter"
} }
]"#; ]"#;
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters); let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
let build_recipe_result = let build_recipe_result = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT);
build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT); assert!(build_recipe_result.is_err());
assert!(build_recipe_result.is_err()); let err = build_recipe_result.unwrap_err();
let err = build_recipe_result.unwrap_err(); println!("{}", err);
println!("{}", err.to_string());
match err { match err {
RecipeError::TemplateRendering { source } => { RecipeError::TemplateRendering { source } => {
let err_str = source.to_string(); let err_str = source.to_string();
assert!(err_str.contains("Unnecessary parameter definitions: wrong_param_key.")); assert!(err_str.contains("Unnecessary parameter definitions: wrong_param_key."));
assert!(err_str.contains("Missing definitions for parameters in the recipe file:")); assert!(err_str.contains("Missing definitions for parameters in the recipe file:"));
assert!(err_str.contains("expected_param1")); assert!(err_str.contains("expected_param1"));
assert!(err_str.contains("expected_param2")); assert!(err_str.contains("expected_param2"));
}
_ => panic!("Expected TemplateRendering error"),
} }
_ => panic!("Expected TemplateRendering error"),
} }
}
#[test] #[test]
fn test_build_recipe_from_template_with_default_values_in_recipe_file() { fn test_build_recipe_from_template_with_default_values_in_recipe_file() {
let instructions_and_parameters = r#" let instructions_and_parameters = r#"
"instructions": "Test instructions with {{ param_with_default }} {{ param_without_default }}", "instructions": "Test instructions with {{ param_with_default }} {{ param_without_default }}",
"parameters": [ "parameters": [
{ {
@@ -205,23 +203,22 @@ description: "A test recipe"
"description": "A test parameter" "description": "A test parameter"
} }
]"#; ]"#;
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters); let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
let params = vec![("param_without_default".to_string(), "value1".to_string())]; let params = vec![("param_without_default".to_string(), "value1".to_string())];
let recipe = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT).unwrap(); let recipe = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT).unwrap();
assert_eq!(recipe.title, "Test Recipe"); assert_eq!(recipe.title, "Test Recipe");
assert_eq!(recipe.description, "A test recipe"); assert_eq!(recipe.description, "A test recipe");
assert_eq!( assert_eq!(
recipe.instructions.unwrap(), recipe.instructions.unwrap(),
"Test instructions with my_default_value value1" "Test instructions with my_default_value value1"
); );
} }
#[test] #[test]
fn test_build_recipe_from_template_optional_parameters_with_empty_default_values_in_recipe_file( fn test_build_recipe_from_template_optional_parameters_with_empty_default_values_in_recipe_file() {
) { let instructions_and_parameters = r#"
let instructions_and_parameters = r#"
"instructions": "Test instructions with {{ optional_param }}", "instructions": "Test instructions with {{ optional_param }}",
"parameters": [ "parameters": [
{ {
@@ -232,17 +229,17 @@ description: "A test recipe"
"default": "" "default": ""
} }
]"#; ]"#;
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters); let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
let recipe = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT).unwrap(); let recipe = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT).unwrap();
assert_eq!(recipe.title, "Test Recipe"); assert_eq!(recipe.title, "Test Recipe");
assert_eq!(recipe.description, "A test recipe"); assert_eq!(recipe.description, "A test recipe");
assert_eq!(recipe.instructions.unwrap(), "Test instructions with "); assert_eq!(recipe.instructions.unwrap(), "Test instructions with ");
} }
#[test] #[test]
fn test_build_recipe_from_template_optional_parameters_without_default_values_in_recipe_file() { fn test_build_recipe_from_template_optional_parameters_without_default_values_in_recipe_file() {
let instructions_and_parameters = r#" let instructions_and_parameters = r#"
"instructions": "Test instructions with {{ optional_param }}", "instructions": "Test instructions with {{ optional_param }}",
"parameters": [ "parameters": [
{ {
@@ -252,24 +249,23 @@ description: "A test recipe"
"description": "A test parameter" "description": "A test parameter"
} }
]"#; ]"#;
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters); let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
let build_recipe_result = let build_recipe_result = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT);
build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT); assert!(build_recipe_result.is_err());
assert!(build_recipe_result.is_err()); let err = build_recipe_result.unwrap_err();
let err = build_recipe_result.unwrap_err(); println!("{}", err);
println!("{}", err.to_string()); match err {
match err { RecipeError::TemplateRendering { source } => {
RecipeError::TemplateRendering { source } => { assert!(source.to_string().to_lowercase().contains("missing"));
assert!(source.to_string().to_lowercase().contains("missing"));
}
_ => panic!("Expected TemplateRendering error"),
} }
_ => panic!("Expected TemplateRendering error"),
} }
}
#[test] #[test]
fn test_build_recipe_from_template_wrong_input_type_in_recipe_file() { fn test_build_recipe_from_template_wrong_input_type_in_recipe_file() {
let instructions_and_parameters = r#" let instructions_and_parameters = r#"
"instructions": "Test instructions with {{ param }}", "instructions": "Test instructions with {{ param }}",
"parameters": [ "parameters": [
{ {
@@ -279,37 +275,37 @@ description: "A test recipe"
"description": "A test parameter" "description": "A test parameter"
} }
]"#; ]"#;
let params = vec![("param".to_string(), "value".to_string())]; let params = vec![("param".to_string(), "value".to_string())];
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters); let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
let build_recipe_result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT); let build_recipe_result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
assert!(build_recipe_result.is_err()); assert!(build_recipe_result.is_err());
let err = build_recipe_result.unwrap_err(); let err = build_recipe_result.unwrap_err();
match err { match err {
RecipeError::TemplateRendering { source } => { RecipeError::TemplateRendering { source } => {
let err_msg = source.to_string(); let err_msg = source.to_string();
eprint!("Error: {}", err_msg); eprint!("Error: {}", err_msg);
assert!(err_msg.contains("unknown variant `some_invalid_type`")); assert!(err_msg.contains("unknown variant `some_invalid_type`"));
}
_ => panic!("Expected TemplateRendering error, got: {:?}", err),
} }
_ => panic!("Expected TemplateRendering error, got: {:?}", err),
} }
}
#[test] #[test]
fn test_build_recipe_from_template_success_without_parameters() { fn test_build_recipe_from_template_success_without_parameters() {
let instructions_and_parameters = r#" let instructions_and_parameters = r#"
"instructions": "Test instructions" "instructions": "Test instructions"
"#; "#;
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters); let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
let recipe = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT).unwrap(); let recipe = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT).unwrap();
assert_eq!(recipe.instructions.unwrap(), "Test instructions"); assert_eq!(recipe.instructions.unwrap(), "Test instructions");
assert!(recipe.parameters.is_none()); assert!(recipe.parameters.is_none());
} }
#[test] #[test]
fn test_template_inheritance() { fn test_template_inheritance() {
let parent_content = r#" let parent_content = r#"
version: 1.0.0 version: 1.0.0
title: Parent title: Parent
description: Parent recipe description: Parent recipe
@@ -334,102 +330,102 @@ description: "A test recipe"
description: whether the feature is enabled description: whether the feature is enabled
"#; "#;
let child_content = r#" let child_content = r#"
{% extends "parent.yaml" -%} {% extends "parent.yaml" -%}
{% block prompt -%} {% block prompt -%}
What is the capital of Germany? What is the capital of Germany?
{%- endblock %} {%- endblock %}
"#; "#;
let (_temp_dir, parent_recipe_file, child_recipe_file) = let (_temp_dir, parent_recipe_file, child_recipe_file) =
setup_yaml_recipe_files(parent_content, child_content); setup_yaml_recipe_files(parent_content, child_content);
let params = vec![ let params = vec![
("date".to_string(), "today".to_string()), ("date".to_string(), "today".to_string()),
("is_enabled".to_string(), "true".to_string()), ("is_enabled".to_string(), "true".to_string()),
]; ];
let parent_recipe = let parent_recipe =
build_recipe_from_template(parent_recipe_file, params.clone(), NO_USER_PROMPT).unwrap(); build_recipe_from_template(parent_recipe_file, params.clone(), NO_USER_PROMPT).unwrap();
assert_eq!(parent_recipe.description, "Parent recipe"); assert_eq!(parent_recipe.description, "Parent recipe");
assert_eq!( assert_eq!(
parent_recipe.prompt.unwrap(), parent_recipe.prompt.unwrap(),
"show me the news for day: today\nWhat is the capital of France?\n\n Feature is enabled.\n" "show me the news for day: today\nWhat is the capital of France?\n\n Feature is enabled.\n"
); );
assert_eq!(parent_recipe.parameters.as_ref().unwrap().len(), 2); assert_eq!(parent_recipe.parameters.as_ref().unwrap().len(), 2);
assert_eq!(parent_recipe.parameters.as_ref().unwrap()[0].key, "date"); assert_eq!(parent_recipe.parameters.as_ref().unwrap()[0].key, "date");
assert_eq!( assert_eq!(
parent_recipe.parameters.as_ref().unwrap()[1].key, parent_recipe.parameters.as_ref().unwrap()[1].key,
"is_enabled" "is_enabled"
); );
let child_recipe = let child_recipe =
build_recipe_from_template(child_recipe_file, params, NO_USER_PROMPT).unwrap(); build_recipe_from_template(child_recipe_file, params, NO_USER_PROMPT).unwrap();
assert_eq!(child_recipe.title, "Parent"); assert_eq!(child_recipe.title, "Parent");
assert_eq!(child_recipe.description, "Parent recipe"); assert_eq!(child_recipe.description, "Parent recipe");
assert_eq!( assert_eq!(
child_recipe.prompt.unwrap().trim(), child_recipe.prompt.unwrap().trim(),
"show me the news for day: today\nWhat is the capital of Germany?\n\n Feature is enabled." "show me the news for day: today\nWhat is the capital of Germany?\n\n Feature is enabled."
); );
assert_eq!(child_recipe.parameters.as_ref().unwrap().len(), 2); assert_eq!(child_recipe.parameters.as_ref().unwrap().len(), 2);
assert_eq!(child_recipe.parameters.as_ref().unwrap()[0].key, "date"); assert_eq!(child_recipe.parameters.as_ref().unwrap()[0].key, "date");
assert_eq!( assert_eq!(
child_recipe.parameters.as_ref().unwrap()[1].key, child_recipe.parameters.as_ref().unwrap()[1].key,
"is_enabled" "is_enabled"
); );
}
mod sub_recipe_path_resolution {
use super::*;
fn create_recipe_file(
temp_path: &std::path::Path,
recipe_folder: &str,
recipe_file_name: &str,
content: &str,
) -> std::path::PathBuf {
let recipes_dir = temp_path.join(recipe_folder);
std::fs::create_dir_all(&recipes_dir).unwrap();
let recipe_path = recipes_dir.join(recipe_file_name);
std::fs::write(&recipe_path, content).unwrap();
recipe_path
} }
mod sub_recipe_path_resolution { #[test]
use super::*; fn test_resolve_sub_recipe_path_relative() {
let temp_dir = tempfile::tempdir().unwrap();
let parent_dir = temp_dir.path();
fn create_recipe_file( let result = resolve_sub_recipe_path("./sub-recipes/child.yaml", parent_dir);
temp_path: &std::path::Path, assert!(result.is_ok());
recipe_folder: &str,
recipe_file_name: &str,
content: &str,
) -> std::path::PathBuf {
let recipes_dir = temp_path.join(recipe_folder);
std::fs::create_dir_all(&recipes_dir).unwrap();
let recipe_path = recipes_dir.join(recipe_file_name);
std::fs::write(&recipe_path, content).unwrap();
recipe_path
}
#[test] let expected_path = parent_dir.join("./sub-recipes/child.yaml");
fn test_resolve_sub_recipe_path_relative() { assert_eq!(result.unwrap(), expected_path.to_str().unwrap());
let temp_dir = tempfile::tempdir().unwrap(); }
let parent_dir = temp_dir.path();
let result = resolve_sub_recipe_path("./sub-recipes/child.yaml", parent_dir); #[test]
assert!(result.is_ok()); fn test_resolve_sub_recipe_path_absolute() {
let temp_dir = tempfile::tempdir().unwrap();
let parent_dir = temp_dir.path();
let absolute_path = "/absolute/path/to/recipe.yaml";
let expected_path = parent_dir.join("./sub-recipes/child.yaml"); let result = resolve_sub_recipe_path(absolute_path, parent_dir);
assert_eq!(result.unwrap(), expected_path.to_str().unwrap()); assert!(result.is_ok());
} assert_eq!(result.unwrap(), absolute_path);
}
#[test] #[test]
fn test_resolve_sub_recipe_path_absolute() { fn test_build_recipe_with_relative_sub_recipe_path() {
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let parent_dir = temp_dir.path(); let temp_path = temp_dir.path();
let absolute_path = "/absolute/path/to/recipe.yaml"; let sub_recipe_content = r#"
let result = resolve_sub_recipe_path(absolute_path, parent_dir);
assert!(result.is_ok());
assert_eq!(result.unwrap(), absolute_path);
}
#[test]
fn test_build_recipe_with_relative_sub_recipe_path() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.path();
let sub_recipe_content = r#"
version: 1.0.0 version: 1.0.0
title: Child Recipe title: Child Recipe
description: A child recipe description: A child recipe
instructions: Child instructions instructions: Child instructions
"#; "#;
create_recipe_file(temp_path, "sub-recipes", "child.yaml", sub_recipe_content); create_recipe_file(temp_path, "sub-recipes", "child.yaml", sub_recipe_content);
let main_recipe_content = r#"{ let main_recipe_content = r#"{
"version": "1.0.0", "version": "1.0.0",
"title": "Main Recipe", "title": "Main Recipe",
"description": "Main recipe with sub-recipe", "description": "Main recipe with sub-recipe",
@@ -441,92 +437,91 @@ instructions: Child instructions
} }
] ]
}"#; }"#;
let main_recipe_path = let main_recipe_path =
create_recipe_file(temp_path, "main", "main.json", main_recipe_content); create_recipe_file(temp_path, "main", "main.json", main_recipe_content);
let recipe_file = RecipeFile { let recipe_file = RecipeFile {
content: main_recipe_content.to_string(), content: main_recipe_content.to_string(),
parent_dir: temp_path.to_path_buf(), parent_dir: temp_path.to_path_buf(),
file_path: main_recipe_path, file_path: main_recipe_path,
}; };
let recipe = let recipe = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT).unwrap();
build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT).unwrap();
assert_eq!(recipe.title, "Main Recipe"); assert_eq!(recipe.title, "Main Recipe");
assert!(recipe.sub_recipes.is_some()); assert!(recipe.sub_recipes.is_some());
let sub_recipes = recipe.sub_recipes.unwrap(); let sub_recipes = recipe.sub_recipes.unwrap();
assert_eq!(sub_recipes.len(), 1); assert_eq!(sub_recipes.len(), 1);
assert_eq!(sub_recipes[0].name, "child"); assert_eq!(sub_recipes[0].name, "child");
let expected_absolute_path = temp_path.join("./sub-recipes/child.yaml"); let expected_absolute_path = temp_path.join("./sub-recipes/child.yaml");
assert_eq!( assert_eq!(
sub_recipes[0].path, sub_recipes[0].path,
expected_absolute_path.to_str().unwrap() expected_absolute_path.to_str().unwrap()
); );
}
}
mod file_parameter_tests {
use super::*;
#[test]
fn test_build_recipe_file_parameter_valid_paths() {
let instructions_and_parameters = r#"instructions: "Test file content: {{ FILE_PARAM }}"
parameters:
- key: FILE_PARAM
input_type: file
requirement: required
description: A file parameter"#;
let (temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters);
let test_content = "Hello from file!\nThis is line 2\n Indented line 3";
let test_file_path = setup_test_file(&temp_dir, "test_file.txt", test_content);
let params = vec![(
"FILE_PARAM".to_string(),
test_file_path.to_string_lossy().to_string(),
)];
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
assert!(result.is_ok());
let recipe = result.unwrap();
let instructions = recipe.instructions.as_ref().unwrap();
assert!(instructions.contains("Hello from file!"));
assert!(instructions.contains("Test file content:"));
}
#[test]
fn test_build_recipe_file_parameter_nonexistent_file() {
let instructions_and_parameters = r#"instructions: "Test file content: {{ FILE_PARAM }}"
parameters:
- key: FILE_PARAM
input_type: file
requirement: required
description: A file parameter"#;
let (_temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters);
let params = vec![(
"FILE_PARAM".to_string(),
"/nonexistent/path/file.txt".to_string(),
)];
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
assert!(result.is_err());
if let Err(RecipeError::TemplateRendering { source }) = result {
assert!(source.to_string().contains("Failed to read parameter file"));
} else {
panic!("Expected TemplateRendering error");
} }
} }
mod file_parameter_tests { #[test]
use super::*; fn test_build_recipe_file_parameter_with_default_rejected() {
let instructions_and_parameters = r#"instructions: "Test file content: {{ FILE_PARAM }}"
#[test]
fn test_build_recipe_file_parameter_valid_paths() {
let instructions_and_parameters = r#"instructions: "Test file content: {{ FILE_PARAM }}"
parameters:
- key: FILE_PARAM
input_type: file
requirement: required
description: A file parameter"#;
let (temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters);
let test_content = "Hello from file!\nThis is line 2\n Indented line 3";
let test_file_path = setup_test_file(&temp_dir, "test_file.txt", test_content);
let params = vec![(
"FILE_PARAM".to_string(),
test_file_path.to_string_lossy().to_string(),
)];
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
assert!(result.is_ok());
let recipe = result.unwrap();
let instructions = recipe.instructions.as_ref().unwrap();
assert!(instructions.contains("Hello from file!"));
assert!(instructions.contains("Test file content:"));
}
#[test]
fn test_build_recipe_file_parameter_nonexistent_file() {
let instructions_and_parameters = r#"instructions: "Test file content: {{ FILE_PARAM }}"
parameters:
- key: FILE_PARAM
input_type: file
requirement: required
description: A file parameter"#;
let (_temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters);
let params = vec![(
"FILE_PARAM".to_string(),
"/nonexistent/path/file.txt".to_string(),
)];
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
assert!(result.is_err());
if let Err(RecipeError::TemplateRendering { source }) = result {
assert!(source.to_string().contains("Failed to read parameter file"));
} else {
panic!("Expected TemplateRendering error");
}
}
#[test]
fn test_build_recipe_file_parameter_with_default_rejected() {
let instructions_and_parameters = r#"instructions: "Test file content: {{ FILE_PARAM }}"
parameters: parameters:
- key: FILE_PARAM - key: FILE_PARAM
input_type: file input_type: file
@@ -534,19 +529,18 @@ parameters:
description: A file parameter description: A file parameter
default: "/etc/passwd""#; default: "/etc/passwd""#;
let (_temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters); let (_temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters);
let params = vec![]; let params = vec![];
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT); let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
assert!(result.is_err()); assert!(result.is_err());
if let Err(RecipeError::TemplateRendering { source }) = result { if let Err(RecipeError::TemplateRendering { source }) = result {
assert!(source assert!(source
.to_string() .to_string()
.contains("File parameters cannot have default values")); .contains("File parameters cannot have default values"));
} else { } else {
panic!("Expected TemplateRendering error for file parameter with default"); panic!("Expected TemplateRendering error for file parameter with default");
}
} }
} }
} }
+54 -54
View File
@@ -1348,6 +1348,60 @@ async fn run_scheduled_job_internal(
Ok(session_id_for_return) Ok(session_id_for_return)
} }
#[async_trait]
impl SchedulerTrait for Scheduler {
async fn add_scheduled_job(&self, job: ScheduledJob) -> Result<(), SchedulerError> {
self.add_scheduled_job(job).await
}
async fn list_scheduled_jobs(&self) -> Result<Vec<ScheduledJob>, SchedulerError> {
Ok(self.list_scheduled_jobs().await)
}
async fn remove_scheduled_job(&self, id: &str) -> Result<(), SchedulerError> {
self.remove_scheduled_job(id).await
}
async fn pause_schedule(&self, id: &str) -> Result<(), SchedulerError> {
self.pause_schedule(id).await
}
async fn unpause_schedule(&self, id: &str) -> Result<(), SchedulerError> {
self.unpause_schedule(id).await
}
async fn run_now(&self, id: &str) -> Result<String, SchedulerError> {
self.run_now(id).await
}
async fn sessions(
&self,
sched_id: &str,
limit: usize,
) -> Result<Vec<(String, SessionMetadata)>, SchedulerError> {
self.sessions(sched_id, limit).await
}
async fn update_schedule(
&self,
sched_id: &str,
new_cron: String,
) -> Result<(), SchedulerError> {
self.update_schedule(sched_id, new_cron).await
}
async fn kill_running_job(&self, sched_id: &str) -> Result<(), SchedulerError> {
self.kill_running_job(sched_id).await
}
async fn get_running_job_info(
&self,
sched_id: &str,
) -> Result<Option<(String, DateTime<Utc>)>, SchedulerError> {
self.get_running_job_info(sched_id).await
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -1527,57 +1581,3 @@ mod tests {
Ok(()) Ok(())
} }
} }
#[async_trait]
impl SchedulerTrait for Scheduler {
async fn add_scheduled_job(&self, job: ScheduledJob) -> Result<(), SchedulerError> {
self.add_scheduled_job(job).await
}
async fn list_scheduled_jobs(&self) -> Result<Vec<ScheduledJob>, SchedulerError> {
Ok(self.list_scheduled_jobs().await)
}
async fn remove_scheduled_job(&self, id: &str) -> Result<(), SchedulerError> {
self.remove_scheduled_job(id).await
}
async fn pause_schedule(&self, id: &str) -> Result<(), SchedulerError> {
self.pause_schedule(id).await
}
async fn unpause_schedule(&self, id: &str) -> Result<(), SchedulerError> {
self.unpause_schedule(id).await
}
async fn run_now(&self, id: &str) -> Result<String, SchedulerError> {
self.run_now(id).await
}
async fn sessions(
&self,
sched_id: &str,
limit: usize,
) -> Result<Vec<(String, SessionMetadata)>, SchedulerError> {
self.sessions(sched_id, limit).await
}
async fn update_schedule(
&self,
sched_id: &str,
new_cron: String,
) -> Result<(), SchedulerError> {
self.update_schedule(sched_id, new_cron).await
}
async fn kill_running_job(&self, sched_id: &str) -> Result<(), SchedulerError> {
self.kill_running_job(sched_id).await
}
async fn get_running_job_info(
&self,
sched_id: &str,
) -> Result<Option<(String, DateTime<Utc>)>, SchedulerError> {
self.get_running_job_info(sched_id).await
}
}
@@ -130,7 +130,7 @@ mod tests {
if inspector.is_enabled() { if inspector.is_enabled() {
// If security is enabled, should detect the dangerous command // If security is enabled, should detect the dangerous command
assert!( assert!(
results.len() >= 1, !results.is_empty(),
"Security inspector should detect dangerous command when enabled" "Security inspector should detect dangerous command when enabled"
); );
if !results.is_empty() { if !results.is_empty() {
-38
View File
@@ -97,41 +97,3 @@ pub fn get_valid_sorted_sessions(sort_order: SortOrder) -> Result<Vec<SessionInf
Ok(session_infos) Ok(session_infos)
} }
#[cfg(test)]
mod tests {
use crate::session::SessionMetadata;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_get_valid_sorted_sessions_with_corrupted_files() {
let temp_dir = tempdir().unwrap();
let session_dir = temp_dir.path().join("sessions");
fs::create_dir_all(&session_dir).unwrap();
// Create a valid session file
let valid_session = session_dir.join("valid_session.jsonl");
let metadata = SessionMetadata::default();
let metadata_json = serde_json::to_string(&metadata).unwrap();
fs::write(&valid_session, format!("{}\n", metadata_json)).unwrap();
// Create a corrupted session file (invalid JSON)
let corrupted_session = session_dir.join("corrupted_session.jsonl");
fs::write(&corrupted_session, "invalid json content").unwrap();
// Create another valid session file
let valid_session2 = session_dir.join("valid_session2.jsonl");
fs::write(&valid_session2, format!("{}\n", metadata_json)).unwrap();
// Mock the session directory by temporarily setting it
// Note: This is a simplified test - in practice, we'd need to mock the session::list_sessions function
// For now, we'll just verify that the function handles errors gracefully
// The key improvement is that get_valid_sorted_sessions should not fail completely
// when encountering corrupted sessions, but should skip them and continue with valid ones
// This test verifies the logic changes we made to handle corrupted sessions gracefully
assert!(true, "Test passes - the function now handles corrupted sessions gracefully by skipping them instead of failing completely");
}
}
+6 -3
View File
@@ -1364,7 +1364,7 @@ mod tests {
#[test] #[test]
fn test_corruption_recovery() -> Result<()> { fn test_corruption_recovery() -> Result<()> {
let test_cases = vec![ let test_cases = [
// Case 1: Unclosed quotes // Case 1: Unclosed quotes
( (
r#"{"role":"user","content":[{"type":"text","text":"Hello there}]"#, r#"{"role":"user","content":[{"type":"text","text":"Hello there}]"#,
@@ -1665,7 +1665,10 @@ mod tests {
let file_path = dir.path().join("metadata.jsonl"); let file_path = dir.path().join("metadata.jsonl");
let mut metadata = SessionMetadata::default(); let mut metadata = SessionMetadata::default();
metadata.description = "Description with\nnewline and \"quotes\" and 🦆".to_string(); #[allow(clippy::field_reassign_with_default)]
{
metadata.description = "Description with\nnewline and \"quotes\" and 🦆".to_string();
}
let messages = Conversation::new_unvalidated(vec![Message::user().with_text("test")]); let messages = Conversation::new_unvalidated(vec![Message::user().with_text("test")]);
@@ -1702,7 +1705,7 @@ mod tests {
let mut lines: Vec<String> = contents.lines().map(String::from).collect(); let mut lines: Vec<String> = contents.lines().map(String::from).collect();
lines[0] = lines[0].replace( lines[0] = lines[0].replace(
&get_home_dir().to_string_lossy().into_owned(), &get_home_dir().to_string_lossy().into_owned(),
&invalid_dir.to_string_lossy().into_owned(), &invalid_dir.to_string_lossy(),
); );
fs::write(&file_path, lines.join("\n"))?; fs::write(&file_path, lines.join("\n"))?;
-24
View File
@@ -274,30 +274,6 @@ mod tests {
use mcp_core::ToolCall; use mcp_core::ToolCall;
use serde_json::json; use serde_json::json;
struct MockInspector {
name: &'static str,
results: Vec<InspectionResult>,
}
#[async_trait]
impl ToolInspector for MockInspector {
fn name(&self) -> &'static str {
self.name
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
async fn inspect(
&self,
_tool_requests: &[ToolRequest],
_messages: &[Message],
) -> Result<Vec<InspectionResult>> {
Ok(self.results.clone())
}
}
#[test] #[test]
fn test_apply_inspection_results() { fn test_apply_inspection_results() {
let tool_request = ToolRequest { let tool_request = ToolRequest {
@@ -344,9 +344,10 @@ mod tests {
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::dispatcher; use tracing::dispatcher;
type Events = Arc<Mutex<Vec<(String, Value)>>>;
struct TestFixture { struct TestFixture {
original_subscriber: Option<dispatcher::Dispatch>, original_subscriber: Option<dispatcher::Dispatch>,
events: Option<Arc<Mutex<Vec<(String, Value)>>>>, events: Option<Events>,
} }
impl TestFixture { impl TestFixture {
+3 -4
View File
@@ -20,6 +20,7 @@ use goose::providers::{
enum ProviderType { enum ProviderType {
Azure, Azure,
OpenAi, OpenAi,
#[allow(dead_code)]
Anthropic, Anthropic,
Bedrock, Bedrock,
Databricks, Databricks,
@@ -540,9 +541,7 @@ mod schedule_tool_tests {
mod final_output_tool_tests { mod final_output_tool_tests {
use super::*; use super::*;
use futures::stream; use futures::stream;
use goose::agents::final_output_tool::{ use goose::agents::final_output_tool::FINAL_OUTPUT_TOOL_NAME;
FINAL_OUTPUT_CONTINUATION_MESSAGE, FINAL_OUTPUT_TOOL_NAME,
};
use goose::conversation::Conversation; use goose::conversation::Conversation;
use goose::providers::base::MessageStream; use goose::providers::base::MessageStream;
use goose::recipe::Response; use goose::recipe::Response;
@@ -1151,7 +1150,7 @@ mod max_turns_tests {
} }
assert!( assert!(
responses.len() >= 1, !responses.is_empty(),
"Expected at least 1 response, got {}", "Expected at least 1 response, got {}",
responses.len() responses.len()
); );
@@ -431,7 +431,7 @@ mod tests {
assert!(recipe.extensions.is_some()); assert!(recipe.extensions.is_some());
let extensions = recipe.extensions.unwrap(); let extensions = recipe.extensions.unwrap();
// At minimum we should get the full config (stdio), shortname may not resolve // At minimum we should get the full config (stdio), shortname may not resolve
assert!(extensions.len() >= 1 && extensions.len() <= 2); assert!(!extensions.is_empty() && extensions.len() <= 2);
// The last one should always be the stdio config we provided // The last one should always be the stdio config we provided
if let Some(last) = extensions.last() { if let Some(last) = extensions.last() {
match last { match last {
+1 -3
View File
@@ -57,9 +57,7 @@ mod execution_tests {
async fn test_session_limit() { async fn test_session_limit() {
let manager = AgentManager::new(Some(3)).await.unwrap(); let manager = AgentManager::new(Some(3)).await.unwrap();
let sessions: Vec<_> = (0..3) let sessions: Vec<_> = (0..3).map(|i| format!("session-{}", i)).collect();
.map(|i| String::from(format!("session-{}", i)))
.collect();
for session in &sessions { for session in &sessions {
manager manager
@@ -156,6 +156,7 @@ async fn test_replayed_session(
let extension_manager = ExtensionManager::new(); let extension_manager = ExtensionManager::new();
#[allow(clippy::redundant_closure_call)]
let result = (async || -> Result<(), Box<dyn std::error::Error>> { let result = (async || -> Result<(), Box<dyn std::error::Error>> {
extension_manager.add_extension(extension_config).await?; extension_manager.add_extension(extension_config).await?;
+13 -30
View File
@@ -29,10 +29,17 @@ pub struct ConfigurableMockScheduler {
running_jobs: Arc<Mutex<HashSet<String>>>, running_jobs: Arc<Mutex<HashSet<String>>>,
call_log: Arc<Mutex<Vec<String>>>, call_log: Arc<Mutex<Vec<String>>>,
behaviors: Arc<Mutex<HashMap<String, MockBehavior>>>, behaviors: Arc<Mutex<HashMap<String, MockBehavior>>>,
#[allow(clippy::type_complexity)]
sessions_data: Arc<Mutex<HashMap<String, Vec<(String, SessionMetadata)>>>>, sessions_data: Arc<Mutex<HashMap<String, Vec<(String, SessionMetadata)>>>>,
} }
#[allow(dead_code)] #[allow(dead_code)]
impl Default for ConfigurableMockScheduler {
fn default() -> Self {
Self::new()
}
}
impl ConfigurableMockScheduler { impl ConfigurableMockScheduler {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -44,36 +51,6 @@ impl ConfigurableMockScheduler {
} }
} }
pub async fn with_behavior(self, method: &str, behavior: MockBehavior) -> Self {
self.behaviors
.lock()
.await
.insert(method.to_string(), behavior);
self
}
pub async fn with_existing_job(self, job: ScheduledJob) -> Self {
self.jobs.lock().await.insert(job.id.clone(), job);
self
}
pub async fn with_running_job(self, job_id: &str) -> Self {
self.running_jobs.lock().await.insert(job_id.to_string());
self
}
pub async fn with_sessions_data(
self,
job_id: &str,
sessions: Vec<(String, SessionMetadata)>,
) -> Self {
self.sessions_data
.lock()
.await
.insert(job_id.to_string(), sessions);
self
}
pub async fn get_calls(&self) -> Vec<String> { pub async fn get_calls(&self) -> Vec<String> {
self.call_log.lock().await.clone() self.call_log.lock().await.clone()
} }
@@ -337,6 +314,12 @@ pub struct ScheduleToolTestBuilder {
scheduler: Arc<ConfigurableMockScheduler>, scheduler: Arc<ConfigurableMockScheduler>,
} }
impl Default for ScheduleToolTestBuilder {
fn default() -> Self {
Self::new()
}
}
impl ScheduleToolTestBuilder { impl ScheduleToolTestBuilder {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -11,7 +11,6 @@ use goose::session::storage::SessionMetadata;
use rmcp::model::Tool; use rmcp::model::Tool;
use std::sync::Arc; use std::sync::Arc;
use tempfile::TempDir; use tempfile::TempDir;
use tokio;
use uuid::Uuid; use uuid::Uuid;
// Mock provider implementation for testing // Mock provider implementation for testing
@@ -298,7 +297,7 @@ async fn test_todo_clear_removes_from_session() {
.unwrap(); .unwrap();
// Consume the stream // Consume the stream
while let Some(_) = stream.next().await {} while (stream.next().await).is_some() {}
// With mock provider, the TODO won't actually be cleared via tool calls // With mock provider, the TODO won't actually be cleared via tool calls
// but we can verify the structure is correct // but we can verify the structure is correct
@@ -463,9 +462,12 @@ async fn test_todo_update_preserves_other_metadata() {
// Set initial metadata with various fields // Set initial metadata with various fields
let mut metadata = SessionMetadata::default(); let mut metadata = SessionMetadata::default();
metadata.message_count = 5; #[allow(clippy::field_reassign_with_default)]
metadata.description = "Test session".to_string(); {
metadata.total_tokens = Some(1000); metadata.message_count = 5;
metadata.description = "Test session".to_string();
metadata.total_tokens = Some(1000);
}
let todo_state = TodoState::new("Initial TODO".to_string()); let todo_state = TodoState::new("Initial TODO".to_string());
todo_state todo_state
.to_extension_data(&mut metadata.extension_data) .to_extension_data(&mut metadata.extension_data)
+2 -2
View File
@@ -14,11 +14,11 @@ echo "🔍 Running all clippy checks..."
# Run standard clippy with strict warnings # Run standard clippy with strict warnings
echo " → Standard clippy rules (strict)" echo " → Standard clippy rules (strict)"
cargo clippy --jobs 2 -- -D warnings cargo clippy --all-targets --jobs 2 -- -D warnings
# Run baseline rules check # Run baseline rules check
echo "" echo ""
check_all_baseline_rules check_all_baseline_rules
echo "" echo ""
echo "✅ All lint checks passed!" echo "✅ All lint checks passed!"
@@ -13,7 +13,16 @@ function getStdioConfig(
timeout: number timeout: number
) { ) {
// Validate that the command is one of the allowed commands // Validate that the command is one of the allowed commands
const allowedCommands = ['cu', 'docker', 'jbang', 'npx', 'uvx', 'goosed', 'npx.cmd', 'i-ching-mcp-server']; const allowedCommands = [
'cu',
'docker',
'jbang',
'npx',
'uvx',
'goosed',
'npx.cmd',
'i-ching-mcp-server',
];
if (!allowedCommands.includes(cmd)) { if (!allowedCommands.includes(cmd)) {
toastService.handleError( toastService.handleError(
'Invalid Command', 'Invalid Command',