fabro/lib/components/fabro-agent/tests/it/docker_shell.rs
2026-08-26 11:16:37 -04:00

97 lines
3.4 KiB
Rust

//! Proves the agent shell tool reports real process outcomes through the
//! Docker provider's streaming path, which uses a `bash -lc` supervisor and
//! separate stdout/stderr channels.
use std::sync::Arc;
use fabro_agent::event::SessionBoundEmitter;
use fabro_agent::sandbox::Sandbox;
use fabro_agent::tool_registry::ToolContext;
use fabro_agent::tools::make_shell_tool;
use fabro_agent::types::AgentEvent;
use fabro_agent::{DockerSandbox, DockerSandboxOptions, Emitter};
use fabro_types::CommandTermination;
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
#[tokio::test]
#[ignore = "requires real Docker container lifecycle; run explicitly when changing shell tool exec integration"]
async fn shell_reports_real_docker_process_outcome() {
let Ok(sandbox) = DockerSandbox::new(
DockerSandboxOptions {
image: "buildpack-deps:noble".to_string(),
auto_pull: false,
skip_clone: true,
..DockerSandboxOptions::default()
},
None,
None,
None,
None,
None,
None,
) else {
return;
};
// No Docker daemon or no local image: the integration precondition is not met.
if sandbox.initialize().await.is_err() {
return;
}
let sandbox = Arc::new(sandbox);
let emitter = Emitter::new();
let mut receiver = emitter.subscribe();
let tool = make_shell_tool();
let result = (tool.executor)(
serde_json::json!({"command": "printf 'out'; printf 'err' >&2; exit 7"}),
ToolContext {
env: sandbox.clone() as Arc<dyn Sandbox>,
cancel: CancellationToken::new(),
tool_env_provider: None,
session_id: Some("test-session".to_string()),
root_session_id: Some("test-session".to_string()),
tool_call_id: Some("call_1".to_string()),
agent_event_emitter: Some(Arc::new(SessionBoundEmitter::new(
emitter.clone(),
"test-session".to_string(),
Some("call_1".to_string()),
))),
},
)
.await;
sandbox
.cleanup()
.await
.expect("docker cleanup should succeed");
let output = result.expect_err("exit 7 is a failed tool result");
assert!(output.contains("Termination: exited"), "got: {output}");
assert!(output.contains("Exit code: 7"), "got: {output}");
assert!(output.contains("stdout:\nout"), "got: {output}");
assert!(output.contains("stderr:\nerr"), "got: {output}");
let event = receiver.try_recv().expect("one process event");
assert_eq!(event.session_id, "test-session");
assert_eq!(event.tool_call_id.as_deref(), Some("call_1"));
assert!(matches!(
receiver.try_recv(),
Err(broadcast::error::TryRecvError::Empty)
));
match event.event {
AgentEvent::ToolProcessCompleted {
exit_code,
termination,
streams_separated,
exec_output_tail,
..
} => {
assert_eq!(exit_code, Some(7));
assert_eq!(termination, CommandTermination::Exited);
assert!(streams_separated);
let tail = exec_output_tail.expect("output tail");
assert_eq!(tail.stdout.as_deref(), Some("out"));
assert_eq!(tail.stderr.as_deref(), Some("err"));
}
other => panic!("expected a process event, got {other:?}"),
}
}