#![cfg(feature = "e2e")]
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use outrig::McpClient;
use outrig::config::{ImageConfig, McpServerSpec};
use outrig::container::{Container, ContainerLaunchSpec};
use outrig::image::{self, ImageTag};
use outrig_cli::rig_tool::McpToolAdapter;
use rig::tool::ToolDyn;
fn fixture_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("outrig-cli is under crates/")
.join("outrig/tests/fixtures/mcp-fs")
}
fn init_tracing() {
let _ = tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_writer(std::io::stderr)
.with_ansi(false)
.try_init();
}
async fn ensure_fixture_image() -> ImageTag {
let cfg = ImageConfig {
image_name: None,
dockerfile: Some("Dockerfile".into()),
context: Some(".".into()),
build_args: BTreeMap::new(),
security: Default::default(),
mcp: BTreeMap::new(),
};
image::ensure_image(&cfg, &fixture_dir(), false)
.await
.expect("ensure_image for mcp-fs fixture")
.tag
}
async fn start_and_bootstrap(image: &ImageTag, host_ws: &Path) -> Container {
let mut container = Container::start(
image,
ContainerLaunchSpec::workspace(host_ws, Path::new("/workspace")),
)
.await
.expect("Container::start");
container.bootstrap_user().await.expect("bootstrap_user");
container
}
#[tokio::test]
async fn adapter_dispatches_tool_call_into_container() {
init_tracing();
let image = ensure_fixture_image().await;
let host_ws = tempfile::tempdir().expect("tempdir host_ws");
let session_dir = tempfile::tempdir().expect("tempdir session");
let log_dir = session_dir.path().join("logs");
let known_content = "rig-tool-dispatch e2e marker\n";
std::fs::write(host_ws.path().join("HELLO.txt"), known_content).expect("write HELLO.txt");
let container = start_and_bootstrap(&image, host_ws.path()).await;
let spec = McpServerSpec::Short(vec![
"mcp-server-filesystem".to_string(),
"/workspace".to_string(),
]);
let client =
McpClient::connect_via_podman_exec(&container, &spec, "fs", &log_dir, &BTreeMap::new())
.await
.expect("connect_via_podman_exec");
let client = Arc::new(client);
let adapters = McpToolAdapter::from_client_tools(client.clone(), usize::MAX)
.await
.expect("from_client_tools");
assert!(
adapters.len() >= 3,
"filesystem MCP should advertise at least 3 tools, got {}",
adapters.len()
);
let read_file = adapters
.iter()
.find(|a| a.mcp_tool_name == "read_file" || a.mcp_tool_name == "read_text_file")
.expect("filesystem MCP should expose a read_file (or read_text_file) tool");
assert!(
read_file.openai_name == "fs__read_file" || read_file.openai_name == "fs__read_text_file",
"unexpected sanitized name: {}",
read_file.openai_name
);
let args = serde_json::json!({ "path": "/workspace/HELLO.txt" }).to_string();
let body = ToolDyn::call(read_file, args)
.await
.expect("ToolDyn::call should succeed for an existing file");
assert!(
body.contains("rig-tool-dispatch e2e marker"),
"tool output should echo the file content, got: {body}"
);
let bad_args = serde_json::json!({ "path": "/workspace/does-not-exist.txt" }).to_string();
let err = ToolDyn::call(read_file, bad_args)
.await
.expect_err("missing file should surface as a tool-call error");
let msg = err.to_string();
assert!(
!msg.is_empty(),
"tool error should carry a non-empty message"
);
drop(adapters);
let client =
Arc::try_unwrap(client).expect("client Arc should be unique once adapters are dropped");
client.shutdown().await.expect("shutdown");
container.stop(Duration::from_secs(2)).await.expect("stop");
}