mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Add arc cp e2e tests and preserve worktrees for post-run access
- E2e tests for local, Docker, and Daytona sandbox backends - Make cp::reconnect pub for integration test access - Stop deleting worktrees after run completion so arc cp can access local/Docker sandbox files (prune step to come later) - Rustfmt cleanups Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aae1efab98
commit
e9ff7f3274
9 changed files with 449 additions and 41 deletions
|
|
@ -956,4 +956,4 @@ mod tests {
|
|||
|
||||
std::fs::remove_dir_all(&host_dir).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,7 +164,9 @@ impl ExeSandbox {
|
|||
rg_available: tokio::sync::OnceCell::const_new(),
|
||||
event_callback: None,
|
||||
data_ssh_factory: Box::new(|_: &str| {
|
||||
Box::pin(async { Err("from_existing sandbox cannot create new SSH connections".to_string()) })
|
||||
Box::pin(async {
|
||||
Err("from_existing sandbox cannot create new SSH connections".to_string())
|
||||
})
|
||||
}),
|
||||
config: ExeConfig::default(),
|
||||
clone_params: None,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ fn parse_direction(src: &str, dst: &str) -> Result<CopyDirection> {
|
|||
run_prefix: run_prefix.to_string(),
|
||||
remote_path: remote_path.to_string(),
|
||||
}),
|
||||
(Some(_), Some(_)) => bail!("Cannot copy between two sandboxes; one argument must be a local path"),
|
||||
(Some(_), Some(_)) => {
|
||||
bail!("Cannot copy between two sandboxes; one argument must be a local path")
|
||||
}
|
||||
(None, None) => bail!("One argument must contain a run-id prefix (e.g. <run-id>:<path>)"),
|
||||
}
|
||||
}
|
||||
|
|
@ -104,9 +106,7 @@ fn find_run_by_prefix(base: &Path, prefix: &str) -> Result<PathBuf> {
|
|||
/// Returns a sandbox that can perform file operations.
|
||||
/// Note: for Docker and Local sandboxes, the container/directory may still
|
||||
/// need to be alive. For Daytona and Exe, we reconnect via their APIs.
|
||||
async fn reconnect(
|
||||
record: &SandboxRecord,
|
||||
) -> Result<Box<dyn arc_agent::sandbox::Sandbox>> {
|
||||
pub async fn reconnect(record: &SandboxRecord) -> Result<Box<dyn arc_agent::sandbox::Sandbox>> {
|
||||
debug!(
|
||||
provider = %record.provider,
|
||||
identifier = record.identifier.as_deref().unwrap_or(""),
|
||||
|
|
@ -115,8 +115,9 @@ async fn reconnect(
|
|||
|
||||
match record.provider.as_str() {
|
||||
"local" => {
|
||||
let sandbox =
|
||||
arc_agent::local_sandbox::LocalSandbox::new(PathBuf::from(&record.working_directory));
|
||||
let sandbox = arc_agent::local_sandbox::LocalSandbox::new(PathBuf::from(
|
||||
&record.working_directory,
|
||||
));
|
||||
Ok(Box::new(sandbox))
|
||||
}
|
||||
"docker" => {
|
||||
|
|
@ -153,15 +154,12 @@ async fn reconnect(
|
|||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create Daytona client: {e}"))?;
|
||||
|
||||
let sdk_sandbox = client
|
||||
.get(name)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to reconnect to Daytona sandbox '{name}': {e}"))?;
|
||||
let sdk_sandbox = client.get(name).await.map_err(|e| {
|
||||
anyhow::anyhow!("Failed to reconnect to Daytona sandbox '{name}': {e}")
|
||||
})?;
|
||||
|
||||
let sandbox = crate::daytona_sandbox::DaytonaSandbox::from_existing(
|
||||
client,
|
||||
sdk_sandbox,
|
||||
);
|
||||
let sandbox =
|
||||
crate::daytona_sandbox::DaytonaSandbox::from_existing(client, sdk_sandbox);
|
||||
Ok(Box::new(sandbox))
|
||||
}
|
||||
"exe" => {
|
||||
|
|
@ -172,7 +170,9 @@ async fn reconnect(
|
|||
|
||||
let data_ssh = arc_exe::OpensshRunner::connect(data_host)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to connect to exe sandbox '{data_host}': {e}"))?;
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!("Failed to connect to exe sandbox '{data_host}': {e}")
|
||||
})?;
|
||||
|
||||
let sandbox = arc_exe::ExeSandbox::from_existing(Box::new(data_ssh));
|
||||
Ok(Box::new(sandbox))
|
||||
|
|
@ -194,8 +194,9 @@ pub async fn cp_command(args: CpArgs) -> Result<()> {
|
|||
let run_dir = find_run_by_prefix(&base, &run_prefix)?;
|
||||
let sandbox_json = run_dir.join("sandbox.json");
|
||||
debug!(path = %sandbox_json.display(), "Loading sandbox record");
|
||||
let record = SandboxRecord::load(&sandbox_json)
|
||||
.context("Failed to load sandbox.json — was this run started with a recent version of arc?")?;
|
||||
let record = SandboxRecord::load(&sandbox_json).context(
|
||||
"Failed to load sandbox.json — was this run started with a recent version of arc?",
|
||||
)?;
|
||||
|
||||
info!(run_id = %run_prefix, provider = %record.provider, "Connecting to sandbox");
|
||||
let sandbox = reconnect(&record).await?;
|
||||
|
|
@ -219,8 +220,9 @@ pub async fn cp_command(args: CpArgs) -> Result<()> {
|
|||
let run_dir = find_run_by_prefix(&base, &run_prefix)?;
|
||||
let sandbox_json = run_dir.join("sandbox.json");
|
||||
debug!(path = %sandbox_json.display(), "Loading sandbox record");
|
||||
let record = SandboxRecord::load(&sandbox_json)
|
||||
.context("Failed to load sandbox.json — was this run started with a recent version of arc?")?;
|
||||
let record = SandboxRecord::load(&sandbox_json).context(
|
||||
"Failed to load sandbox.json — was this run started with a recent version of arc?",
|
||||
)?;
|
||||
|
||||
info!(run_id = %run_prefix, provider = %record.provider, "Connecting to sandbox");
|
||||
let sandbox = reconnect(&record).await?;
|
||||
|
|
@ -278,7 +280,8 @@ async fn upload_recursive(
|
|||
remote_path: &str,
|
||||
) -> Result<()> {
|
||||
let mut file_count = 0usize;
|
||||
let mut stack: Vec<(PathBuf, String)> = vec![(local_path.to_path_buf(), remote_path.to_string())];
|
||||
let mut stack: Vec<(PathBuf, String)> =
|
||||
vec![(local_path.to_path_buf(), remote_path.to_string())];
|
||||
|
||||
while let Some((dir_path, dir_remote)) = stack.pop() {
|
||||
let mut entries = tokio::fs::read_dir(&dir_path)
|
||||
|
|
@ -448,4 +451,4 @@ mod tests {
|
|||
"Should mention ambiguity"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ pub async fn run_command(
|
|||
};
|
||||
|
||||
// Set up git worktree for local execution (must happen before cwd is captured)
|
||||
let (worktree_work_dir, worktree_path, worktree_branch, worktree_base_sha) = if git_clean {
|
||||
let (worktree_work_dir, _worktree_path, worktree_branch, worktree_base_sha) = if git_clean {
|
||||
match setup_worktree(&original_cwd, &logs_dir, &run_id) {
|
||||
Ok((wd, wt, branch, base)) => (Some(wd), Some(wt), Some(branch), Some(base)),
|
||||
Err(e) => {
|
||||
|
|
@ -617,7 +617,11 @@ pub async fn run_command(
|
|||
{
|
||||
let sandbox_info_opt = {
|
||||
let info = sandbox.sandbox_info();
|
||||
if info.is_empty() { None } else { Some(info) }
|
||||
if info.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(info)
|
||||
}
|
||||
};
|
||||
let record = match sandbox_provider {
|
||||
SandboxProvider::Local => crate::sandbox_record::SandboxRecord {
|
||||
|
|
@ -933,11 +937,8 @@ pub async fn run_command(
|
|||
};
|
||||
let run_duration_ms = run_start.elapsed().as_millis() as u64;
|
||||
|
||||
// Restore cwd and clean up worktree (best-effort)
|
||||
// Restore cwd (worktree is kept for `arc cp` access; pruned separately)
|
||||
let _ = std::env::set_current_dir(&original_cwd);
|
||||
if let Some(ref wt) = worktree_path {
|
||||
let _ = crate::git::remove_worktree(&original_cwd, wt);
|
||||
}
|
||||
|
||||
{
|
||||
let (status, failure_reason) = match &engine_result {
|
||||
|
|
@ -1447,11 +1448,8 @@ async fn run_from_branch(
|
|||
.await;
|
||||
let run_duration_ms = run_start.elapsed().as_millis() as u64;
|
||||
|
||||
// Clean up
|
||||
// Restore cwd (worktree is kept for `arc cp` access; pruned separately)
|
||||
let _ = std::env::set_current_dir(&original_cwd);
|
||||
if let Some(ref wt) = worktree_path {
|
||||
let _ = crate::git::remove_worktree(&original_cwd, wt);
|
||||
}
|
||||
if sandbox_provider == SandboxProvider::Exe {
|
||||
let _ = sandbox.cleanup().await;
|
||||
}
|
||||
|
|
@ -2258,4 +2256,4 @@ mod tests {
|
|||
let parsed: serde_json::Value = serde_json::from_str(&redacted).unwrap();
|
||||
assert_eq!(parsed["run_id"], "def-456");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -660,4 +660,4 @@ mod tests {
|
|||
let result = parse_label_filters(&args);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,10 +81,7 @@ mod tests {
|
|||
loaded.host_working_directory.as_deref(),
|
||||
Some("/home/user/project")
|
||||
);
|
||||
assert_eq!(
|
||||
loaded.container_mount_point.as_deref(),
|
||||
Some("/workspace")
|
||||
);
|
||||
assert_eq!(loaded.container_mount_point.as_deref(), Some("/workspace"));
|
||||
assert!(loaded.data_host.is_none());
|
||||
}
|
||||
|
||||
|
|
|
|||
303
crates/arc-workflows/tests/cp_integration.rs
Normal file
303
crates/arc-workflows/tests/cp_integration.rs
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
//! E2E tests for `arc cp` against local and Docker sandbox backends.
|
||||
//!
|
||||
//! Local tests run without `#[ignore]` (no external dependencies).
|
||||
//! Docker tests require a Docker daemon and are marked `#[ignore]`.
|
||||
//! Run Docker tests with: `cargo test --package arc-workflows --test cp_integration -- --ignored`
|
||||
|
||||
use arc_workflows::cli::cp::reconnect;
|
||||
use arc_workflows::sandbox_record::SandboxRecord;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local sandbox
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn local_record(working_directory: &std::path::Path) -> SandboxRecord {
|
||||
SandboxRecord {
|
||||
provider: "local".to_string(),
|
||||
working_directory: working_directory.to_string_lossy().to_string(),
|
||||
identifier: None,
|
||||
host_working_directory: None,
|
||||
container_mount_point: None,
|
||||
data_host: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_cp_upload_download_round_trip() {
|
||||
let sandbox_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = local_record(sandbox_dir.path());
|
||||
let sandbox = reconnect(&record).await.expect("reconnect local");
|
||||
|
||||
// Upload a text file
|
||||
let content = b"hello from local cp test\n";
|
||||
let local_src = scratch.path().join("upload.txt");
|
||||
std::fs::write(&local_src, content).unwrap();
|
||||
|
||||
sandbox
|
||||
.upload_file_from_local(&local_src, "cp_test.txt")
|
||||
.await
|
||||
.expect("upload text");
|
||||
|
||||
// Verify it landed in the sandbox working directory
|
||||
assert!(sandbox_dir.path().join("cp_test.txt").exists());
|
||||
|
||||
// Download it back
|
||||
let local_dst = scratch.path().join("download.txt");
|
||||
sandbox
|
||||
.download_file_to_local("cp_test.txt", &local_dst)
|
||||
.await
|
||||
.expect("download text");
|
||||
|
||||
assert_eq!(std::fs::read(&local_dst).unwrap(), content);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_cp_binary_round_trip() {
|
||||
let sandbox_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = local_record(sandbox_dir.path());
|
||||
let sandbox = reconnect(&record).await.expect("reconnect local");
|
||||
|
||||
// All 256 byte values
|
||||
let binary: Vec<u8> = (0..=255).collect();
|
||||
let local_src = scratch.path().join("binary.bin");
|
||||
std::fs::write(&local_src, &binary).unwrap();
|
||||
|
||||
sandbox
|
||||
.upload_file_from_local(&local_src, "binary.bin")
|
||||
.await
|
||||
.expect("upload binary");
|
||||
|
||||
let local_dst = scratch.path().join("binary_dl.bin");
|
||||
sandbox
|
||||
.download_file_to_local("binary.bin", &local_dst)
|
||||
.await
|
||||
.expect("download binary");
|
||||
|
||||
assert_eq!(std::fs::read(&local_dst).unwrap(), binary);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_cp_creates_parent_dirs() {
|
||||
let sandbox_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = local_record(sandbox_dir.path());
|
||||
let sandbox = reconnect(&record).await.expect("reconnect local");
|
||||
|
||||
let content = b"nested file\n";
|
||||
let local_src = scratch.path().join("nested.txt");
|
||||
std::fs::write(&local_src, content).unwrap();
|
||||
|
||||
// Upload to a nested path that doesn't exist yet
|
||||
sandbox
|
||||
.upload_file_from_local(&local_src, "a/b/c/nested.txt")
|
||||
.await
|
||||
.expect("upload to nested path");
|
||||
|
||||
assert!(sandbox_dir.path().join("a/b/c/nested.txt").exists());
|
||||
|
||||
// Download to a nested local path that doesn't exist yet
|
||||
let local_dst = scratch.path().join("x/y/z/nested.txt");
|
||||
sandbox
|
||||
.download_file_to_local("a/b/c/nested.txt", &local_dst)
|
||||
.await
|
||||
.expect("download to nested path");
|
||||
|
||||
assert_eq!(std::fs::read(&local_dst).unwrap(), content);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_cp_record_save_load_round_trip() {
|
||||
let sandbox_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = local_record(sandbox_dir.path());
|
||||
let path = scratch.path().join("sandbox.json");
|
||||
record.save(&path).unwrap();
|
||||
|
||||
let loaded = SandboxRecord::load(&path).unwrap();
|
||||
assert_eq!(loaded.provider, "local");
|
||||
assert_eq!(loaded.working_directory, record.working_directory);
|
||||
assert!(loaded.identifier.is_none());
|
||||
|
||||
// Reconnect from the loaded record still works
|
||||
let sandbox = reconnect(&loaded).await.expect("reconnect from loaded");
|
||||
assert_eq!(sandbox.working_directory(), record.working_directory);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Docker sandbox
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn docker_record(host_dir: &std::path::Path, mount_point: &str) -> SandboxRecord {
|
||||
SandboxRecord {
|
||||
provider: "docker".to_string(),
|
||||
working_directory: mount_point.to_string(),
|
||||
identifier: None,
|
||||
host_working_directory: Some(host_dir.to_string_lossy().to_string()),
|
||||
container_mount_point: Some(mount_point.to_string()),
|
||||
data_host: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires Docker daemon
|
||||
async fn docker_cp_upload_download_round_trip() {
|
||||
let host_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = docker_record(host_dir.path(), "/workspace");
|
||||
let sandbox = reconnect(&record).await.expect("reconnect docker");
|
||||
|
||||
// Upload a text file
|
||||
let content = b"hello from docker cp test\n";
|
||||
let local_src = scratch.path().join("upload.txt");
|
||||
std::fs::write(&local_src, content).unwrap();
|
||||
|
||||
sandbox
|
||||
.upload_file_from_local(&local_src, "cp_test.txt")
|
||||
.await
|
||||
.expect("upload text");
|
||||
|
||||
// Verify it landed on the host filesystem (bind mount path)
|
||||
assert!(host_dir.path().join("cp_test.txt").exists());
|
||||
assert_eq!(
|
||||
std::fs::read(host_dir.path().join("cp_test.txt")).unwrap(),
|
||||
content
|
||||
);
|
||||
|
||||
// Download it back
|
||||
let local_dst = scratch.path().join("download.txt");
|
||||
sandbox
|
||||
.download_file_to_local("cp_test.txt", &local_dst)
|
||||
.await
|
||||
.expect("download text");
|
||||
|
||||
assert_eq!(std::fs::read(&local_dst).unwrap(), content);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires Docker daemon
|
||||
async fn docker_cp_binary_round_trip() {
|
||||
let host_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = docker_record(host_dir.path(), "/workspace");
|
||||
let sandbox = reconnect(&record).await.expect("reconnect docker");
|
||||
|
||||
let binary: Vec<u8> = (0..=255).collect();
|
||||
let local_src = scratch.path().join("binary.bin");
|
||||
std::fs::write(&local_src, &binary).unwrap();
|
||||
|
||||
sandbox
|
||||
.upload_file_from_local(&local_src, "binary.bin")
|
||||
.await
|
||||
.expect("upload binary");
|
||||
|
||||
let local_dst = scratch.path().join("binary_dl.bin");
|
||||
sandbox
|
||||
.download_file_to_local("binary.bin", &local_dst)
|
||||
.await
|
||||
.expect("download binary");
|
||||
|
||||
assert_eq!(std::fs::read(&local_dst).unwrap(), binary);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires Docker daemon
|
||||
async fn docker_cp_creates_parent_dirs() {
|
||||
let host_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = docker_record(host_dir.path(), "/workspace");
|
||||
let sandbox = reconnect(&record).await.expect("reconnect docker");
|
||||
|
||||
let content = b"nested docker file\n";
|
||||
let local_src = scratch.path().join("nested.txt");
|
||||
std::fs::write(&local_src, content).unwrap();
|
||||
|
||||
sandbox
|
||||
.upload_file_from_local(&local_src, "deep/nested/file.txt")
|
||||
.await
|
||||
.expect("upload to nested path");
|
||||
|
||||
assert!(host_dir.path().join("deep/nested/file.txt").exists());
|
||||
|
||||
let local_dst = scratch.path().join("p/q/file.txt");
|
||||
sandbox
|
||||
.download_file_to_local("deep/nested/file.txt", &local_dst)
|
||||
.await
|
||||
.expect("download to nested path");
|
||||
|
||||
assert_eq!(std::fs::read(&local_dst).unwrap(), content);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires Docker daemon
|
||||
async fn docker_cp_custom_mount_point() {
|
||||
let host_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
// Use a non-default mount point
|
||||
let record = docker_record(host_dir.path(), "/app");
|
||||
let sandbox = reconnect(&record).await.expect("reconnect docker");
|
||||
|
||||
let content = b"custom mount\n";
|
||||
let local_src = scratch.path().join("mount.txt");
|
||||
std::fs::write(&local_src, content).unwrap();
|
||||
|
||||
sandbox
|
||||
.upload_file_from_local(&local_src, "mount.txt")
|
||||
.await
|
||||
.expect("upload with custom mount");
|
||||
|
||||
assert!(host_dir.path().join("mount.txt").exists());
|
||||
|
||||
let local_dst = scratch.path().join("mount_dl.txt");
|
||||
sandbox
|
||||
.download_file_to_local("mount.txt", &local_dst)
|
||||
.await
|
||||
.expect("download with custom mount");
|
||||
|
||||
assert_eq!(std::fs::read(&local_dst).unwrap(), content);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires Docker daemon
|
||||
async fn docker_cp_record_save_load_round_trip() {
|
||||
let host_dir = tempfile::tempdir().unwrap();
|
||||
let scratch = tempfile::tempdir().unwrap();
|
||||
|
||||
let record = docker_record(host_dir.path(), "/workspace");
|
||||
let path = scratch.path().join("sandbox.json");
|
||||
record.save(&path).unwrap();
|
||||
|
||||
let loaded = SandboxRecord::load(&path).unwrap();
|
||||
assert_eq!(loaded.provider, "docker");
|
||||
assert_eq!(
|
||||
loaded.host_working_directory.as_deref(),
|
||||
Some(host_dir.path().to_str().unwrap())
|
||||
);
|
||||
assert_eq!(loaded.container_mount_point.as_deref(), Some("/workspace"));
|
||||
|
||||
// Reconnect from the loaded record still works
|
||||
let sandbox = reconnect(&loaded).await.expect("reconnect from loaded");
|
||||
|
||||
let content = b"from loaded record\n";
|
||||
let local_src = scratch.path().join("loaded.txt");
|
||||
std::fs::write(&local_src, content).unwrap();
|
||||
|
||||
sandbox
|
||||
.upload_file_from_local(&local_src, "loaded.txt")
|
||||
.await
|
||||
.expect("upload via loaded record");
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read(host_dir.path().join("loaded.txt")).unwrap(),
|
||||
content
|
||||
);
|
||||
}
|
||||
|
|
@ -1743,3 +1743,104 @@ async fn daytona_toolbox_idle_diagnostic() {
|
|||
eprintln!("\n=== PASS: all idle durations survived ===");
|
||||
env.cleanup().await.unwrap();
|
||||
}
|
||||
|
||||
/// E2E test for `arc cp` against a live Daytona sandbox.
|
||||
///
|
||||
/// Creates a sandbox, saves a SandboxRecord, reconnects via `cp::reconnect`,
|
||||
/// uploads a file, downloads it back, and verifies the round-trip.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn daytona_cp_upload_download_round_trip() {
|
||||
use arc_workflows::cli::cp::reconnect;
|
||||
use arc_workflows::sandbox_record::SandboxRecord;
|
||||
|
||||
// 1. Create and initialize a real Daytona sandbox
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
let sandbox_name = env.sandbox_info();
|
||||
assert!(
|
||||
!sandbox_name.is_empty(),
|
||||
"sandbox_info() should return the Daytona sandbox name"
|
||||
);
|
||||
|
||||
// 2. Build a SandboxRecord (same as `arc run` would persist)
|
||||
let record = SandboxRecord {
|
||||
provider: "daytona".to_string(),
|
||||
working_directory: env.working_directory().to_string(),
|
||||
identifier: Some(sandbox_name.clone()),
|
||||
host_working_directory: None,
|
||||
container_mount_point: None,
|
||||
data_host: None,
|
||||
};
|
||||
|
||||
// 3. Save to temp dir and reload (verify serialization round-trip)
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let record_path = tmp.path().join("sandbox.json");
|
||||
record.save(&record_path).unwrap();
|
||||
let loaded = SandboxRecord::load(&record_path).unwrap();
|
||||
assert_eq!(loaded.provider, "daytona");
|
||||
assert_eq!(loaded.identifier.as_deref(), Some(sandbox_name.as_str()));
|
||||
|
||||
// 4. Reconnect via the real cp::reconnect path
|
||||
let reconnected = reconnect(&loaded).await.expect("reconnect should succeed");
|
||||
|
||||
// 5. Upload: write a local file, then upload it to the sandbox
|
||||
let upload_content = b"hello from arc cp e2e test\n";
|
||||
let local_upload = tmp.path().join("upload.txt");
|
||||
std::fs::write(&local_upload, upload_content).unwrap();
|
||||
|
||||
reconnected
|
||||
.upload_file_from_local(&local_upload, "cp_test_upload.txt")
|
||||
.await
|
||||
.expect("upload_file_from_local should succeed");
|
||||
|
||||
// 6. Verify the file exists in the sandbox via the original connection
|
||||
assert!(
|
||||
env.file_exists("cp_test_upload.txt").await.unwrap(),
|
||||
"uploaded file should exist in the sandbox"
|
||||
);
|
||||
let remote_content = env
|
||||
.read_file("cp_test_upload.txt", None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
remote_content.contains("hello from arc cp e2e test"),
|
||||
"expected uploaded content in sandbox, got: {remote_content}"
|
||||
);
|
||||
|
||||
// 7. Download: retrieve the file back to local via the reconnected sandbox
|
||||
let local_download = tmp.path().join("download.txt");
|
||||
reconnected
|
||||
.download_file_to_local("cp_test_upload.txt", &local_download)
|
||||
.await
|
||||
.expect("download_file_to_local should succeed");
|
||||
|
||||
let downloaded = std::fs::read(&local_download).unwrap();
|
||||
assert_eq!(downloaded, upload_content);
|
||||
|
||||
// 8. Upload a binary file to test non-UTF-8 content
|
||||
let binary_content: Vec<u8> = (0..=255).collect();
|
||||
let local_binary = tmp.path().join("binary.bin");
|
||||
std::fs::write(&local_binary, &binary_content).unwrap();
|
||||
|
||||
reconnected
|
||||
.upload_file_from_local(&local_binary, "cp_test_binary.bin")
|
||||
.await
|
||||
.expect("binary upload should succeed");
|
||||
|
||||
let local_binary_dl = tmp.path().join("binary_dl.bin");
|
||||
reconnected
|
||||
.download_file_to_local("cp_test_binary.bin", &local_binary_dl)
|
||||
.await
|
||||
.expect("binary download should succeed");
|
||||
|
||||
let downloaded_binary = std::fs::read(&local_binary_dl).unwrap();
|
||||
assert_eq!(
|
||||
downloaded_binary, binary_content,
|
||||
"binary round-trip should be exact"
|
||||
);
|
||||
|
||||
// 9. Cleanup
|
||||
env.cleanup().await.unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8926,7 +8926,11 @@ impl arc_agent::Sandbox for RemoteMockEnv {
|
|||
Err("not implemented".to_string())
|
||||
}
|
||||
|
||||
async fn upload_file_from_local(&self, _: &std::path::Path, _: &str) -> std::result::Result<(), String> {
|
||||
async fn upload_file_from_local(
|
||||
&self,
|
||||
_: &std::path::Path,
|
||||
_: &str,
|
||||
) -> std::result::Result<(), String> {
|
||||
Err("not implemented".to_string())
|
||||
}
|
||||
|
||||
|
|
@ -13256,4 +13260,4 @@ async fn wait_timer_e2e() {
|
|||
};
|
||||
let outcome = engine.run(&graph, &config).await.expect("run");
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue