use everruns_core::atoms::PostToolExecHook;
use everruns_core::capabilities::{BashTool, DistillOutputHook, PersistOutputHook};
use everruns_core::tool_types::{
BuiltinTool, DeferrablePolicy, ToolCall, ToolDefinition, ToolHints, ToolPolicy, ToolResult,
};
use everruns_core::tools::{Tool, ToolExecutionResult};
use everruns_core::traits::ToolContext;
use everruns_core::{MountFs, SessionFileSystem, SessionId, WorkspaceRootSet};
use everruns_runtime::{InMemorySessionFileStore, RealDiskFileStore, multi_root_file_system};
use serde_json::json;
use std::sync::Arc;
use tempfile::TempDir;
async fn bash_pwd(ctx: &ToolContext) -> String {
match BashTool::default()
.execute_with_context(json!({ "commands": "pwd" }), ctx)
.await
{
ToolExecutionResult::Success(output) => output["stdout"]
.as_str()
.expect("pwd stdout is a string")
.trim()
.to_string(),
other => panic!("bash pwd did not succeed: {other:?}"),
}
}
fn output_tool_def(persist_output: bool) -> ToolDefinition {
let hints = if persist_output {
ToolHints::default().with_persist_output(true)
} else {
ToolHints::default()
};
ToolDefinition::Builtin(BuiltinTool {
name: "test_output".to_string(),
display_name: None,
description: "test output".to_string(),
parameters: json!({}),
policy: ToolPolicy::Auto,
category: None,
deferrable: DeferrablePolicy::default(),
hints,
full_parameters: None,
})
}
fn output_tool_call() -> ToolCall {
ToolCall {
id: "call_paths".to_string(),
name: "test_output".to_string(),
arguments: json!({}),
}
}
async fn assert_output_pointers_follow_store(store: Arc<dyn SessionFileSystem>) {
let session = SessionId::from_seed(662);
let context = ToolContext::with_file_store(session, store.clone());
let root = store.display_root();
let stdout_path = format!("{root}/outputs/call_paths.stdout");
let stderr_path = format!("{root}/outputs/call_paths.stderr");
let mut result = ToolResult {
tool_call_id: "call_paths".to_string(),
result: Some(json!({
"stdout": "stdout body",
"stderr": "stderr body",
"exit_code": 0,
})),
images: None,
error: None,
connection_required: None,
raw_output: Some("stdout body\n--- stderr ---\nstderr body".to_string()),
};
PersistOutputHook
.after_exec(
&output_tool_call(),
&output_tool_def(true),
&mut result,
&context,
)
.await;
let value = result.result.as_ref().unwrap();
assert_eq!(value["full_output"], json!(stdout_path));
assert_eq!(value["output_files"], json!([stdout_path, stderr_path]));
for pointer in value["output_files"].as_array().unwrap() {
assert!(
store
.read_file(session, pointer.as_str().unwrap())
.await
.unwrap()
.is_some(),
"model-visible output pointer must be readable by file tools"
);
}
assert!(
value["stdout"]
.as_str()
.unwrap()
.contains(&store.display_path("/outputs/call_paths.stdout"))
);
}
async fn assert_distillation_pointer_follows_store(store: Arc<dyn SessionFileSystem>) {
let session = SessionId::from_seed(663);
let context = ToolContext::with_file_store(session, store.clone());
let rows: Vec<_> = (0..2000)
.map(|i| json!({"id": i, "name": format!("row-{i}")}))
.collect();
let mut result = ToolResult {
tool_call_id: "call_paths".to_string(),
result: Some(json!({"rows": rows})),
images: None,
error: None,
connection_required: None,
raw_output: None,
};
DistillOutputHook
.after_exec(
&output_tool_call(),
&output_tool_def(false),
&mut result,
&context,
)
.await;
let expected = store.display_path("/outputs/call_paths.stdout");
let value = result.result.as_ref().unwrap();
assert_eq!(value["full_output"], json!(expected));
assert_eq!(value["output_files"], json!([expected]));
assert!(value["distill_note"].as_str().unwrap().contains(&expected));
assert!(store.read_file(session, &expected).await.unwrap().is_some());
}
#[tokio::test]
async fn persisted_output_paths_follow_real_disk_mount_and_vfs_identity() {
let direct_root = TempDir::new().unwrap();
let direct: Arc<dyn SessionFileSystem> =
Arc::new(RealDiskFileStore::new(direct_root.path()).unwrap());
assert_output_pointers_follow_store(direct.clone()).await;
assert_distillation_pointer_follows_store(direct).await;
let mounted_root = TempDir::new().unwrap();
let mounted = MountFs::wrap(Arc::new(
RealDiskFileStore::new(mounted_root.path()).unwrap(),
));
assert_output_pointers_follow_store(mounted.clone()).await;
assert_distillation_pointer_follows_store(mounted).await;
let memory: Arc<dyn SessionFileSystem> = Arc::new(InMemorySessionFileStore::new());
assert_eq!(memory.display_root(), "/workspace");
assert_output_pointers_follow_store(memory.clone()).await;
assert_distillation_pointer_follows_store(memory).await;
}
#[tokio::test]
async fn all_surfaces_agree_on_root_across_worktree_switch() {
let session = SessionId::from_seed(660);
let worktree_a = TempDir::new().unwrap();
let backend = Arc::new(RealDiskFileStore::new(worktree_a.path()).unwrap());
let store: Arc<dyn SessionFileSystem> = MountFs::wrap(backend.clone());
let ctx = ToolContext::with_file_store(session, store.clone());
let root_a = backend.root();
store
.write_file(session, "/workspace/marker.txt", "ALPHA", "text")
.await
.unwrap();
let read = store
.read_file(session, "/workspace/marker.txt")
.await
.unwrap()
.unwrap();
assert_eq!(read.content.as_deref(), Some("ALPHA"));
assert_eq!(read.path, "/marker.txt");
let direct = backend
.read_file(session, "/marker.txt")
.await
.unwrap()
.unwrap();
assert_eq!(
read.path, direct.path,
"MountFs preserves primary result paths"
);
let relative = store
.read_file(session, "marker.txt")
.await
.unwrap()
.unwrap();
assert_eq!(relative.content.as_deref(), Some("ALPHA"));
let hits = store.grep_files(session, "ALPHA", None).await.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].path, "/marker.txt");
assert_eq!(
std::fs::read_to_string(backend.root().join("marker.txt")).unwrap(),
"ALPHA"
);
assert_eq!(store.display_root(), root_a.display().to_string());
assert_eq!(
store.display_path("/marker.txt"),
root_a.join("marker.txt").display().to_string()
);
assert_eq!(
store.resolve_path("marker.txt"),
root_a.join("marker.txt").display().to_string()
);
assert_eq!(bash_pwd(&ctx).await, root_a.display().to_string());
let worktree_b = TempDir::new().unwrap();
backend.set_host_root(worktree_b.path()).unwrap();
let root_b = backend.root();
assert_ne!(root_a, root_b);
store
.write_file(session, "/marker.txt", "BETA", "text")
.await
.unwrap();
let read_b = store
.read_file(session, "/workspace/marker.txt")
.await
.unwrap()
.unwrap();
assert_eq!(read_b.content.as_deref(), Some("BETA"));
let hits_b = store.grep_files(session, "BETA", None).await.unwrap();
assert_eq!(hits_b.len(), 1);
assert_eq!(hits_b[0].path, "/marker.txt");
assert_eq!(
std::fs::read_to_string(root_b.join("marker.txt")).unwrap(),
"BETA"
);
assert_eq!(
std::fs::read_to_string(root_a.join("marker.txt")).unwrap(),
"ALPHA",
"worktree A is untouched by writes after the switch"
);
assert_eq!(store.display_root(), root_b.display().to_string());
assert_eq!(
store.display_path("/marker.txt"),
root_b.join("marker.txt").display().to_string()
);
assert_eq!(bash_pwd(&ctx).await, root_b.display().to_string());
}
#[tokio::test]
async fn multi_root_display_and_containment_contract() {
let session = SessionId::from_seed(661);
let primary = TempDir::new().unwrap();
let secondary = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let roots = WorkspaceRootSet::new(
primary.path(),
[("backend".to_string(), secondary.path().to_path_buf())],
)
.unwrap();
let store = multi_root_file_system(&roots).unwrap();
assert_eq!(
store.display_root(),
roots.primary_host_root().display().to_string()
);
assert_eq!(
store.display_path("/src/lib.rs"),
roots
.primary_host_root()
.join("src/lib.rs")
.display()
.to_string()
);
let primary_file = store
.write_file(session, "src/lib.rs", "primary", "text")
.await
.unwrap();
assert_eq!(primary_file.path, "/src/lib.rs");
assert_eq!(
std::fs::read_to_string(primary.path().join("src/lib.rs")).unwrap(),
"primary"
);
let secondary_file = store
.write_file(
session,
"/workspace/roots/backend/Cargo.toml",
"secondary",
"text",
)
.await
.unwrap();
assert_eq!(secondary_file.path, "/workspace/roots/backend/Cargo.toml");
assert_eq!(
store.display_path(&secondary_file.path),
"/workspace/roots/backend/Cargo.toml"
);
assert_eq!(
std::fs::read_to_string(secondary.path().join("Cargo.toml")).unwrap(),
"secondary"
);
let traversal = store
.read_file(session, "/workspace/roots/backend/../escape")
.await
.unwrap_err();
assert!(traversal.to_string().contains("path traversal rejected"));
let outside_path = outside.path().join("secret.txt");
std::fs::write(&outside_path, "secret").unwrap();
let containment = store
.read_file(session, outside_path.to_str().unwrap())
.await
.unwrap();
assert!(
containment.is_none(),
"an absolute path outside every registered root cannot expose that host file"
);
}