use rustc_hash::FxHashMap;
use serde::{Deserialize, Deserializer, Serialize};
use crate::ast::{AgentParams, InvokeParams};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ResponseFormat {
#[default]
Text,
Json,
Markdown,
}
#[derive(Debug, Clone, Default)]
pub struct InferParams {
pub prompt: String,
pub provider: Option<String>,
pub model: Option<String>,
pub temperature: Option<f64>,
pub max_tokens: Option<u32>,
pub system: Option<String>,
pub response_format: Option<ResponseFormat>,
pub extended_thinking: Option<bool>,
pub thinking_budget: Option<u64>,
}
impl<'de> Deserialize<'de> for InferParams {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum InferParamsHelper {
Short(String),
Full {
prompt: String,
#[serde(default)]
provider: Option<String>,
#[serde(default)]
model: Option<String>,
#[serde(default)]
temperature: Option<f64>,
#[serde(default)]
max_tokens: Option<u32>,
#[serde(default)]
system: Option<String>,
#[serde(default)]
response_format: Option<ResponseFormat>,
#[serde(default)]
extended_thinking: Option<bool>,
#[serde(default)]
thinking_budget: Option<u64>,
},
}
match InferParamsHelper::deserialize(deserializer)? {
InferParamsHelper::Short(prompt) => Ok(InferParams {
prompt,
provider: None,
model: None,
temperature: None,
max_tokens: None,
system: None,
response_format: None,
extended_thinking: None,
thinking_budget: None,
}),
InferParamsHelper::Full {
prompt,
provider,
model,
temperature,
max_tokens,
system,
response_format,
extended_thinking,
thinking_budget,
} => Ok(InferParams {
prompt,
provider,
model,
temperature,
max_tokens,
system,
response_format,
extended_thinking,
thinking_budget,
}),
}
}
}
impl InferParams {
pub fn validate(&self) -> Result<(), String> {
if self.prompt.trim().is_empty() {
return Err("Infer prompt cannot be empty".to_string());
}
if let Some(temp) = self.temperature {
if !(0.0..=2.0).contains(&temp) {
return Err(format!(
"temperature must be between 0.0 and 2.0, got {}",
temp
));
}
}
if self.extended_thinking == Some(true) {
if let Some(ref provider) = self.provider {
if provider != "claude" {
return Err(format!(
"extended_thinking only supported for claude provider, got '{}'",
provider
));
}
}
}
if let Some(budget) = self.thinking_budget {
if !(1024..=65536).contains(&budget) {
return Err(format!(
"thinking_budget must be between 1024 and 65536, got {}",
budget
));
}
}
Ok(())
}
pub fn effective_thinking_budget(&self) -> u64 {
self.thinking_budget.unwrap_or(4096)
}
}
#[derive(Debug, Clone)]
pub struct ExecParams {
pub command: String,
pub shell: Option<bool>,
pub timeout: Option<u64>,
pub cwd: Option<String>,
}
impl<'de> Deserialize<'de> for ExecParams {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum ExecParamsHelper {
Short(String),
Full {
command: String,
#[serde(default)]
shell: Option<bool>,
#[serde(default)]
timeout: Option<u64>,
#[serde(default)]
cwd: Option<String>,
},
}
match ExecParamsHelper::deserialize(deserializer)? {
ExecParamsHelper::Short(command) => Ok(ExecParams {
command,
shell: None,
timeout: None,
cwd: None,
}),
ExecParamsHelper::Full {
command,
shell,
timeout,
cwd,
} => Ok(ExecParams {
command,
shell,
timeout,
cwd,
}),
}
}
}
#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
pub struct RetryConfig {
#[serde(default = "default_max_attempts")]
pub max_attempts: u32,
#[serde(default = "default_backoff_ms")]
pub backoff_ms: u64,
#[serde(default = "default_multiplier")]
pub multiplier: f64,
}
fn default_max_attempts() -> u32 {
3
}
fn default_backoff_ms() -> u64 {
1000
}
fn default_multiplier() -> f64 {
2.0
}
#[derive(Debug, Clone, Deserialize)]
pub struct FetchParams {
pub url: String,
#[serde(default = "default_method")]
pub method: String,
#[serde(default)]
pub headers: FxHashMap<String, String>,
pub body: Option<String>,
pub timeout: Option<u64>,
#[serde(default)]
pub retry: Option<RetryConfig>,
}
fn default_method() -> String {
"GET".to_string()
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum TaskAction {
Infer { infer: InferParams },
Exec { exec: ExecParams },
Fetch { fetch: FetchParams },
Invoke { invoke: InvokeParams },
Agent { agent: AgentParams },
}
impl TaskAction {
pub fn verb_name(&self) -> &'static str {
match self {
TaskAction::Infer { .. } => "infer",
TaskAction::Exec { .. } => "exec",
TaskAction::Fetch { .. } => "fetch",
TaskAction::Invoke { .. } => "invoke",
TaskAction::Agent { .. } => "agent",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serde_yaml;
use serde_json::json;
#[test]
fn test_infer_params_shorthand_deserialize() {
let yaml = r#"
infer: "Generate a headline for QR Code AI"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.prompt, "Generate a headline for QR Code AI");
assert!(infer.provider.is_none());
assert!(infer.model.is_none());
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_full_form_deserialize() {
let yaml = r#"
infer:
prompt: "Generate a headline"
provider: claude
model: claude-sonnet-4-6
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.prompt, "Generate a headline");
assert_eq!(infer.provider, Some("claude".to_string()));
assert_eq!(infer.model, Some("claude-sonnet-4-6".to_string()));
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_full_form_only_prompt() {
let yaml = r#"
infer:
prompt: "Generate a headline"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.prompt, "Generate a headline");
assert!(infer.provider.is_none());
assert!(infer.model.is_none());
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_multiline_prompt_shorthand() {
let yaml = r#"
infer: |
Generate a comprehensive headline for QR Code AI.
Include value proposition and key benefit.
Keep under 100 characters.
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert!(infer.prompt.contains("Generate a comprehensive headline"));
assert!(infer.prompt.contains("value proposition"));
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_with_provider_only() {
let yaml = r#"
infer:
prompt: "Test"
provider: openai
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.provider, Some("openai".to_string()));
assert!(infer.model.is_none());
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_with_model_only() {
let yaml = r#"
infer:
prompt: "Test"
model: gpt-4
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert!(infer.provider.is_none());
assert_eq!(infer.model, Some("gpt-4".to_string()));
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_validate_ok() {
let params = InferParams {
prompt: "Generate something".to_string(),
provider: None,
model: None,
temperature: Some(0.7),
max_tokens: None,
system: None,
response_format: None,
extended_thinking: None,
thinking_budget: None,
};
assert!(params.validate().is_ok());
}
#[test]
fn test_infer_params_validate_empty_prompt() {
let params = InferParams {
prompt: "".to_string(),
..Default::default()
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty"));
}
#[test]
fn test_infer_params_validate_whitespace_only_prompt() {
let params = InferParams {
prompt: " \n\t ".to_string(),
..Default::default()
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty"));
}
#[test]
fn test_infer_params_validate_temperature_too_low() {
let params = InferParams {
prompt: "Test".to_string(),
temperature: Some(-0.1),
..Default::default()
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("temperature"));
}
#[test]
fn test_infer_params_validate_temperature_too_high() {
let params = InferParams {
prompt: "Test".to_string(),
temperature: Some(2.5),
..Default::default()
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().contains("temperature"));
}
#[test]
fn test_infer_params_validate_temperature_boundary_valid() {
let params_min = InferParams {
prompt: "Test".to_string(),
temperature: Some(0.0),
..Default::default()
};
assert!(params_min.validate().is_ok());
let params_max = InferParams {
prompt: "Test".to_string(),
temperature: Some(2.0),
..Default::default()
};
assert!(params_max.validate().is_ok());
}
#[test]
fn test_infer_params_with_temperature() {
let yaml = r#"
infer:
prompt: "Be creative"
temperature: 0.9
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.prompt, "Be creative");
assert_eq!(infer.temperature, Some(0.9));
assert!(infer.max_tokens.is_none());
assert!(infer.system.is_none());
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_with_max_tokens() {
let yaml = r#"
infer:
prompt: "Short answer"
max_tokens: 100
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.prompt, "Short answer");
assert_eq!(infer.max_tokens, Some(100));
assert!(infer.temperature.is_none());
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_with_system_prompt() {
let yaml = r#"
infer:
prompt: "Explain quantum computing"
system: "You are a physics professor explaining to undergraduates."
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.prompt, "Explain quantum computing");
assert_eq!(
infer.system,
Some("You are a physics professor explaining to undergraduates.".to_string())
);
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_full_llm_control() {
let yaml = r#"
infer:
prompt: "Write a haiku"
provider: openai
model: gpt-4o
temperature: 0.7
max_tokens: 50
system: "You are a Japanese poetry master."
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.prompt, "Write a haiku");
assert_eq!(infer.provider, Some("openai".to_string()));
assert_eq!(infer.model, Some("gpt-4o".to_string()));
assert_eq!(infer.temperature, Some(0.7));
assert_eq!(infer.max_tokens, Some(50));
assert_eq!(
infer.system,
Some("You are a Japanese poetry master.".to_string())
);
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_shorthand_defaults_llm_options() {
let yaml = r#"
infer: "Simple prompt"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.prompt, "Simple prompt");
assert!(infer.temperature.is_none());
assert!(infer.max_tokens.is_none());
assert!(infer.system.is_none());
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_temperature_zero() {
let yaml = r#"
infer:
prompt: "Deterministic output"
temperature: 0.0
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.temperature, Some(0.0));
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_exec_params_shorthand_deserialize() {
let yaml = r#"
exec: "npm run build"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Exec { exec } => {
assert_eq!(exec.command, "npm run build");
}
_ => panic!("Expected TaskAction::Exec"),
}
}
#[test]
fn test_exec_params_full_form_deserialize() {
let yaml = r#"
exec:
command: "npm run build"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Exec { exec } => {
assert_eq!(exec.command, "npm run build");
}
_ => panic!("Expected TaskAction::Exec"),
}
}
#[test]
fn test_exec_params_complex_command() {
let yaml = r#"
exec: "cargo test --lib -- --test-threads=1 --nocapture"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Exec { exec } => {
assert!(exec.command.contains("cargo test"));
assert!(exec.command.contains("--test-threads=1"));
}
_ => panic!("Expected TaskAction::Exec"),
}
}
#[test]
fn test_exec_params_with_pipes_and_redirects() {
let yaml = r#"
exec: "cat file.txt | grep pattern > output.txt"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Exec { exec } => {
assert!(exec.command.contains("grep pattern"));
}
_ => panic!("Expected TaskAction::Exec"),
}
}
#[test]
fn test_exec_params_shell_field_default_none() {
let yaml = r#"
exec:
command: "echo hello"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Exec { exec } => {
assert_eq!(exec.command, "echo hello");
assert_eq!(exec.shell, None); }
_ => panic!("Expected TaskAction::Exec"),
}
}
#[test]
fn test_exec_params_shell_true_explicit() {
let yaml = r#"
exec:
command: "echo $HOME && ls | grep foo"
shell: true
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Exec { exec } => {
assert!(exec.command.contains("$HOME"));
assert_eq!(exec.shell, Some(true));
}
_ => panic!("Expected TaskAction::Exec"),
}
}
#[test]
fn test_exec_params_shell_false_explicit() {
let yaml = r#"
exec:
command: "echo hello"
shell: false
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Exec { exec } => {
assert_eq!(exec.shell, Some(false));
}
_ => panic!("Expected TaskAction::Exec"),
}
}
#[test]
fn test_fetch_params_minimal() {
let yaml = r#"
fetch:
url: "https://api.example.com/data"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Fetch { fetch } => {
assert_eq!(fetch.url, "https://api.example.com/data");
assert_eq!(fetch.method, "GET");
assert!(fetch.headers.is_empty());
assert!(fetch.body.is_none());
}
_ => panic!("Expected TaskAction::Fetch"),
}
}
#[test]
fn test_fetch_params_with_method() {
let yaml = r#"
fetch:
url: "https://api.example.com/data"
method: "POST"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Fetch { fetch } => {
assert_eq!(fetch.method, "POST");
}
_ => panic!("Expected TaskAction::Fetch"),
}
}
#[test]
fn test_fetch_params_default_method_get() {
let yaml = r#"
fetch:
url: "https://api.example.com/data"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Fetch { fetch } => {
assert_eq!(fetch.method, "GET");
}
_ => panic!("Expected TaskAction::Fetch"),
}
}
#[test]
fn test_fetch_params_with_headers() {
let yaml = r#"
fetch:
url: "https://api.example.com/data"
method: "GET"
headers:
Authorization: "Bearer token123"
Content-Type: "application/json"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Fetch { fetch } => {
assert_eq!(fetch.headers.len(), 2);
assert_eq!(
fetch.headers.get("Authorization"),
Some(&"Bearer token123".to_string())
);
assert_eq!(
fetch.headers.get("Content-Type"),
Some(&"application/json".to_string())
);
}
_ => panic!("Expected TaskAction::Fetch"),
}
}
#[test]
fn test_fetch_params_with_body() {
let yaml = r#"
fetch:
url: "https://api.example.com/data"
method: "POST"
body: '{"key": "value"}'
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Fetch { fetch } => {
assert_eq!(fetch.body, Some(r#"{"key": "value"}"#.to_string()));
}
_ => panic!("Expected TaskAction::Fetch"),
}
}
#[test]
fn test_fetch_params_complete() {
let yaml = r#"
fetch:
url: "https://api.example.com/users"
method: "POST"
headers:
Authorization: "Bearer token"
Content-Type: "application/json"
body: '{"name": "Alice"}'
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Fetch { fetch } => {
assert_eq!(fetch.url, "https://api.example.com/users");
assert_eq!(fetch.method, "POST");
assert_eq!(fetch.headers.len(), 2);
assert_eq!(fetch.body, Some(r#"{"name": "Alice"}"#.to_string()));
}
_ => panic!("Expected TaskAction::Fetch"),
}
}
#[test]
fn test_invoke_params_tool_call() {
let yaml = r#"
invoke:
mcp: novanet
tool: novanet_generate
params:
entity: qr-code
locale: fr-FR
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Invoke { invoke } => {
assert_eq!(invoke.mcp, "novanet");
assert_eq!(invoke.tool, Some("novanet_generate".to_string()));
assert_eq!(
invoke.params,
Some(json!({"entity": "qr-code", "locale": "fr-FR"}))
);
assert!(invoke.resource.is_none());
}
_ => panic!("Expected TaskAction::Invoke"),
}
}
#[test]
fn test_invoke_params_resource_read() {
let yaml = r#"
invoke:
mcp: novanet
resource: entity://qr-code/fr-FR
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Invoke { invoke } => {
assert_eq!(invoke.mcp, "novanet");
assert!(invoke.tool.is_none());
assert_eq!(invoke.resource, Some("entity://qr-code/fr-FR".to_string()));
assert!(invoke.params.is_none());
}
_ => panic!("Expected TaskAction::Invoke"),
}
}
#[test]
fn test_invoke_params_tool_without_params() {
let yaml = r#"
invoke:
mcp: test_server
tool: simple_tool
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Invoke { invoke } => {
assert_eq!(invoke.mcp, "test_server");
assert_eq!(invoke.tool, Some("simple_tool".to_string()));
assert!(invoke.params.is_none());
}
_ => panic!("Expected TaskAction::Invoke"),
}
}
#[test]
fn test_agent_params_minimal() {
let yaml = r#"
agent:
prompt: "Generate content for homepage"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Agent { agent } => {
assert_eq!(agent.prompt, "Generate content for homepage");
assert!(agent.system.is_none());
assert!(agent.provider.is_none());
assert!(agent.model.is_none());
assert!(agent.mcp.is_empty());
}
_ => panic!("Expected TaskAction::Agent"),
}
}
#[test]
fn test_agent_params_with_mcp() {
let yaml = r#"
agent:
prompt: "Generate with MCP tools"
mcp:
- novanet
- perplexity
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Agent { agent } => {
assert_eq!(agent.mcp.len(), 2);
assert!(agent.mcp.contains(&"novanet".to_string()));
assert!(agent.mcp.contains(&"perplexity".to_string()));
}
_ => panic!("Expected TaskAction::Agent"),
}
}
#[test]
fn test_agent_params_with_max_turns() {
let yaml = r#"
agent:
prompt: "Test prompt"
max_turns: 5
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Agent { agent } => {
assert_eq!(agent.max_turns, Some(5));
}
_ => panic!("Expected TaskAction::Agent"),
}
}
#[test]
fn test_agent_params_with_stop_conditions() {
let yaml = r#"
agent:
prompt: "Test prompt"
stop_conditions:
- "GENERATION_COMPLETE"
- "ERROR"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Agent { agent } => {
assert_eq!(agent.stop_conditions.len(), 2);
assert!(agent
.stop_conditions
.contains(&"GENERATION_COMPLETE".to_string()));
}
_ => panic!("Expected TaskAction::Agent"),
}
}
#[test]
fn test_agent_params_with_extended_thinking() {
let yaml = r#"
agent:
prompt: "Test prompt"
extended_thinking: true
thinking_budget: 8192
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Agent { agent } => {
assert_eq!(agent.extended_thinking, Some(true));
assert_eq!(agent.thinking_budget, Some(8192));
}
_ => panic!("Expected TaskAction::Agent"),
}
}
#[test]
fn test_agent_params_with_provider_and_model() {
let yaml = r#"
agent:
prompt: "Test prompt"
provider: claude
model: claude-sonnet-4-6
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Agent { agent } => {
assert_eq!(agent.provider, Some("claude".to_string()));
assert_eq!(agent.model, Some("claude-sonnet-4-6".to_string()));
}
_ => panic!("Expected TaskAction::Agent"),
}
}
#[test]
fn test_agent_params_complete() {
let yaml = r#"
agent:
prompt: "Generate landing page content"
system: "You are a web content expert"
provider: claude
model: claude-sonnet-4-6
mcp:
- novanet
max_turns: 10
token_budget: 10000
stop_conditions:
- "CONTENT_READY"
scope: full
extended_thinking: true
thinking_budget: 4096
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Agent { agent } => {
assert_eq!(agent.prompt, "Generate landing page content");
assert_eq!(
agent.system,
Some("You are a web content expert".to_string())
);
assert_eq!(agent.provider, Some("claude".to_string()));
assert_eq!(agent.model, Some("claude-sonnet-4-6".to_string()));
assert_eq!(agent.mcp.len(), 1);
assert_eq!(agent.max_turns, Some(10));
assert_eq!(agent.token_budget, Some(10000));
assert_eq!(agent.stop_conditions.len(), 1);
assert_eq!(agent.scope, Some("full".to_string()));
assert_eq!(agent.extended_thinking, Some(true));
assert_eq!(agent.thinking_budget, Some(4096));
}
_ => panic!("Expected TaskAction::Agent"),
}
}
#[test]
fn test_verb_name_infer() {
let action = TaskAction::Infer {
infer: InferParams {
prompt: "test".to_string(),
..Default::default()
},
};
assert_eq!(action.verb_name(), "infer");
}
#[test]
fn test_verb_name_exec() {
let action = TaskAction::Exec {
exec: ExecParams {
command: "echo test".to_string(),
shell: None,
timeout: None,
cwd: None,
},
};
assert_eq!(action.verb_name(), "exec");
}
#[test]
fn test_verb_name_fetch() {
let action = TaskAction::Fetch {
fetch: FetchParams {
url: "https://example.com".to_string(),
method: "GET".to_string(),
headers: FxHashMap::default(),
body: None,
timeout: None,
retry: None,
},
};
assert_eq!(action.verb_name(), "fetch");
}
#[test]
fn test_verb_name_invoke() {
let action = TaskAction::Invoke {
invoke: InvokeParams {
mcp: "test".to_string(),
tool: Some("test_tool".to_string()),
params: None,
resource: None,
},
};
assert_eq!(action.verb_name(), "invoke");
}
#[test]
fn test_verb_name_agent() {
let action = TaskAction::Agent {
agent: AgentParams {
prompt: "test".to_string(),
..Default::default()
},
};
assert_eq!(action.verb_name(), "agent");
}
#[test]
fn test_parse_multiple_action_types() {
let infer_yaml = r#"infer: "test""#;
let exec_yaml = r#"exec: "test""#;
let fetch_yaml = r#"fetch: { url: "http://example.com" }"#;
let infer_action: TaskAction = serde_yaml::from_str(infer_yaml).unwrap();
let exec_action: TaskAction = serde_yaml::from_str(exec_yaml).unwrap();
let fetch_action: TaskAction = serde_yaml::from_str(fetch_yaml).unwrap();
assert_eq!(infer_action.verb_name(), "infer");
assert_eq!(exec_action.verb_name(), "exec");
assert_eq!(fetch_action.verb_name(), "fetch");
}
#[test]
fn test_infer_params_empty_prompt() {
let yaml = r#"
infer: ""
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert_eq!(infer.prompt, "");
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_exec_params_empty_command() {
let yaml = r#"
exec: ""
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Exec { exec } => {
assert_eq!(exec.command, "");
}
_ => panic!("Expected TaskAction::Exec"),
}
}
#[test]
fn test_fetch_params_empty_headers() {
let yaml = r#"
fetch:
url: "https://example.com"
headers: {}
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Fetch { fetch } => {
assert!(fetch.headers.is_empty());
}
_ => panic!("Expected TaskAction::Fetch"),
}
}
#[test]
fn test_agent_params_empty_mcp_list() {
let yaml = r#"
agent:
prompt: "test"
mcp: []
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Agent { agent } => {
assert!(agent.mcp.is_empty());
}
_ => panic!("Expected TaskAction::Agent"),
}
}
#[test]
fn test_agent_params_empty_stop_conditions() {
let yaml = r#"
agent:
prompt: "test"
stop_conditions: []
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Agent { agent } => {
assert!(agent.stop_conditions.is_empty());
}
_ => panic!("Expected TaskAction::Agent"),
}
}
#[test]
fn test_infer_params_special_characters() {
let yaml = r#"
infer: "Generate content with special chars: !@#$%^&*()"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert!(infer.prompt.contains("!@#$%^&*()"));
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_infer_params_unicode() {
let yaml = r#"
infer: "Generate content en français: résumé, café, naïve"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Infer { infer } => {
assert!(infer.prompt.contains("français"));
assert!(infer.prompt.contains("résumé"));
}
_ => panic!("Expected TaskAction::Infer"),
}
}
#[test]
fn test_fetch_params_url_with_query_string() {
let yaml = r#"
fetch:
url: "https://api.example.com/search?q=rust&limit=10&offset=5"
"#;
let action: TaskAction = serde_yaml::from_str(yaml).unwrap();
match action {
TaskAction::Fetch { fetch } => {
assert!(fetch.url.contains("search?q=rust"));
assert!(fetch.url.contains("limit=10"));
}
_ => panic!("Expected TaskAction::Fetch"),
}
}
#[test]
fn test_infer_action_clone() {
let action = TaskAction::Infer {
infer: InferParams {
prompt: "test".to_string(),
provider: Some("claude".to_string()),
model: Some("claude-sonnet-4-6".to_string()),
..Default::default()
},
};
let cloned = action.clone();
assert_eq!(action.verb_name(), cloned.verb_name());
}
#[test]
fn test_all_action_types_cloneable() {
let infer = TaskAction::Infer {
infer: InferParams {
prompt: "test".to_string(),
..Default::default()
},
};
let exec = TaskAction::Exec {
exec: ExecParams {
command: "echo".to_string(),
shell: None,
timeout: None,
cwd: None,
},
};
let fetch = TaskAction::Fetch {
fetch: FetchParams {
url: "http://example.com".to_string(),
method: "GET".to_string(),
headers: FxHashMap::default(),
body: None,
timeout: None,
retry: None,
},
};
let _ = infer.clone();
let _ = exec.clone();
let _ = fetch.clone();
}
}