use std::sync::Arc;
use std::time::Duration;
use hotl_engine::{spawn_session, EngineConfig, EngineEvent, Outcome, SessionDeps, SessionHandle};
use hotl_platform::SystemClock;
use hotl_provider::{Provider, ScriptedProvider};
use hotl_store::{Masker, SessionLog};
use hotl_tools::{rules::Rules, Registry};
use serde_json::json;
struct Session {
handle: SessionHandle,
#[allow(dead_code)]
dir: tempfile::TempDir,
}
fn session_with(
provider: Arc<dyn Provider>,
registry: Arc<Registry>,
config: EngineConfig,
) -> Session {
let dir = tempfile::tempdir().expect("tempdir");
let log = SessionLog::create(dir.path(), &config.model, None, Masker::empty(), 0)
.expect("session log");
let handle = spawn_session(SessionDeps {
provider,
registry,
rules: Arc::new(Rules::default()),
sandbox_enforced: false,
clock: Arc::new(SystemClock),
log,
system: "test-system".into(),
cwd: dir.path().to_path_buf(),
snapshots: None,
hooks: None,
initial_items: Vec::new(),
initial_todos: Vec::new(),
config,
});
Session { handle, dir }
}
async fn next_event(s: &mut Session) -> EngineEvent {
tokio::time::timeout(Duration::from_secs(30), s.handle.events.recv())
.await
.expect("event timeout")
.expect("event channel closed")
}
async fn wait_outcome(s: &mut Session) -> Outcome {
loop {
if let EngineEvent::TurnDone { outcome, .. } = next_event(s).await {
return outcome;
}
}
}
struct PanicTool;
impl hotl_tools::Tool for PanicTool {
fn name(&self) -> &'static str {
"boom"
}
fn description(&self) -> &str {
"test-only: panics"
}
fn schema(&self) -> serde_json::Value {
json!({"type": "object", "properties": {}})
}
fn permission(&self, _input: &serde_json::Value) -> hotl_tools::Permission {
hotl_tools::Permission::None
}
fn run<'a>(
&'a self,
_input: serde_json::Value,
_cancel: tokio_util::sync::CancellationToken,
) -> futures_util::future::BoxFuture<'a, hotl_tools::ToolOutcome> {
Box::pin(async { panic!("tool panicked on purpose") })
}
}
#[tokio::test]
async fn a_panicking_turn_reports_an_error_and_the_session_keeps_working() {
let mut registry = Registry::builtin();
registry.register(Box::new(PanicTool));
let provider = Arc::new(ScriptedProvider::new(vec![
ScriptedProvider::tool_call("t1", "boom", json!({})),
ScriptedProvider::text_reply("still alive"),
]));
let mut s = session_with(provider, Arc::new(registry), EngineConfig::default());
s.handle.prompt("trip the panic".into()).await;
let first = wait_outcome(&mut s).await;
assert!(
matches!(first, Outcome::Error { .. }),
"a panicked turn must report an error outcome, got {first:?}"
);
s.handle.prompt("are you there".into()).await;
let second = wait_outcome(&mut s).await;
assert_eq!(
second,
Outcome::Done {
text: "still alive".into()
}
);
}