mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-17 23:52:34 +00:00
SandboxOptions was an intermediate between the environment's settings and the driver's SandboxSpec that mirrored the spec field for field: image and Dockerfile for the source, cpu and byte sizes for the resources, auto-stop for the timers, plus the two clone fields. Every provider overlay then read the options a second time to fill the spec. The environment now maps onto the driver spec once, in sandbox_spec_for_environment, and the overlays read the spec: Docker takes its image from the source and clears the timers it cannot honor, Daytona takes its snapshot inputs from the source and resources and its auto-stop from the timers. A plugin gets the spec trimmed to the network and timer capabilities it declares. The snapshot carries the resources a Daytona sandbox is sized by, so the overlay clears them from the spec the sandbox is created with; the driver refuses them there, which the options path never reached in a live run. The clone selectors, depth, and skip flag travel as one CloneRequest beside the spec instead of five loose parameters and two option fields, so provider_sandbox takes six arguments instead of nine. The two helpers that read environment settings for a local run, its working directory and its unresolved variables, become methods on RunEnvironmentSettings in fabro-types, where the settings live. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
97 lines
3.4 KiB
Rust
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::tool_registry::ToolContext;
|
|
use fabro_agent::tools::make_shell_tool;
|
|
use fabro_agent::types::AgentEvent;
|
|
use fabro_agent::{
|
|
CloneRequest, DriverSpec, Emitter, ProviderAccess, SandboxProviderKind, SandboxSource,
|
|
provider_sandbox,
|
|
};
|
|
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) = provider_sandbox(
|
|
SandboxProviderKind::DOCKER,
|
|
&ProviderAccess::default(),
|
|
DriverSpec::new(SandboxSource::Image {
|
|
reference: "buildpack-deps:noble".to_string(),
|
|
}),
|
|
&CloneRequest::none(),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
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(),
|
|
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:?}"),
|
|
}
|
|
}
|