Adds json schema validation to goose recipe validate cli (#3234)

This commit is contained in:
Jarrod Sibbison
2025-07-03 18:54:16 +10:00
committed by GitHub
parent 2c86a0eb6e
commit ee714d7e73
5 changed files with 74 additions and 6 deletions

1
Cargo.lock generated
View File

@@ -3544,6 +3544,7 @@ dependencies = [
"goose-mcp",
"http 1.2.0",
"indicatif",
"jsonschema",
"mcp-client",
"mcp-core",
"mcp-server",

View File

@@ -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

View File

@@ -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"));
}
}

View File

@@ -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)
}
}

View File

@@ -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;