fabro/lib/apps/fabro-cli/tests/it/cmd/sandbox_preview.rs
Bryan Helmkamp 28d4242df0
Route Daytona sandboxes through the sandbox driver
The `daytona` provider kind now maps onto the sandbox-driver Daytona
provider instead of fabro's own SDK client. Fabro keeps what is its own:
the HMAC-named snapshot built from the environment's image or Dockerfile,
the explicit 120 minute auto-stop default, the managed labels that gate
destructive operations, the clone decision and layout, and push
credentials. The driver creates the sandbox, clones natively, and serves
exec, files, search, terminal, SSH, preview, and VNC through its facets.

- `daytona.rs` builds the driver `SandboxSpec` (snapshot source,
  `/home/daytona/workspace`, labels, timers, network policy, run name),
  ensures the snapshot through the driver `SnapshotProvider`, attaches by
  persisted id with fabro's label guard, and probes credentials through
  the provider health check under fabro's 20 second budget.
- `DriverSandbox` gains a create plan that settles the spec right before
  the provider call, records the snapshot a sandbox came from, and
  reports the provider console URL on `Ready`.
- Terminals use the driver `Pty` facet; the server's SSH, preview, and
  VNC endpoints use the `SshAccess`, `PreviewUrls`, and `Vnc` facets
  through the driver-typed reconnect. The preview endpoint now answers
  for every provider with a preview facet, so the local sandbox returns
  its loopback URL.
- Daytona credentials travel as `DaytonaCredentials` built from the
  vault key plus configured URL and organization; nothing reads the
  process environment implicitly. The inventory registry uses the shared
  `DriverInventoryProvider`.
- The SDK-based `daytona/mod.rs`, `provider/daytona.rs`, the Daytona
  terminal, the `daytona` cargo feature, and the direct daytona-sdk,
  git2 (in fabro-sandbox), tungstenite, and rustls dependencies are
  gone. The live Daytona tests run against the driver-backed sandbox.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 19:14:13 -06:00

122 lines
4 KiB
Rust

use fabro_test::{fabro_snapshot, test_context};
use httpmock::MockServer;
use serde_json::{Value, json};
use super::support::{remote_run_summary_json, setup_local_sandbox_run};
use crate::support::unique_run_id;
fn remote_run_summary(run_id: &str) -> serde_json::Value {
remote_run_summary_json(
run_id,
"Preview Test",
"preview-test",
"Preview test",
&json!({
"kind": "running"
}),
"2026-04-19T12:00:00Z",
)
}
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["sandbox", "preview", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Get a preview URL for a port on a run's sandbox
Usage: fabro sandbox preview [OPTIONS] <RUN> <PORT>
Arguments:
<RUN> Run ID or prefix
<PORT> Port number
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=]
--signed Generate a signed URL (embeds auth token, no headers needed)
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--ttl <TTL> Signed URL expiry in seconds (default 3600, requires --signed) [default: 3600]
--open Open URL in browser (implies --signed)
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-h, --help Print help
----- stderr -----
");
}
/// Preview URLs come from whichever provider facet the run's sandbox
/// exposes. The local provider runs on the server host, so its preview is
/// the loopback address for the port.
#[test]
fn sandbox_preview_uses_the_local_provider_loopback_url() {
let context = test_context!();
let setup = setup_local_sandbox_run(&context);
let mut cmd = context.preview();
cmd.args([&setup.run.run_id, "3000"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
http://127.0.0.1:3000
----- stderr -----
");
}
#[test]
fn sandbox_preview_open_is_suppressed_by_json_output_format_from_home_config() {
let context = test_context!();
context.write_home(
".fabro/settings.toml",
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
);
let server = MockServer::start();
let run_id = unique_run_id();
let resolve_run = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/runs/resolve")
.query_param("selector", run_id.as_str());
then.status(200)
.header("content-type", "application/json")
.body(remote_run_summary(&run_id).to_string());
});
let preview = server.mock(|when, then| {
when.method("POST")
.path(format!("/api/v1/runs/{run_id}/preview"))
.json_body(json!({
"port": 3000,
"expires_in_secs": 3600,
"signed": true,
}));
then.status(201)
.header("content-type", "application/json")
.body(json!({ "url": "https://preview.example.test/app" }).to_string());
});
let mut cmd = context.preview();
cmd.args([
"--server",
&format!("{}/api/v1", server.base_url()),
"--open",
run_id.as_str(),
"3000",
]);
let output = cmd.output().expect("command should run");
assert!(output.status.success(), "sandbox preview should succeed");
let value: Value = serde_json::from_slice(&output.stdout).expect("preview JSON should parse");
assert_eq!(
value,
json!({
"url": "https://preview.example.test/app"
})
);
resolve_run.assert();
preview.assert();
}