use std::sync::Arc;
use std::time::Duration;
use everruns_core::llmsim_driver::{LlmSimConfig, OnExhausted, SimError, SimToolCall, SimTurn};
use serde_json::json;
use crate::approval::ApprovalGate;
use crate::runtime::{BuildOptions, BuiltRuntime, ProviderChoice, build_with_options};
use crate::settings::SettingsStore;
const TURN_TIMEOUT: Duration = Duration::from_secs(15);
async fn build_scripted_runtime(config: LlmSimConfig) -> (BuiltRuntime, std::path::PathBuf) {
let workspace_root = tempfile::tempdir().expect("workspace tempdir").keep();
let sessions_root = tempfile::tempdir().expect("sessions tempdir").keep();
let llmsim = config.with_model("llmsim-yolop");
let settings_path = sessions_root.join("settings.toml");
let settings = Arc::new(SettingsStore::open(settings_path));
let runtime = build_with_options(
workspace_root.clone(),
ProviderChoice::Sim,
ApprovalGate::auto(),
None,
sessions_root,
settings,
BuildOptions {
llmsim_override: Some(llmsim),
},
)
.await
.expect("build scripted llmsim runtime");
(runtime, workspace_root)
}
async fn run_single_turn(runtime: &BuiltRuntime, user_text: &str) -> everruns_runtime::TurnResult {
let session_id = runtime.handles.session_id;
let input = runtime.model.input_message(user_text.to_string());
tokio::time::timeout(
TURN_TIMEOUT,
runtime.handles.runtime.run_turn(session_id, input),
)
.await
.expect("run_turn timed out")
.expect("run_turn errored")
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn scripted_assistant_text_returns_success_with_no_tools() {
let (runtime, _ws) = build_scripted_runtime(LlmSimConfig::scripted(vec![SimTurn::Assistant(
"hello from scripted llmsim".to_string(),
)]))
.await;
let result = run_single_turn(&runtime, "ping").await;
assert!(
result.success,
"scripted assistant turn must succeed: {result:?}"
);
assert_eq!(
result.tool_calls_count, 0,
"Assistant-only turn must not run tools"
);
assert!(
result.iterations >= 1,
"agent loop must record at least one iteration: {result:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn scripted_tool_call_executes_bash_then_assistant_completes() {
let marker = "scripted_bash_ran.marker";
let cmd = format!("touch {marker}");
let (runtime, workspace) = build_scripted_runtime(LlmSimConfig::scripted(vec![
SimTurn::ToolCalls(vec![SimToolCall {
name: "bash".to_string(),
arguments: json!({ "command": cmd }),
id: None,
}]),
SimTurn::Assistant("did the bash".to_string()),
]))
.await;
let result = run_single_turn(&runtime, "do the thing").await;
assert!(
result.success,
"tool-call + assistant turn must succeed: {result:?}"
);
assert_eq!(
result.tool_calls_count, 1,
"exactly one bash invocation expected"
);
assert!(
workspace.join(marker).exists(),
"BashTool must have run the scripted command and created {marker:?} in {workspace:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn scripted_error_turn_marks_run_failed() {
let (runtime, _ws) = build_scripted_runtime(LlmSimConfig::scripted(vec![SimTurn::Error(
SimError::Other("scripted llmsim refused the request".to_string()),
)]))
.await;
let result = run_single_turn(&runtime, "anything").await;
assert!(
!result.success,
"scripted error turn must surface as failure"
);
let error = result
.error
.as_deref()
.expect("failed run_turn must carry an error message");
assert!(
error.contains("scripted llmsim refused"),
"the scripted error message must reach the caller, got: {error}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn scripted_default_repeat_last_lets_second_call_succeed_after_exhaustion() {
let (runtime, _ws) = build_scripted_runtime(LlmSimConfig::scripted(vec![SimTurn::Assistant(
"only turn".to_string(),
)]))
.await;
let first = run_single_turn(&runtime, "first").await;
let second = run_single_turn(&runtime, "second").await;
assert!(first.success, "first run_turn must succeed");
assert!(
second.success,
"RepeatLast must let a second run_turn succeed past exhaustion: {second:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn scripted_on_exhausted_error_fails_second_call() {
let (runtime, _ws) = build_scripted_runtime(
LlmSimConfig::scripted(vec![SimTurn::Assistant("once".to_string())])
.with_on_exhausted(OnExhausted::Error),
)
.await;
let first = run_single_turn(&runtime, "first").await;
assert!(
first.success,
"first run_turn before exhaustion must succeed"
);
let second = run_single_turn(&runtime, "second").await;
assert!(
!second.success,
"OnExhausted::Error must surface as a failed turn after the script is consumed"
);
}