Douwe/fix include for recipes (#2914)
Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Generated
+14
@@ -5469,6 +5469,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memo-map"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
@@ -5491,6 +5497,8 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e36f1329330bb1614c94b78632b9ce45dd7d761f3304a1bed07b2990a7c5097"
|
checksum = "6e36f1329330bb1614c94b78632b9ce45dd7d761f3304a1bed07b2990a7c5097"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"memo-map",
|
||||||
|
"self_cell",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7462,6 +7470,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "self_cell"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ shlex = "1.3.0"
|
|||||||
async-trait = "0.1.86"
|
async-trait = "0.1.86"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
minijinja = "2.8.0"
|
minijinja = { version = "2.8.0", features = ["loader"] }
|
||||||
nix = { version = "0.30.1", features = ["process", "signal"] }
|
nix = { version = "0.30.1", features = ["process", "signal"] }
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
# Web server dependencies
|
# Web server dependencies
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ use crate::commands::schedule::{
|
|||||||
};
|
};
|
||||||
use crate::commands::session::{handle_session_list, handle_session_remove};
|
use crate::commands::session::{handle_session_list, handle_session_remove};
|
||||||
use crate::logging::setup_logging;
|
use crate::logging::setup_logging;
|
||||||
use crate::recipes::recipe::{explain_recipe_with_parameters, load_recipe_as_template};
|
use crate::recipes::recipe::{
|
||||||
|
explain_recipe_with_parameters, load_recipe_as_template, load_recipe_content_as_template,
|
||||||
|
};
|
||||||
use crate::session;
|
use crate::session;
|
||||||
use crate::session::{build_session, SessionBuilderConfig, SessionSettings};
|
use crate::session::{build_session, SessionBuilderConfig, SessionSettings};
|
||||||
use goose_bench::bench_config::BenchRunConfig;
|
use goose_bench::bench_config::BenchRunConfig;
|
||||||
@@ -431,6 +433,13 @@ enum Command {
|
|||||||
)]
|
)]
|
||||||
explain: bool,
|
explain: bool,
|
||||||
|
|
||||||
|
/// Print the rendered recipe instead of running it
|
||||||
|
#[arg(
|
||||||
|
long = "render-recipe",
|
||||||
|
help = "Print the rendered recipe instead of running it."
|
||||||
|
)]
|
||||||
|
render_recipe: bool,
|
||||||
|
|
||||||
/// Maximum number of consecutive identical tool calls allowed
|
/// Maximum number of consecutive identical tool calls allowed
|
||||||
#[arg(
|
#[arg(
|
||||||
long = "max-tool-repetitions",
|
long = "max-tool-repetitions",
|
||||||
@@ -681,6 +690,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
handle_projects_interactive()?;
|
handle_projects_interactive()?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Command::Run {
|
Some(Command::Run {
|
||||||
instructions,
|
instructions,
|
||||||
input_text,
|
input_text,
|
||||||
@@ -697,11 +707,17 @@ pub async fn cli() -> Result<()> {
|
|||||||
builtins,
|
builtins,
|
||||||
params,
|
params,
|
||||||
explain,
|
explain,
|
||||||
|
render_recipe,
|
||||||
quiet,
|
quiet,
|
||||||
}) => {
|
}) => {
|
||||||
let (input_config, session_settings) = match (instructions, input_text, recipe, explain)
|
let (input_config, session_settings) = match (
|
||||||
{
|
instructions,
|
||||||
(Some(file), _, _, _) if file == "-" => {
|
input_text,
|
||||||
|
recipe,
|
||||||
|
explain,
|
||||||
|
render_recipe,
|
||||||
|
) {
|
||||||
|
(Some(file), _, _, _, _) if file == "-" => {
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
std::io::stdin()
|
std::io::stdin()
|
||||||
.read_to_string(&mut input)
|
.read_to_string(&mut input)
|
||||||
@@ -716,7 +732,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
(Some(file), _, _, _) => {
|
(Some(file), _, _, _, _) => {
|
||||||
let contents = std::fs::read_to_string(&file).unwrap_or_else(|err| {
|
let contents = std::fs::read_to_string(&file).unwrap_or_else(|err| {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Instruction file not found — did you mean to use goose run --text?\n{}",
|
"Instruction file not found — did you mean to use goose run --text?\n{}",
|
||||||
@@ -733,7 +749,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
(_, Some(text), _, _) => (
|
(_, Some(text), _, _, _) => (
|
||||||
InputConfig {
|
InputConfig {
|
||||||
contents: Some(text),
|
contents: Some(text),
|
||||||
extensions_override: None,
|
extensions_override: None,
|
||||||
@@ -741,11 +757,20 @@ pub async fn cli() -> Result<()> {
|
|||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
(_, _, Some(recipe_name), explain) => {
|
(_, _, Some(recipe_name), explain, render_recipe) => {
|
||||||
if explain {
|
if explain {
|
||||||
explain_recipe_with_parameters(&recipe_name, params)?;
|
explain_recipe_with_parameters(&recipe_name, params)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if render_recipe {
|
||||||
|
let recipe = load_recipe_content_as_template(&recipe_name, params)
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
println!("{}", recipe);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let recipe =
|
let recipe =
|
||||||
load_recipe_as_template(&recipe_name, params).unwrap_or_else(|err| {
|
load_recipe_as_template(&recipe_name, params).unwrap_or_else(|err| {
|
||||||
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
||||||
@@ -764,7 +789,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
(None, None, None, _) => {
|
(None, None, None, _, _) => {
|
||||||
eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin.");
|
eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin.");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,31 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use console::style;
|
|
||||||
|
|
||||||
use crate::recipes::print_recipe::{
|
use crate::recipes::print_recipe::{
|
||||||
missing_parameters_command_line, print_parameters_with_values, print_recipe_explanation,
|
missing_parameters_command_line, print_parameters_with_values, print_recipe_explanation,
|
||||||
print_required_parameters_for_template,
|
print_required_parameters_for_template,
|
||||||
};
|
};
|
||||||
use crate::recipes::search_recipe::retrieve_recipe_file;
|
use crate::recipes::search_recipe::retrieve_recipe_file;
|
||||||
|
use anyhow::Result;
|
||||||
|
use console::style;
|
||||||
use goose::recipe::{Recipe, RecipeParameter, RecipeParameterRequirement};
|
use goose::recipe::{Recipe, RecipeParameter, RecipeParameterRequirement};
|
||||||
use minijinja::{Environment, Error, Template, UndefinedBehavior};
|
use minijinja::{Environment, Error, UndefinedBehavior};
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use serde_yaml::Value as YamlValue;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir";
|
pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir";
|
||||||
pub const RECIPE_FILE_EXTENSIONS: &[&str] = &["yaml", "json"];
|
pub const RECIPE_FILE_EXTENSIONS: &[&str] = &["yaml", "json"];
|
||||||
/// Loads, validates a recipe from a YAML or JSON file, and renders it with the given parameters
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `path` - Path to the recipe file (YAML or JSON)
|
|
||||||
/// * `params` - parameters to render the recipe with
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// The rendered recipe if successful
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an error if:
|
|
||||||
/// - Recipe is not valid
|
|
||||||
/// - The required fields are missing
|
|
||||||
pub fn load_recipe_as_template(recipe_name: &str, params: Vec<(String, String)>) -> Result<Recipe> {
|
|
||||||
let (recipe_file_content, recipe_parent_dir) = retrieve_recipe_file(recipe_name)?;
|
|
||||||
|
|
||||||
let recipe = validate_recipe_file_parameters(&recipe_file_content)?;
|
pub fn load_recipe_content_as_template(
|
||||||
|
recipe_name: &str,
|
||||||
|
params: Vec<(String, String)>,
|
||||||
|
) -> Result<String> {
|
||||||
|
let (recipe_file_content, recipe_parent_dir) = retrieve_recipe_file(recipe_name)?;
|
||||||
|
let recipe_parameters = extract_parameters_from_content(&recipe_file_content)?;
|
||||||
|
|
||||||
|
validate_optional_parameters(&recipe_parameters)?;
|
||||||
|
validate_parameters_in_template(&recipe_parameters, &recipe_file_content)?;
|
||||||
|
|
||||||
let (params_for_template, missing_params) =
|
let (params_for_template, missing_params) =
|
||||||
apply_values_to_parameters(¶ms, recipe.parameters, recipe_parent_dir, true)?;
|
apply_values_to_parameters(¶ms, recipe_parameters, recipe_parent_dir, true)?;
|
||||||
|
|
||||||
if !missing_params.is_empty() {
|
if !missing_params.is_empty() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"Please provide the following parameters in the command line: {}",
|
"Please provide the following parameters in the command line: {}",
|
||||||
@@ -45,8 +33,11 @@ pub fn load_recipe_as_template(recipe_name: &str, params: Vec<(String, String)>)
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let rendered_content = render_content_with_params(&recipe_file_content, ¶ms_for_template)?;
|
render_content_with_params(&recipe_file_content, ¶ms_for_template)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_recipe_as_template(recipe_name: &str, params: Vec<(String, String)>) -> Result<Recipe> {
|
||||||
|
let rendered_content = load_recipe_content_as_template(recipe_name, params.clone())?;
|
||||||
let recipe = parse_recipe_content(&rendered_content)?;
|
let recipe = parse_recipe_content(&rendered_content)?;
|
||||||
|
|
||||||
// Display information about the loaded recipe
|
// Display information about the loaded recipe
|
||||||
@@ -57,32 +48,14 @@ pub fn load_recipe_as_template(recipe_name: &str, params: Vec<(String, String)>)
|
|||||||
);
|
);
|
||||||
println!("{} {}", style("Description:").bold(), &recipe.description);
|
println!("{} {}", style("Description:").bold(), &recipe.description);
|
||||||
|
|
||||||
if !params_for_template.is_empty() {
|
if !params.is_empty() {
|
||||||
println!("{}", style("Parameters used to load this recipe:").bold());
|
println!("{}", style("Parameters used to load this recipe:").bold());
|
||||||
print_parameters_with_values(params_for_template);
|
print_parameters_with_values(params.into_iter().collect());
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
Ok(recipe)
|
Ok(recipe)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads and validates a recipe from a YAML or JSON file
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `path` - Path to the recipe file (YAML or JSON)
|
|
||||||
/// * `params` - optional parameters to render the recipe with
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// The parsed recipe struct if successful
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an error if:
|
|
||||||
/// - The file doesn't exist
|
|
||||||
/// - The file can't be read
|
|
||||||
/// - The YAML/JSON is invalid
|
|
||||||
/// - The parameter definition does not match the template variables in the recipe file
|
|
||||||
pub fn load_recipe(recipe_name: &str) -> Result<Recipe> {
|
pub fn load_recipe(recipe_name: &str) -> Result<Recipe> {
|
||||||
let (recipe_file_content, _) = retrieve_recipe_file(recipe_name)?;
|
let (recipe_file_content, _) = retrieve_recipe_file(recipe_name)?;
|
||||||
|
|
||||||
@@ -105,11 +78,34 @@ pub fn explain_recipe_with_parameters(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_recipe_file_parameters(recipe_file_content: &str) -> Result<Recipe> {
|
fn extract_parameters_from_content(content: &str) -> Result<Option<Vec<RecipeParameter>>> {
|
||||||
let recipe_from_recipe_file: Recipe = parse_recipe_content(recipe_file_content)?;
|
let lines = content.lines();
|
||||||
validate_optional_parameters(&recipe_from_recipe_file)?;
|
let mut params_block = String::new();
|
||||||
validate_parameters_in_template(&recipe_from_recipe_file.parameters, recipe_file_content)?;
|
let mut collecting = false;
|
||||||
Ok(recipe_from_recipe_file)
|
|
||||||
|
for line in lines {
|
||||||
|
if line.starts_with("parameters:") {
|
||||||
|
collecting = true;
|
||||||
|
}
|
||||||
|
if collecting {
|
||||||
|
if !line.is_empty() && !line.starts_with(' ') && !line.starts_with('\t') {
|
||||||
|
let parameters: Vec<RecipeParameter> = serde_yaml::from_str(¶ms_block)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to parse parameters block: {}", e))?;
|
||||||
|
return Ok(Some(parameters));
|
||||||
|
}
|
||||||
|
params_block.push_str(line);
|
||||||
|
params_block.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't find a parameter block it might be because it is defined in json style or some such:
|
||||||
|
if serde_yaml::from_str::<serde_yaml::Value>(content).is_err() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let recipe: Recipe = serde_yaml::from_str(content)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Valid YAML but invalid Recipe structure: {}", e))?;
|
||||||
|
Ok(recipe.parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_parameters_in_template(
|
fn validate_parameters_in_template(
|
||||||
@@ -164,9 +160,8 @@ fn validate_parameters_in_template(
|
|||||||
Err(anyhow::anyhow!("{}", message.trim_end()))
|
Err(anyhow::anyhow!("{}", message.trim_end()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_optional_parameters(recipe: &Recipe) -> Result<()> {
|
fn validate_optional_parameters(parameters: &Option<Vec<RecipeParameter>>) -> Result<()> {
|
||||||
let optional_params_without_default_values: Vec<String> = recipe
|
let optional_params_without_default_values: Vec<String> = parameters
|
||||||
.parameters
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(&vec![])
|
.unwrap_or(&vec![])
|
||||||
.iter()
|
.iter()
|
||||||
@@ -184,15 +179,12 @@ fn validate_optional_parameters(recipe: &Recipe) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_recipe_content(content: &str) -> Result<Recipe> {
|
fn parse_recipe_content(content: &str) -> Result<Recipe> {
|
||||||
if serde_json::from_str::<JsonValue>(content).is_ok() {
|
if content.trim().is_empty() {
|
||||||
Ok(serde_json::from_str(content)?)
|
return Err(anyhow::anyhow!("Recipe content is empty"));
|
||||||
} else if serde_yaml::from_str::<YamlValue>(content).is_ok() {
|
|
||||||
Ok(serde_yaml::from_str(content)?)
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Unsupported file format for recipe file. Expected .yaml or .json"
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serde_yaml::from_str(content)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to parse recipe content: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_template_variables(template_str: &str) -> Result<HashSet<String>> {
|
fn extract_template_variables(template_str: &str) -> Result<HashSet<String>> {
|
||||||
@@ -244,20 +236,40 @@ fn apply_values_to_parameters(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_content_with_params(content: &str, params: &HashMap<String, String>) -> Result<String> {
|
fn render_content_with_params(content: &str, params: &HashMap<String, String>) -> Result<String> {
|
||||||
// Create a minijinja environment and context
|
|
||||||
let mut env = minijinja::Environment::new();
|
let mut env = minijinja::Environment::new();
|
||||||
env.set_undefined_behavior(UndefinedBehavior::Strict);
|
env.set_undefined_behavior(UndefinedBehavior::Strict);
|
||||||
let template: Template<'_, '_> = env
|
|
||||||
.template_from_str(content)
|
|
||||||
.map_err(|e: Error| anyhow::anyhow!("Invalid template syntax: {}", e.to_string()))?;
|
|
||||||
|
|
||||||
// Render the template with the parameters
|
if let Some(recipe_dir) = params.get("recipe_dir") {
|
||||||
template.render(params).map_err(|e: Error| {
|
let recipe_dir = recipe_dir.clone();
|
||||||
anyhow::anyhow!(
|
env.set_loader(move |name| {
|
||||||
"Failed to render the recipe {} - please check if all required parameters are provided",
|
let path = Path::new(&recipe_dir).join(name);
|
||||||
e.to_string()
|
match std::fs::read_to_string(&path) {
|
||||||
)
|
Ok(content) => Ok(Some(content)),
|
||||||
})
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||||
|
Err(e) => Err(minijinja::Error::new(
|
||||||
|
minijinja::ErrorKind::InvalidOperation,
|
||||||
|
"could not read template",
|
||||||
|
)
|
||||||
|
.with_source(e)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = env
|
||||||
|
.template_from_str(content)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid template syntax: {}", e))?;
|
||||||
|
|
||||||
|
template
|
||||||
|
.render(params)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to render the recipe {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_recipe_file_parameters(recipe_file_content: &str) -> Result<Recipe> {
|
||||||
|
let recipe_from_recipe_file: Recipe = parse_recipe_content(recipe_file_content)?;
|
||||||
|
let parameters = extract_parameters_from_content(recipe_file_content)?;
|
||||||
|
validate_optional_parameters(¶meters)?;
|
||||||
|
validate_parameters_in_template(¶meters, recipe_file_content)?;
|
||||||
|
Ok(recipe_from_recipe_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -279,9 +291,9 @@ mod tests {
|
|||||||
}}"#,
|
}}"#,
|
||||||
instructions_and_parameters
|
instructions_and_parameters
|
||||||
);
|
);
|
||||||
// Create a temporary file
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let recipe_path: std::path::PathBuf = temp_dir.path().join("test_recipe.json");
|
let recipe_path: std::path::PathBuf = temp_dir.path().join("test_recipe.yaml");
|
||||||
|
|
||||||
std::fs::write(&recipe_path, recipe_content).unwrap();
|
std::fs::write(&recipe_path, recipe_content).unwrap();
|
||||||
(temp_dir, recipe_path)
|
(temp_dir, recipe_path)
|
||||||
}
|
}
|
||||||
@@ -314,9 +326,8 @@ mod tests {
|
|||||||
let content = "Hello {{ missing }}!";
|
let content = "Hello {{ missing }}!";
|
||||||
let params = HashMap::new();
|
let params = HashMap::new();
|
||||||
let err = render_content_with_params(content, ¶ms).unwrap_err();
|
let err = render_content_with_params(content, ¶ms).unwrap_err();
|
||||||
assert!(err
|
let error_msg = err.to_string();
|
||||||
.to_string()
|
assert!(error_msg.contains("Failed to render the recipe"));
|
||||||
.contains("please check if all required parameters"));
|
|
||||||
|
|
||||||
// Test invalid template syntax results in error
|
// Test invalid template syntax results in error
|
||||||
let content = "Hello {{ unclosed";
|
let content = "Hello {{ unclosed";
|
||||||
@@ -490,9 +501,7 @@ mod tests {
|
|||||||
assert!(load_recipe_result.is_err());
|
assert!(load_recipe_result.is_err());
|
||||||
let err = load_recipe_result.unwrap_err();
|
let err = load_recipe_result.unwrap_err();
|
||||||
println!("{}", err.to_string());
|
println!("{}", err.to_string());
|
||||||
assert!(err.to_string().contains(
|
assert!(err.to_string().to_lowercase().contains("missing"));
|
||||||
"Optional parameters missing default values in the recipe: optional_param. Please provide defaults."
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -513,9 +522,9 @@ mod tests {
|
|||||||
let load_recipe_result = load_recipe_as_template(recipe_path.to_str().unwrap(), params);
|
let load_recipe_result = load_recipe_as_template(recipe_path.to_str().unwrap(), params);
|
||||||
assert!(load_recipe_result.is_err());
|
assert!(load_recipe_result.is_err());
|
||||||
let err = load_recipe_result.unwrap_err();
|
let err = load_recipe_result.unwrap_err();
|
||||||
assert!(err
|
let err_msg = err.to_string();
|
||||||
.to_string()
|
eprint!("Error: {}", err_msg);
|
||||||
.contains("unknown variant `some_invalid_type`"));
|
assert!(err_msg.contains("unknown variant `some_invalid_type`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -529,4 +538,48 @@ mod tests {
|
|||||||
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]
|
||||||
|
fn test_template_inheritance() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let temp_path = temp_dir.path();
|
||||||
|
|
||||||
|
let parent_content = [
|
||||||
|
"version: 1.0.0",
|
||||||
|
"title: Parent",
|
||||||
|
"description: Parent recipe",
|
||||||
|
"prompt: |",
|
||||||
|
" {% block prompt -%}",
|
||||||
|
" What is the capital of France?",
|
||||||
|
" {%- endblock -%}",
|
||||||
|
]
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
let parent_path = temp_path.join("parent.yaml");
|
||||||
|
std::fs::write(&parent_path, parent_content).unwrap();
|
||||||
|
|
||||||
|
let child_content = [
|
||||||
|
"{% extends \"parent.yaml\" -%}",
|
||||||
|
"{%- block prompt -%}",
|
||||||
|
" What is the capital of Germany?",
|
||||||
|
"{%- endblock -%}",
|
||||||
|
]
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
let child_path = temp_path.join("child.yaml");
|
||||||
|
std::fs::write(&child_path, child_content).unwrap();
|
||||||
|
|
||||||
|
let params = vec![];
|
||||||
|
let result = load_recipe_as_template(child_path.to_str().unwrap(), params);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let recipe = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(recipe.title, "Parent");
|
||||||
|
assert_eq!(recipe.description, "Parent recipe");
|
||||||
|
assert_eq!(
|
||||||
|
recipe.prompt.unwrap().trim(),
|
||||||
|
"What is the capital of Germany?"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user