fabro/lib/apps/fabro-cli/tests/it/cmd/sandbox_cp.rs
Bryan Helmkamp f0b23fe426
Project a Petri run's sandbox instance from its scope records
Petri a5906f6 records where each scope's sandbox ran (`scope.acquired`,
`scope.failed`) and how its lease was released (`scope.released`). The
projection folds the root invocation's records into `Run.sandbox`:
`initializing` from `run.started`, `ready` with the `RunSandboxInstance`
(the provider, Petri's `host` as Fabro's `local`, the provider's id, the
image and snapshot, the working directory) from `scope.acquired`, `failed`
from `scope.failed`; the retention outcome is kept in the fold state, since
the view has no field for it. Ask Fabro reconnect and `sandbox cp`,
`preview` and `ssh` reach the run's sandbox again.

A local reconnect designates the recorded working directory again when the
host provider does not know the id: the provider mints a registry-only id
for a workspace path too long for a path-derived one, and that registry
belongs to the run's worker. The stream listing redacts its items the way
the attached stream does, so a client that pages after a stream sees the
same items.

Pins move to Petri a5906f6 (run format 6, engine log v11, event contract
4). The attach stream snapshot is re-recorded with the new record and a
filter for the host provider's minted ids; `sandbox cp` reads an upload
back through the run's workspace, which is no longer the target folder.
Server scenario tests prove the projected instance on the host and Docker
providers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 15:57:12 -04:00

159 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
)]
use std::path::Path;
use fabro_test::{fabro_snapshot, test_context};
use fabro_types::RunSandbox;
use super::support::{
read_text, run_state, setup_local_sandbox_run, setup_seeded_created_dry_run, text_tree,
};
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["sandbox", "cp", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Copy files to/from a run's sandbox
Usage: fabro sandbox cp [OPTIONS] <SRC> <DST>
Arguments:
<SRC> Source: `<run-id>:<path>` or local path
<DST> Destination: `<run-id>:<path>` or local path
Options:
--json Output as JSON [env: FABRO_JSON=]
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
-r, --recursive Recurse into directories
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-h, --help Print help
----- stderr -----
");
}
#[test]
fn sandbox_cp_run_without_sandbox_json_errors_cleanly() {
let context = test_context!();
let run = setup_seeded_created_dry_run(&context);
let dest = context.temp_dir.join("missing.txt");
let mut cmd = context.cp();
cmd.args([&format!("{}:foo.txt", run.run_id), dest.to_str().unwrap()]);
fabro_snapshot!(context.filters(), cmd, @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Run sandbox was not created.
");
}
#[test]
fn sandbox_cp_downloads_file_from_run() {
let context = test_context!();
let setup = setup_local_sandbox_run(&context);
let dest = context.temp_dir.join("downloaded-root.txt");
let mut cmd = context.cp();
cmd.args([
&format!("{}:sandbox_dir/download_me/root.txt", setup.run.run_id),
dest.to_str().unwrap(),
]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
assert_eq!(read_text(&dest), "keep");
}
#[test]
fn sandbox_cp_downloads_file_from_store_without_sandbox_json() {
let context = test_context!();
let setup = setup_local_sandbox_run(&context);
let dest = context.temp_dir.join("downloaded-from-store.txt");
let mut cmd = context.cp();
cmd.args([
&format!("{}:sandbox_dir/download_me/root.txt", setup.run.run_id),
dest.to_str().unwrap(),
]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
assert_eq!(read_text(&dest), "keep");
}
#[test]
fn sandbox_cp_uploads_file_to_run() {
let context = test_context!();
let setup = setup_local_sandbox_run(&context);
let local = context.temp_dir.join("upload.txt");
std::fs::write(&local, "uploaded-root")
.unwrap_or_else(|err| panic!("failed to write {}: {err}", local.display()));
let mut cmd = context.cp();
cmd.args([
local.to_str().unwrap(),
&format!("{}:sandbox_dir/uploaded.txt", setup.run.run_id),
]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
// The run executes in its own workspace, not in the target folder: the
// upload lands where the run's sandbox works.
let working_directory = run_state(&setup.run.run_dir)
.sandbox
.and_then(RunSandbox::into_instance)
.map(|instance| instance.runtime.working_directory)
.expect("the run's sandbox instance");
assert_eq!(
read_text(&Path::new(&working_directory).join("sandbox_dir/uploaded.txt")),
"uploaded-root"
);
}
#[test]
fn sandbox_cp_recursive_downloads_directory() {
let context = test_context!();
let setup = setup_local_sandbox_run(&context);
let dest = context.temp_dir.join("download-dir");
let mut cmd = context.cp();
cmd.args([
"-r",
&format!("{}:sandbox_dir/download_me", setup.run.run_id),
dest.to_str().unwrap(),
]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
insta::assert_snapshot!(
text_tree(&dest).join("\n"),
@r"
nested/child.txt = nested
root.txt = keep
"
);
}