diff --git a/lib/crates/fabro-cli/src/commands/runs/rm.rs b/lib/crates/fabro-cli/src/commands/runs/rm.rs index 67d79f238..0f7e71d8d 100644 --- a/lib/crates/fabro-cli/src/commands/runs/rm.rs +++ b/lib/crates/fabro-cli/src/commands/runs/rm.rs @@ -69,7 +69,18 @@ async fn remove_from( } write_run_status(&run.path, RunStatus::Removing, None); - if let Ok(Some(run_store)) = store.open_run_reader(&run.run_id).await { + let run_store = match store.open_run_reader(&run.run_id).await { + Ok(run_store) => run_store, + Err(err) => { + warn!( + run_id = %run.run_id, + error = %err, + "failed to open run store during removal" + ); + None + } + }; + if let Some(run_store) = run_store.as_ref() { if let Err(err) = run_store .put_status(&RunStatusRecord::new(RunStatus::Removing, None)) .await @@ -82,8 +93,7 @@ async fn remove_from( } } - let sandbox_path = run.path.join("sandbox.json"); - if let Ok(record) = fabro_sandbox::SandboxRecord::load(&sandbox_path) { + if let Some(record) = load_sandbox_record(&run.path, run_store.as_deref()).await { if record.provider != "local" { match reconnect_sandbox(&record).await { Ok(sandbox) => { @@ -144,3 +154,21 @@ async fn remove_from( } Ok(()) } + +async fn load_sandbox_record( + run_dir: &Path, + run_store: Option<&dyn fabro_store::RunStore>, +) -> Option { + if let Some(run_store) = run_store { + match run_store.get_sandbox().await { + Ok(Some(record)) => return Some(record), + Ok(None) => {} + Err(err) => { + warn!(error = %err, "failed to load sandbox record from store"); + } + } + } + + let sandbox_path = run_dir.join("sandbox.json"); + fabro_sandbox::SandboxRecord::load(&sandbox_path).ok() +} diff --git a/lib/crates/fabro-cli/tests/it/cmd/rm.rs b/lib/crates/fabro-cli/tests/it/cmd/rm.rs index 858341967..70030259e 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/rm.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/rm.rs @@ -1,7 +1,7 @@ use fabro_test::{fabro_snapshot, test_context}; use serde_json::Value; -use super::support::{setup_completed_dry_run, setup_created_dry_run}; +use super::support::{setup_completed_dry_run, setup_created_dry_run, setup_local_sandbox_run}; use walkdir::WalkDir; #[test] @@ -118,6 +118,33 @@ fn rm_force_deletes_submitted_run() { "###); } +#[test] +fn rm_force_deletes_run_without_sandbox_json_when_store_has_sandbox() { + let context = test_context!(); + let setup = setup_local_sandbox_run(&context); + std::fs::remove_file(setup.run.run_dir.join("sandbox.json")).unwrap(); + + let mut filters = context.filters(); + filters.push(( + r"\b[0-9A-HJKMNP-TV-Z]{12}\b".to_string(), + "[ULID]".to_string(), + )); + + let mut cmd = context.command(); + cmd.args(["rm", "--force", &setup.run.run_id]); + fabro_snapshot!(filters, cmd, @" + success: true + exit_code: 0 + ----- stdout ----- + ----- stderr ----- + [ULID] + "); + assert!( + !setup.run.run_dir.exists(), + "run directory should be deleted even without sandbox.json" + ); +} + #[test] fn rm_partial_failure_reports_which_identifiers_failed() { let context = test_context!();