use std::sync::Arc;
use agent_engine::config;
use agent_engine::{EngineHost, HostOpts};
async fn host() -> Arc<EngineHost> {
static INIT: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new();
INIT.get_or_init(|| async {
let home = std::env::temp_dir().join(format!("synaps-engine-host-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
config::set_base_dir_for_tests(home);
let h = EngineHost::boot(HostOpts {
profile: None,
no_extensions: true,
})
.await
.expect("host boot");
assert!(EngineHost::install(h).is_ok(), "first install");
})
.await;
EngineHost::current().expect("installed")
}
#[tokio::test]
async fn boot_twice_returns_same_host() {
let a = host().await;
let b = EngineHost::current().unwrap();
assert!(Arc::ptr_eq(&a, &b));
let routing_before = agent_engine::runtime::openai::extension_manager_for_routing().unwrap();
assert!(Arc::ptr_eq(&routing_before, a.ext_manager()));
let other = EngineHost::boot(HostOpts {
profile: None,
no_extensions: true,
})
.await
.unwrap();
let rejected = EngineHost::install(Arc::clone(&other)).expect_err("rejected");
assert!(Arc::ptr_eq(&rejected, &other));
assert!(Arc::ptr_eq(&a, &EngineHost::current().unwrap()));
let routing_after = agent_engine::runtime::openai::extension_manager_for_routing().unwrap();
assert!(Arc::ptr_eq(&routing_after, a.ext_manager()), "static untouched");
assert!(EngineHost::install(Arc::clone(&a)).is_ok());
let again = EngineHost::boot_and_install(HostOpts {
profile: None,
no_extensions: true,
})
.await
.unwrap();
assert!(Arc::ptr_eq(&a, &again));
drop(other); }
#[tokio::test]
async fn foreground_runtime_shares_tools_and_hookbus() {
let h = host().await;
let r1 = h.foreground_runtime().await.unwrap();
let r2 = h.foreground_runtime().await.unwrap();
assert!(Arc::ptr_eq(&r1.tools_shared(), &h.parts().tools));
assert!(Arc::ptr_eq(&r2.tools_shared(), &h.parts().tools));
assert!(Arc::ptr_eq(r1.hook_bus(), &h.parts().hook_bus));
assert!(Arc::ptr_eq(r2.hook_bus(), &h.parts().hook_bus));
assert_ne!(r1.host_tool_session_id(), r2.host_tool_session_id());
assert_eq!(r1.session_id(), None);
}
#[tokio::test]
async fn worker_runtime_fresh_hookbus_and_private_registry() {
let h = host().await;
let w = h.worker_runtime().await.unwrap();
assert!(!Arc::ptr_eq(w.hook_bus(), &h.parts().hook_bus));
assert!(!Arc::ptr_eq(&w.tools_shared(), &h.parts().tools));
let reg = w.tools_shared();
let reg = reg.read().await;
assert!(reg.get("subagent").is_none());
assert!(reg.get("bash").is_some());
}
#[tokio::test]
async fn worker_registry_cached_by_generation() {
let h = host().await;
let a = h.worker_registry().await;
let b = h.worker_registry().await;
assert!(Arc::ptr_eq(&a.tools_schema(), &b.tools_schema()));
assert_eq!(a.catalog().generation(), b.catalog().generation());
let before = h.parts().tools.read().await.catalog().generation();
h.parts()
.tools
.write()
.await
.disable(&["ls".to_string()]);
let after = h.parts().tools.read().await.catalog().generation();
assert!(after > before);
let c = h.worker_registry().await;
assert!(!Arc::ptr_eq(&a.tools_schema(), &c.tools_schema()));
let d = h.worker_registry().await;
assert!(Arc::ptr_eq(&c.tools_schema(), &d.tools_schema()));
}
#[tokio::test]
async fn runtime_new_is_still_fully_fresh() {
let h = host().await;
let r = agent_engine::Runtime::new().await.unwrap();
assert!(!Arc::ptr_eq(&r.tools_shared(), &h.parts().tools));
assert!(!Arc::ptr_eq(r.hook_bus(), &h.parts().hook_bus));
}
#[tokio::test]
async fn extensions_ready_tracks_the_loader() {
let h = host().await;
tokio::time::timeout(std::time::Duration::from_millis(200), h.extensions_ready())
.await
.expect("no loader → immediate");
let guard = h.ext_manager().write().await;
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let loader = agent_engine::extensions::loader::spawn_discover_and_load(
Arc::clone(h.ext_manager()),
tx,
Some("sess-ready".to_string()),
);
let waiter = {
let h = Arc::clone(&h);
tokio::spawn(async move {
h.extensions_ready().await;
std::time::Instant::now()
})
};
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
assert!(!waiter.is_finished(), "waiter must block while discovery is pending");
let released = std::time::Instant::now();
drop(guard);
loader.await.unwrap();
let woke = waiter.await.unwrap();
assert!(woke >= released, "woke only after the loader finished");
assert!(matches!(
rx.recv().await,
Some(agent_engine::extensions::loader::ExtensionLoaderEvent::Started)
));
tokio::time::timeout(std::time::Duration::from_millis(200), h.extensions_ready())
.await
.expect("ready → immediate");
assert!(h.ext_manager().read().await.discovery_done().is_some());
}
#[tokio::test]
async fn extensions_ready_returns_when_loader_panics() {
let _ = host().await; let h = EngineHost::boot(HostOpts {
profile: None,
no_extensions: true,
})
.await
.unwrap();
let guard = h.extensions_loading_guard();
let waiter = {
let h = Arc::clone(&h);
tokio::spawn(async move { h.extensions_ready().await })
};
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(!waiter.is_finished(), "loader dispatched → waiter blocks");
let loader = tokio::spawn(async move {
let _guard = guard;
panic!("discovery blew up");
});
assert!(loader.await.is_err(), "loader panicked");
tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
.await
.expect("waiter released by the guard drop")
.unwrap();
}