use std::fmt::Write as _;
use tokio_util::sync::CancellationToken;
use crate::cli::args::{OutputFormat, ScenariosListArgs, ScenariosRunArgs, ScenariosShowArgs};
use crate::error::ThoughtJackError;
use crate::scenarios::{self, ScenarioCategory};
#[allow(clippy::unused_async)]
pub async fn list(args: &ScenariosListArgs, quiet: bool) -> Result<(), ThoughtJackError> {
let results = scenarios::list_scenarios(args.category, args.tag.as_deref());
if quiet {
return Ok(());
}
match args.format {
OutputFormat::Json => {
let json_entries: Vec<serde_json::Value> = results
.iter()
.map(|s| {
serde_json::json!({
"name": s.name,
"description": s.description,
"category": s.category.to_string(),
"tags": s.tags,
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&json_entries)
.map_err(|e| ThoughtJackError::Io(std::io::Error::other(e.to_string())))?
);
}
OutputFormat::Human => {
if results.is_empty() {
println!("No scenarios match the given filters.");
return Ok(());
}
let total = results.len();
println!("Built-in Scenarios ({total} available)\n");
for cat in ScenarioCategory::all() {
let in_cat: Vec<_> = results.iter().filter(|s| s.category == *cat).collect();
if in_cat.is_empty() {
continue;
}
println!(" {}", cat.label());
for s in in_cat {
println!(" {:<24}{}", s.name, s.description);
}
println!();
}
println!("Run a scenario: thoughtjack scenarios run <name>");
println!("View YAML: thoughtjack scenarios show <name>");
}
}
Ok(())
}
#[allow(clippy::unused_async)]
pub async fn show(args: &ScenariosShowArgs, quiet: bool) -> Result<(), ThoughtJackError> {
let scenario = scenarios::find_scenario(&args.name).ok_or_else(|| {
let mut message = format!("Unknown scenario '{}'", args.name);
if let Some(suggestion) = scenarios::suggest_scenario(&args.name) {
let _ = write!(message, "\n\nDid you mean '{suggestion}'?");
}
message.push_str("\n\nAvailable scenarios:");
for name in scenarios::list_scenario_names() {
if let Some(s) = scenarios::find_scenario(name) {
let _ = write!(message, "\n {:<24}{}", s.name, s.description);
}
}
message.push_str("\n\nUse 'thoughtjack scenarios list' for full details.");
ThoughtJackError::Usage(message)
})?;
if !quiet {
print!("{}", scenario.yaml);
}
Ok(())
}
pub async fn run_scenario(
args: &ScenariosRunArgs,
quiet: bool,
color: crate::cli::args::ColorChoice,
cancel: CancellationToken,
) -> Result<(), ThoughtJackError> {
let scenario = scenarios::find_scenario(&args.name).ok_or_else(|| {
let mut message = format!("Unknown scenario '{}'", args.name);
if let Some(suggestion) = scenarios::suggest_scenario(&args.name) {
let _ = write!(message, "\n\nDid you mean '{suggestion}'?");
}
message.push_str("\n\nUse 'thoughtjack scenarios list' to see available scenarios.");
ThoughtJackError::Usage(message)
})?;
super::run::run_from_yaml(scenario.yaml, &args.execution, quiet, color, cancel).await
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::cli::args::{ExecutionArgs, ProgressLevel};
fn test_run_args() -> ExecutionArgs {
ExecutionArgs {
mcp_server: None,
mcp_client_command: None,
mcp_client_args: None,
mcp_client_endpoint: None,
agui_client_endpoint: None,
a2a_server: None,
a2a_client_endpoint: None,
grace_period: None,
max_session: humantime::Duration::from(Duration::from_secs(1)),
readiness_timeout: humantime::Duration::from(Duration::from_secs(1)),
output: None,
header: vec![],
no_semantic: false,
raw_synthesize: false,
metrics_port: None,
events_file: None,
export_trace: None,
progress: ProgressLevel::Off,
context: false,
context_model: None,
context_api_key: None,
context_base_url: None,
context_provider: "openai".to_string(),
context_temperature: None,
context_max_tokens: None,
context_system_prompt: None,
context_timeout: None,
max_turns: None,
}
}
#[tokio::test]
async fn run_scenario_unknown_name_is_usage_error() {
let args = ScenariosRunArgs {
name: "not-a-real-scenario".to_string(),
execution: test_run_args(),
};
let err = run_scenario(
&args,
true,
crate::cli::args::ColorChoice::Never,
CancellationToken::new(),
)
.await
.expect_err("unknown scenario should fail with usage error");
match err {
ThoughtJackError::Usage(msg) => {
assert!(
msg.contains("Unknown scenario"),
"unexpected message: {msg}"
);
}
other => panic!("expected usage error, got {other}"),
}
}
}