mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
## 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.
149 lines
4.4 KiB
Rust
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"));
|
|
}
|