mirror of
https://github.com/aljazceru/goose.git
synced 2026-02-18 13:04:38 +01:00
Adds json schema validation to goose recipe validate cli (#3234)
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3544,6 +3544,7 @@ dependencies = [
|
||||
"goose-mcp",
|
||||
"http 1.2.0",
|
||||
"indicatif",
|
||||
"jsonschema",
|
||||
"mcp-client",
|
||||
"mcp-core",
|
||||
"mcp-server",
|
||||
|
||||
@@ -27,6 +27,7 @@ console = "0.15.8"
|
||||
bat = "0.24.0"
|
||||
anyhow = "1.0"
|
||||
serde_json = "1.0"
|
||||
jsonschema = "0.30.0"
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
futures = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] } # For serialization
|
||||
|
||||
@@ -74,10 +74,22 @@ mod tests {
|
||||
}
|
||||
|
||||
const VALID_RECIPE_CONTENT: &str = r#"
|
||||
title: "Test Recipe"
|
||||
description: "A test recipe for deeplink generation"
|
||||
title: "Test Recipe with Valid JSON Schema"
|
||||
description: "A test recipe with valid JSON schema"
|
||||
prompt: "Test prompt content"
|
||||
instructions: "Test instructions"
|
||||
response:
|
||||
json_schema:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
type: string
|
||||
description: "The result"
|
||||
count:
|
||||
type: number
|
||||
description: "A count value"
|
||||
required:
|
||||
- result
|
||||
"#;
|
||||
|
||||
const INVALID_RECIPE_CONTENT: &str = r#"
|
||||
@@ -87,6 +99,20 @@ prompt: "Test prompt content {{ name }}"
|
||||
instructions: "Test instructions"
|
||||
"#;
|
||||
|
||||
const RECIPE_WITH_INVALID_JSON_SCHEMA: &str = r#"
|
||||
title: "Test Recipe with Invalid JSON Schema"
|
||||
description: "A test recipe with invalid JSON schema"
|
||||
prompt: "Test prompt content"
|
||||
instructions: "Test instructions"
|
||||
response:
|
||||
json_schema:
|
||||
type: invalid_type
|
||||
properties:
|
||||
result:
|
||||
type: unknown_type
|
||||
required: "should_be_array_not_string"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn test_handle_deeplink_valid_recipe() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
@@ -95,7 +121,7 @@ instructions: "Test instructions"
|
||||
|
||||
let result = handle_deeplink(&recipe_path);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().contains("goose://recipe?config=eyJ2ZXJzaW9uIjoiMS4wLjAiLCJ0aXRsZSI6IlRlc3QgUmVjaXBlIiwiZGVzY3JpcHRpb24iOiJBIHRlc3QgcmVjaXBlIGZvciBkZWVwbGluayBnZW5lcmF0aW9uIiwiaW5zdHJ1Y3Rpb25zIjoiVGVzdCBpbnN0cnVjdGlvbnMiLCJwcm9tcHQiOiJUZXN0IHByb21wdCBjb250ZW50In0%3D"));
|
||||
assert!(result.unwrap().contains("goose://recipe?config=eyJ2ZXJzaW9uIjoiMS4wLjAiLCJ0aXRsZSI6IlRlc3QgUmVjaXBlIHdpdGggVmFsaWQgSlNPTiBTY2hlbWEiLCJkZXNjcmlwdGlvbiI6IkEgdGVzdCByZWNpcGUgd2l0aCB2YWxpZCBKU09OIHNjaGVtYSIsImluc3RydWN0aW9ucyI6IlRlc3QgaW5zdHJ1Y3Rpb25zIiwicHJvbXB0IjoiVGVzdCBwcm9tcHQgY29udGVudCIsInJlc3BvbnNlIjp7Impzb25fc2NoZW1hIjp7InByb3BlcnRpZXMiOnsiY291bnQiOnsiZGVzY3JpcHRpb24iOiJBIGNvdW50IHZhbHVlIiwidHlwZSI6Im51bWJlciJ9LCJyZXN1bHQiOnsiZGVzY3JpcHRpb24iOiJUaGUgcmVzdWx0IiwidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsicmVzdWx0Il0sInR5cGUiOiJvYmplY3QifX19"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -125,4 +151,21 @@ instructions: "Test instructions"
|
||||
let result = handle_validate(&recipe_path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_validation_recipe_with_invalid_json_schema() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let recipe_path = create_test_recipe_file(
|
||||
&temp_dir,
|
||||
"test_recipe.yaml",
|
||||
RECIPE_WITH_INVALID_JSON_SCHEMA,
|
||||
);
|
||||
|
||||
let result = handle_validate(&recipe_path);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("JSON schema validation failed"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,13 +169,21 @@ mod tests {
|
||||
assert!(sub_recipes[0].values.is_none());
|
||||
assert_eq!(
|
||||
sub_recipes[1].path,
|
||||
sub_recipe1_path.to_string_lossy().to_string()
|
||||
sub_recipe1_path
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
);
|
||||
assert_eq!(sub_recipes[1].name, "sub_recipe1".to_string());
|
||||
assert!(sub_recipes[1].values.is_none());
|
||||
assert_eq!(
|
||||
sub_recipes[2].path,
|
||||
sub_recipe2_path.to_string_lossy().to_string()
|
||||
sub_recipe2_path
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
);
|
||||
assert_eq!(sub_recipes[2].name, "sub_recipe2".to_string());
|
||||
assert!(sub_recipes[2].values.is_none());
|
||||
@@ -221,6 +229,7 @@ response:
|
||||
let recipe_path: std::path::PathBuf = temp_dir.path().join("test_recipe.yaml");
|
||||
|
||||
std::fs::write(&recipe_path, test_recipe_content).unwrap();
|
||||
(temp_dir, recipe_path)
|
||||
let canonical_recipe_path = recipe_path.canonicalize().unwrap();
|
||||
(temp_dir, canonical_recipe_path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,13 @@ pub fn load_recipe(recipe_name: &str) -> Result<Recipe> {
|
||||
recipe_dir_str.to_string(),
|
||||
&HashMap::new(),
|
||||
)?;
|
||||
|
||||
if let Some(response) = &recipe.response {
|
||||
if let Some(json_schema) = &response.json_schema {
|
||||
validate_json_schema(json_schema)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(recipe)
|
||||
}
|
||||
|
||||
@@ -222,5 +229,12 @@ fn apply_values_to_parameters(
|
||||
Ok((param_map, missing_params))
|
||||
}
|
||||
|
||||
fn validate_json_schema(schema: &serde_json::Value) -> Result<()> {
|
||||
match jsonschema::validator_for(schema) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(anyhow::anyhow!("JSON schema validation failed: {}", err)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user