fabro/lib/crates/fabro-sandbox/tests/docker_streaming.rs
Bryan Helmkamp c0fe29390a
feat(sandbox): prepare clone layout for multi-repo runs (#250)
## Summary

- Clone primary GitHub repos into provider-owned `/repos/{owner}/{repo}`
paths for Docker and Daytona sandboxes.
- Keep user/agent execution rooted at the workspace symlink, e.g.
`/workspace/{repo}` or `/home/daytona/workspace/{repo}`.
- Persist optional runtime layout metadata (`workspace_root`,
`repos_root`, `primary_repo_path`, `primary_repo_link`) through events,
projections, OpenAPI, Rust API tests, and the TS client.
- Preserve empty workspace behavior and reconnect from stored
`working_directory` for existing run records.

## Verification

- `cargo nextest run -p fabro-sandbox --features docker,daytona`
- `cargo nextest run -p fabro-workflow`
- `cargo nextest run -p fabro-server`
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-api run_sandbox_json_matches_openapi_shape
sandbox_details_json_matches_openapi_shape`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `git diff --check`

## Notes

- Added ignored live smoke tests for Docker and Daytona layout
validation; they require real provider credentials/runtime.
2026-05-14 09:38:20 -04:00

149 lines
4.4 KiB
Rust

#![cfg(feature = "docker")]
use std::sync::Arc;
use bollard::Docker;
use fabro_sandbox::{CommandOutputCallback, DockerSandbox, DockerSandboxOptions, Sandbox};
use tokio::sync::Mutex;
#[tokio::test]
#[ignore = "requires real Docker container lifecycle; run explicitly when changing Docker exec integration"]
async fn streaming_timeout_terminates_docker_exec_before_returning() {
let image = "buildpack-deps:noble";
let Ok(docker) = Docker::connect_with_local_defaults() else {
return;
};
if docker.inspect_image(image).await.is_err() {
return;
}
let sandbox = DockerSandbox::new(
DockerSandboxOptions {
image: image.to_string(),
auto_pull: false,
skip_clone: true,
..DockerSandboxOptions::default()
},
None,
None,
None,
None,
)
.expect("docker sandbox should construct");
sandbox
.initialize()
.await
.expect("docker sandbox should initialize");
let chunks = Arc::new(Mutex::new(Vec::new()));
let callback_chunks = Arc::clone(&chunks);
let callback: CommandOutputCallback = Arc::new(move |_stream, bytes| {
let callback_chunks = Arc::clone(&callback_chunks);
Box::pin(async move {
callback_chunks.lock().await.extend(bytes);
Ok(())
})
});
let marker = "fabro_streaming_timeout_sentinel";
let result = sandbox
.exec_command_streaming(
&format!("trap '' HUP TERM; echo start; sleep 5 # {marker}"),
Some(200),
None,
None,
None,
callback,
)
.await
.expect("streaming command should return a timeout result");
assert!(result.result.is_timed_out());
assert!(
String::from_utf8_lossy(&chunks.lock().await).contains("start"),
"stream should include output emitted before timeout"
);
let probe = sandbox
.exec_command(
"marker='fabro_streaming_timeout_''sentinel'; \
ps -eo pid,args | awk -v marker=\"$marker\" \
'index($0, marker) && $0 !~ /awk/ && $0 !~ /ps -eo/ { print }'",
1_000,
None,
None,
None,
)
.await
.expect("process probe should run");
sandbox
.cleanup()
.await
.expect("docker cleanup should succeed");
assert!(
!probe.stdout.contains(marker),
"timed-out docker exec should be terminated before returning, found: {}",
probe.stdout
);
}
#[tokio::test]
#[ignore = "requires real Docker container lifecycle, image, network, and a public GitHub clone"]
async fn cloned_docker_sandbox_uses_repos_checkout_and_workspace_symlink() {
let image = "buildpack-deps:noble";
let Ok(docker) = Docker::connect_with_local_defaults() else {
return;
};
if docker.inspect_image(image).await.is_err() {
return;
}
let sandbox = DockerSandbox::new(
DockerSandboxOptions {
image: image.to_string(),
auto_pull: false,
skip_clone: false,
..DockerSandboxOptions::default()
},
None,
None,
Some("https://github.com/brynary/rack-test".to_string()),
None,
)
.expect("docker sandbox should construct");
sandbox
.initialize()
.await
.expect("docker sandbox should initialize");
assert_eq!(sandbox.working_directory(), "/workspace/rack-test");
let result = sandbox
.exec_command(
"test -d /repos/brynary/rack-test/.git && \
test -L /workspace/rack-test && \
test \"$(readlink /workspace/rack-test)\" = /repos/brynary/rack-test && \
test \"$(git -C /repos/brynary/rack-test rev-parse HEAD)\" = \
\"$(git -C /workspace/rack-test rev-parse HEAD)\" && \
git rev-parse --is-inside-work-tree",
10_000,
None,
None,
None,
)
.await
.expect("layout verification command should run");
sandbox
.cleanup()
.await
.expect("docker cleanup should succeed");
assert!(
result.is_success(),
"layout verification failed: stdout={} stderr={}",
result.stdout,
result.stderr
);
assert!(result.stdout.contains("true"));
}