mod session_actor_common;
use session_actor_common::*;
use std::path::Path;
use std::sync::{Arc, Mutex};
use agent_engine::extensions::hooks::events::{hook_cwd, HookEvent, HookResult};
use agent_engine::extensions::runtime::process::RegisteredExtensionToolSpec;
use agent_engine::extensions::runtime::ExtensionHandler;
use agent_engine::memory_backend::MemoryBinding;
use agent_engine::session::{
ClientKind, ClientMeta, ClientTransport, LocalTransport, SessionCommand, SessionConfig,
SessionEventWire,
};
use agent_engine::{EngineHost, Tool, ToolContext};
use serde_json::{json, Value};
use serial_test::serial;
#[derive(Default)]
struct Calls(Mutex<Vec<(Option<String>, Option<String>)>>);
#[async_trait::async_trait]
impl ExtensionHandler for Calls {
fn id(&self) -> &str {
"where"
}
async fn handle(&self, _event: &HookEvent) -> HookResult {
HookResult::Continue
}
async fn call_tool_for(
&self,
_name: &str,
_input: Value,
session_id: Option<&str>,
cwd: Option<&Path>,
) -> Result<Value, String> {
self.0
.lock()
.unwrap()
.push((session_id.map(str::to_string), hook_cwd(cwd)));
Ok(json!("here"))
}
async fn shutdown(&self) {}
}
#[derive(Default)]
struct Project(Mutex<Vec<Option<String>>>);
#[async_trait::async_trait]
impl Tool for Project {
fn name(&self) -> &str {
"where:project"
}
fn description(&self) -> &str {
"records the project of the memory binding it is given"
}
fn parameters(&self) -> Value {
json!({"type": "object"})
}
async fn execute(&self, _params: Value, ctx: ToolContext) -> agent_engine::Result<String> {
let project = ctx
.capabilities
.memory_backend
.as_ref()
.and_then(|binding| binding.scope().ok())
.map(|scope| scope.key().to_string());
self.0.lock().unwrap().push(project);
Ok("recorded".to_string())
}
}
const WHOSE: &str = "SYNAPS_TEST_WHOSE_COMMAND";
const HOST_ONLY: &str = "SYNAPS_TEST_HOST_ONLY";
fn print_both() -> Value {
if cfg!(windows) {
json!({"name": "env", "command": "cmd", "args": ["/C", format!("echo %{WHOSE}% %{HOST_ONLY}%")]})
} else {
let script = format!("printf '%s %s' \"${WHOSE}\" \"${HOST_ONLY}\"");
json!({"name": "env", "command": "sh", "args": ["-c", script]})
}
}
fn session_env(whose: &str) -> Vec<(String, String)> {
let mut env = vec![(WHOSE.to_string(), whose.to_string())];
for key in ["PATH", "SystemRoot", "ComSpec", "PATHEXT"] {
if let Ok(value) = std::env::var(key) {
env.push((key.to_string(), value));
}
}
env
}
async fn host_with_plugin_commands(
) -> (Arc<EngineHost>, Arc<Calls>, Arc<Project>, tempfile::TempDir) {
let host = host().await;
assert!(
EngineHost::install(Arc::clone(&host)).is_ok(),
"this binary installs one host"
);
let calls = Arc::new(Calls::default());
let tool = agent_engine::tools::ExtensionTool::new(
"where",
RegisteredExtensionToolSpec {
name: "ami".into(),
description: "records the caller".into(),
input_schema: json!({"type": "object"}),
},
calls.clone(),
);
let project = Arc::new(Project::default());
{
let mut tools = host.parts().tools.write().await;
tools.register(Arc::new(tool));
tools.register(project.clone());
}
let plugin_root = tempfile::tempdir().unwrap();
host.command_registry().rebuild_with_plugins(
vec![],
vec![agent_engine::skills::Plugin {
name: "where".into(),
root: plugin_root.path().to_path_buf(),
marketplace: None,
version: None,
description: None,
extension: None,
manifest: Some(
serde_json::from_value(json!({
"name": "where",
"commands": [
{"name": "ami", "tool": "ami", "input": {}},
{"name": "project", "tool": "project", "input": {}},
print_both(),
],
}))
.unwrap(),
),
}],
);
(host, calls, project, plugin_root)
}
async fn run(transport: &mut LocalTransport, query: u64, name: &str) -> String {
transport
.send(SessionCommand::PluginCommand {
id: query,
plugin: "where".into(),
name: name.into(),
arg: String::new(),
})
.await
.unwrap();
let seen = until(
transport,
|e| matches!(e, SessionEventWire::QueryResult { id, .. } if *id == query),
)
.await;
let Some(SessionEventWire::QueryResult { value, .. }) = seen.last().map(|e| &e.event) else {
unreachable!()
};
assert_eq!(value["kind"], "plugin_output", "/where:{name}: {value}");
assert_eq!(value["status"], 0, "/where:{name}: {value}");
value["stdout"].as_str().unwrap().to_string()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn a_chats_plugin_commands_get_that_chats_id_folder_binding_and_environment_not_the_hosts() {
let _h = Home::new();
let (url, _) = stub(SSE_HI, false).await;
std::env::set_var("SYNAPS_ANTHROPIC_BASE_URL", &url);
std::env::set_var(WHOSE, "the-host");
std::env::set_var(HOST_ONLY, "host-only-value");
let (host, calls, project, _plugin_root) = host_with_plugin_commands().await;
let (dir_a, dir_b) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
let host_dir = std::env::current_dir().unwrap();
assert!(dir_a.path() != host_dir && dir_b.path() != host_dir && dir_a.path() != dir_b.path());
let mut sessions = Vec::new();
for (dir, whose) in [(dir_a.path(), "chat-A"), (dir_b.path(), "chat-B")] {
let handle = host
.create_session(SessionConfig {
cwd: Some(dir.to_path_buf()),
env: Some(session_env(whose)),
env_stripped: vec![HOST_ONLY.to_string()],
..cfg()
})
.await
.unwrap();
let (transport, _) =
LocalTransport::attach(handle.clone(), ClientMeta::new(ClientKind::Test))
.await
.unwrap();
sessions.push((handle, transport, dir, whose));
}
let project_of = |dir: &Path| {
MemoryBinding::for_cwd(Some(dir.to_path_buf()))
.scope()
.unwrap()
.key()
.to_string()
};
let hosts_project = project_of(&host_dir);
let (mut callers, mut projects) = (Vec::new(), Vec::new());
for (n, (handle, transport, dir, whose)) in sessions.iter_mut().enumerate() {
let query = n as u64 * 10;
assert_eq!(run(transport, query + 1, "ami").await, "here");
callers.push((
Some(handle.id.as_str().to_string()),
dir.to_str().map(str::to_string),
));
assert_eq!(run(transport, query + 2, "project").await, "recorded");
assert_ne!(project_of(dir), hosts_project);
projects.push(Some(project_of(dir)));
let printed = run(transport, query + 3, "env").await;
assert!(printed.contains(*whose), "{whose}: {printed:?}");
assert!(!printed.contains("the-host"), "{whose}: {printed:?}");
assert!(!printed.contains("host-only-value"), "{whose}: {printed:?}");
}
std::env::remove_var(WHOSE);
std::env::remove_var(HOST_ONLY);
let calls = calls.0.lock().unwrap().clone();
assert_eq!(calls, callers, "host folder is {host_dir:?}");
assert_ne!(projects[0], projects[1]);
let given = project.0.lock().unwrap().clone();
assert_eq!(given, projects, "the host's project is {hosts_project}");
for (_, transport, _, _) in sessions.iter_mut() {
end(transport).await;
}
}