diff --git a/lib/apps/fabro-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs index 1e31c426a..347a569df 100644 --- a/lib/apps/fabro-server/src/run_manifest.rs +++ b/lib/apps/fabro-server/src/run_manifest.rs @@ -2058,8 +2058,12 @@ provider = "local" #[tokio::test] async fn the_local_sandbox_check_passes_through_the_host_providers_health() { - let (prepared, resolved) = + let (mut prepared, resolved) = prepared_and_resolved_for_sandbox(&SandboxProviderKind::LOCAL, false, None); + // The local check resolves the run's working directory from the + // manifest's source directory, which must exist on this server. + let source = tempfile::tempdir().expect("a source directory"); + prepared.source_directory = source.path().to_path_buf(); let mut checks = Vec::new(); let passed = run_sandbox_check( &mut checks, diff --git a/lib/apps/fabro-server/src/sandbox_access.rs b/lib/apps/fabro-server/src/sandbox_access.rs index 2fde91ee3..7df6f7d6c 100644 --- a/lib/apps/fabro-server/src/sandbox_access.rs +++ b/lib/apps/fabro-server/src/sandbox_access.rs @@ -535,10 +535,12 @@ enum Connection { #[cfg(test)] Connected(Arc), /// Connected through [`connect_provider`] on first use. - Lazy { - access: ProviderAccess, - provider: OnceCell>, - }, + Lazy(Box), +} + +struct LazyConnection { + access: ProviderAccess, + provider: OnceCell>, } impl SandboxInventory { @@ -569,10 +571,13 @@ impl SandboxInventory { /// A provider connected through `access` on first use. #[must_use] pub(crate) fn with_lazy(self, kind: SandboxProviderKind, access: ProviderAccess) -> Self { - self.with_entry(kind, Connection::Lazy { - access, - provider: OnceCell::new(), - }) + self.with_entry( + kind, + Connection::Lazy(Box::new(LazyConnection { + access, + provider: OnceCell::new(), + })), + ) } fn with_entry(mut self, kind: SandboxProviderKind, connection: Connection) -> Self { @@ -658,9 +663,10 @@ impl InventoryEntry { Connection::HostDirectories => Ok(None), #[cfg(test)] Connection::Connected(provider) => Ok(Some(provider)), - Connection::Lazy { access, provider } => provider + Connection::Lazy(lazy) => lazy + .provider .get_or_try_init(|| async { - connect_provider(&self.kind, access) + connect_provider(&self.kind, &lazy.access) .await .with_context(|| format!("Failed to connect to the {} provider", self.kind)) }) @@ -1007,7 +1013,6 @@ mod tests { let derived = HostProvider::directory_id(directory.path()) .await .expect("an id for the directory"); - let mut record = record; record.runtime.id = derived.to_string(); let sandbox = attach_run_sandbox(&ProviderAccess::default(), &record, RunId::new()) .await diff --git a/lib/apps/fabro-server/tests/it/scenario/petri.rs b/lib/apps/fabro-server/tests/it/scenario/petri.rs index 651b885cc..d3018c3ee 100644 --- a/lib/apps/fabro-server/tests/it/scenario/petri.rs +++ b/lib/apps/fabro-server/tests/it/scenario/petri.rs @@ -1024,8 +1024,11 @@ async fn the_server_attaches_to_the_container_petri_created() { "{preview}" ); - // Ask Fabro: the session reconnects to the container for its turn and - // the turn completes on the model's answer. + // Ask Fabro: the session's turn reconnects to the container (attach, + // start, the platform probe) before anything else. The in-process app + // has no daemon record for the run-tools client the turn builds next, + // so the turn stops there, past the sandbox: a failure the sandbox + // caused would carry the sandbox code instead. let session = Request::builder() .method("POST") .uri(api(&format!("/runs/{run_id}/sessions"))) @@ -1058,19 +1061,27 @@ async fn the_server_attaches_to_the_container_petri_created() { .filter_map(|line| line.strip_prefix("data: ")) .map(|data| serde_json::from_str(data).expect("session event data should be JSON")) .collect(); - let failed = events + let outcome = events .iter() - .find(|event| event["event"] == "run.session.turn.failed"); + .find(|event| { + event["event"] == "run.session.turn.failed" + || event["event"] == "run.session.turn.succeeded" + }) + .unwrap_or_else(|| panic!("the turn ends: {events:?}")); + let code = outcome["properties"]["code"].as_str().unwrap_or_default(); assert!( - failed.is_none(), - "the turn reached the container: {failed:?}" - ); - assert!( - events - .iter() - .any(|event| event["event"] == "run.session.turn.succeeded"), - "the turn completed: {events:?}" + !matches!(code, "sandbox_unavailable" | "no_sandbox"), + "the turn reached the container: {outcome}" ); + if outcome["event"] == "run.session.turn.failed" { + assert_eq!(code, "agent_error", "{outcome}"); + assert!( + outcome["properties"]["error"] + .as_str() + .is_some_and(|error| error.contains("server record")), + "the turn stopped at the run-tools client, after the sandbox: {outcome}" + ); + } let _ = Command::new("docker") .args(["rm", "-f", &container]) diff --git a/lib/components/fabro-pebble-sandbox/src/environment.rs b/lib/components/fabro-pebble-sandbox/src/environment.rs index 2e6f8dcdf..94ee100b3 100644 --- a/lib/components/fabro-pebble-sandbox/src/environment.rs +++ b/lib/components/fabro-pebble-sandbox/src/environment.rs @@ -642,7 +642,12 @@ mod tests { let results = Environment::grep(&sandbox, "println", "test.rs", &GrepOptions::default()) .await .unwrap(); - assert_eq!(results, ["test.rs:2: println!(\"hello\");"]); + // The path is resolved against the working directory before the + // driver sees it, and comes back as the driver reports it. + let working_dir = sandbox.working_directory(); + assert_eq!(results, [format!( + "{working_dir}/test.rs:2: println!(\"hello\");" + )]); drop((directory, provider)); }