fix(sandbox): surface snapshot lifecycle progress

Emit snapshot slow-path events only when Docker or Daytona actually performs image or snapshot work, replace retired completion markers with snapshot.ready, and render the lifecycle in attach/log output.
This commit is contained in:
Bryan Helmkamp 2026-05-03 20:44:16 -04:00
parent fe342a4bd7
commit 446c7cc065
No known key found for this signature in database
13 changed files with 533 additions and 96 deletions

View file

@ -151,6 +151,8 @@ Prefer `Emitter::emit(&Event::...)`.
Use `to_run_event()` only for true bypass paths.
For cache-backed lifecycle work, emit slow-path start events only when the operation actually misses cache or waits on remote state. Completion events should represent a real ensure step (inspect, build, pull, or poll), not a configured no-op.
### 7. Update consumers
Check:

View file

@ -1652,6 +1652,8 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
### `sandbox.snapshot.pulling`
Emitted only when the Docker image cache misses and Fabro starts pulling the image.
```json
{
"id": "...", "ts": "...", "run_id": "...",
@ -1666,42 +1668,10 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
|----------|------|-------------|
| `name` | string | Image/snapshot name |
### `sandbox.snapshot.pulled`
```json
{
"id": "...", "ts": "...", "run_id": "...",
"event": "sandbox.snapshot.pulled",
"properties": {
"name": "my-image:latest",
"duration_ms": 15000
}
}
```
| Property | Type | Description |
|----------|------|-------------|
| `name` | string | Image/snapshot name |
| `duration_ms` | number | Pull duration |
### `sandbox.snapshot.ensuring`
```json
{
"id": "...", "ts": "...", "run_id": "...",
"event": "sandbox.snapshot.ensuring",
"properties": {
"name": "my-snapshot"
}
}
```
| Property | Type | Description |
|----------|------|-------------|
| `name` | string | Snapshot name |
### `sandbox.snapshot.creating`
Emitted only when a Daytona snapshot cache miss or inactive snapshot requires Fabro to create or wait for the snapshot.
```json
{
"id": "...", "ts": "...", "run_id": "...",
@ -1718,6 +1688,8 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
### `sandbox.snapshot.ready`
Emitted when an image or snapshot ensure step succeeds. Cache hits still emit this event with a near-zero `duration_ms`; explicit no-op paths such as Docker `auto_pull = false` and the Daytona default snapshot path do not.
```json
{
"id": "...", "ts": "...", "run_id": "...",
@ -1732,10 +1704,12 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
| Property | Type | Description |
|----------|------|-------------|
| `name` | string | Snapshot name |
| `duration_ms` | number | Creation duration |
| `duration_ms` | number | Ensure duration |
### `sandbox.snapshot.failed`
Emitted when an image or snapshot ensure step fails.
```json
{
"id": "...", "ts": "...", "run_id": "...",
@ -1751,6 +1725,7 @@ Emitted after the engine completes sandbox initialization (distinct from `sandbo
|----------|------|-------------|
| `name` | string | Snapshot name |
| `error` | string | Error message |
| `causes` | string[] | Optional error cause chain |
### `sandbox.git.started`

View file

@ -642,6 +642,43 @@ fn format_event_pretty_value(envelope: &serde_json::Value, styles: &Styles) -> O
styles.dim.apply_to(&duration),
))
}
"sandbox.snapshot.pulling" => {
let name = prop_str_field(envelope, "name").unwrap_or("?");
Some(format!(
"{} Sandbox: pulling {}",
styles.dim.apply_to(&ts),
name,
))
}
"sandbox.snapshot.creating" => {
let name = prop_str_field(envelope, "name").unwrap_or("?");
Some(format!(
"{} Sandbox: building {}",
styles.dim.apply_to(&ts),
name,
))
}
"sandbox.snapshot.ready" => {
let name = prop_str_field(envelope, "name").unwrap_or("?");
let duration = format_duration_ms(prop_field(envelope, "duration_ms"));
Some(format!(
"{} Sandbox snapshot: {} {}",
styles.dim.apply_to(&ts),
name,
styles.dim.apply_to(&duration),
))
}
"sandbox.snapshot.failed" => {
let name = prop_str_field(envelope, "name").unwrap_or("?");
let error = prop_str_field(envelope, "error").unwrap_or("unknown error");
Some(format!(
"{} {} Sandbox snapshot {} failed: {}",
styles.dim.apply_to(&ts),
styles.bold_red.apply_to("\u{2717}"),
name,
styles.red.apply_to(error),
))
}
"setup.completed" => {
let count = prop_field(envelope, "command_count").and_then(serde_json::Value::as_u64);
let duration = format_duration_ms(prop_field(envelope, "duration_ms"));
@ -1155,6 +1192,45 @@ mod tests {
assert!(result.contains("[push]"), "got: {result}");
}
#[test]
fn pretty_sandbox_snapshot_pulling() {
let styles = no_color_styles();
let line = r#"{"ts":"2026-01-01T14:25:00Z","event":"sandbox.snapshot.pulling","properties":{"name":"buildpack-deps:noble"}}"#;
let result = format_event_pretty(line, &styles).unwrap();
assert!(result.contains("Sandbox: pulling"), "got: {result}");
assert!(result.contains("buildpack-deps:noble"), "got: {result}");
}
#[test]
fn pretty_sandbox_snapshot_creating() {
let styles = no_color_styles();
let line = r#"{"ts":"2026-01-01T14:25:00Z","event":"sandbox.snapshot.creating","properties":{"name":"fabro-v9-test"}}"#;
let result = format_event_pretty(line, &styles).unwrap();
assert!(result.contains("Sandbox: building"), "got: {result}");
assert!(result.contains("fabro-v9-test"), "got: {result}");
}
#[test]
fn pretty_sandbox_snapshot_ready() {
let styles = no_color_styles();
let line = r#"{"ts":"2026-01-01T14:25:00Z","event":"sandbox.snapshot.ready","properties":{"name":"buildpack-deps:noble","duration_ms":8200}}"#;
let result = format_event_pretty(line, &styles).unwrap();
assert!(result.contains("Sandbox snapshot:"), "got: {result}");
assert!(result.contains("buildpack-deps:noble"), "got: {result}");
assert!(result.contains("8s"), "got: {result}");
}
#[test]
fn pretty_sandbox_snapshot_failed() {
let styles = no_color_styles();
let line = r#"{"ts":"2026-01-01T14:25:00Z","event":"sandbox.snapshot.failed","properties":{"name":"buildpack-deps:noble","error":"pull failed"}}"#;
let result = format_event_pretty(line, &styles).unwrap();
assert!(
result.contains("Sandbox snapshot buildpack-deps:noble failed: pull failed"),
"got: {result}"
);
}
#[test]
fn pretty_stream_suppresses_metadata_compat_notice_only() {
let styles = no_color_styles();

View file

@ -55,6 +55,24 @@ pub(super) enum ProgressEvent {
memory: Option<f64>,
url: Option<String>,
},
SandboxFailed {
provider: String,
error: String,
},
SnapshotPulling {
name: String,
},
SnapshotCreating {
name: String,
},
SnapshotReady {
name: String,
duration_ms: u64,
},
SnapshotFailed {
name: String,
error: String,
},
SshAccessReady {
ssh_command: String,
},
@ -252,6 +270,24 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option<ProgressEvent> {
memory: props.memory,
url: props.url.clone(),
}),
EventBody::SandboxFailed(props) => Some(ProgressEvent::SandboxFailed {
provider: props.provider.clone(),
error: props.error.clone(),
}),
EventBody::SnapshotPulling(props) => Some(ProgressEvent::SnapshotPulling {
name: props.name.clone(),
}),
EventBody::SnapshotCreating(props) => Some(ProgressEvent::SnapshotCreating {
name: props.name.clone(),
}),
EventBody::SnapshotReady(props) => Some(ProgressEvent::SnapshotReady {
name: props.name.clone(),
duration_ms: props.duration_ms,
}),
EventBody::SnapshotFailed(props) => Some(ProgressEvent::SnapshotFailed {
name: props.name.clone(),
error: props.error.clone(),
}),
EventBody::SshAccessReady(props) => Some(ProgressEvent::SshAccessReady {
ssh_command: props.ssh_command.clone(),
}),
@ -698,6 +734,72 @@ mod tests {
));
}
#[test]
fn round_trip_sandbox_failed() {
let event = Event::Sandbox {
event: fabro_agent::SandboxEvent::InitializeFailed {
provider: "docker".into(),
error: "pull failed".into(),
causes: Vec::new(),
duration_ms: 900,
},
};
let stored = to_run_event(&fixtures::RUN_1, &event);
let parsed = from_run_event(&stored).unwrap();
assert!(matches!(
parsed,
ProgressEvent::SandboxFailed { provider, error }
if provider == "docker" && error == "pull failed"
));
}
#[test]
fn round_trip_snapshot_lifecycle_events() {
let pulling = to_run_event(&fixtures::RUN_1, &Event::Sandbox {
event: fabro_agent::SandboxEvent::SnapshotPulling {
name: "buildpack-deps:noble".into(),
},
});
let creating = to_run_event(&fixtures::RUN_1, &Event::Sandbox {
event: fabro_agent::SandboxEvent::SnapshotCreating {
name: "fabro-v9".into(),
},
});
let ready = to_run_event(&fixtures::RUN_1, &Event::Sandbox {
event: fabro_agent::SandboxEvent::SnapshotReady {
name: "buildpack-deps:noble".into(),
duration_ms: 1200,
},
});
let failed = to_run_event(&fixtures::RUN_1, &Event::Sandbox {
event: fabro_agent::SandboxEvent::SnapshotFailed {
name: "fabro-v9".into(),
error: "build failed".into(),
causes: Vec::new(),
},
});
assert!(matches!(
from_run_event(&pulling).unwrap(),
ProgressEvent::SnapshotPulling { name } if name == "buildpack-deps:noble"
));
assert!(matches!(
from_run_event(&creating).unwrap(),
ProgressEvent::SnapshotCreating { name } if name == "fabro-v9"
));
assert!(matches!(
from_run_event(&ready).unwrap(),
ProgressEvent::SnapshotReady { name, duration_ms }
if name == "buildpack-deps:noble" && duration_ms == 1200
));
assert!(matches!(
from_run_event(&failed).unwrap(),
ProgressEvent::SnapshotFailed { name, error }
if name == "fabro-v9" && error == "build failed"
));
}
#[test]
fn round_trip_run_notice() {
let event = Event::RunNotice {

View file

@ -122,6 +122,21 @@ impl ProgressUI {
ProgressEvent::SandboxInitializing { provider } => {
self.setup.on_sandbox_initializing(renderer, &provider);
}
ProgressEvent::SandboxFailed { provider, error } => {
self.setup.on_sandbox_failed(renderer, &provider, &error);
}
ProgressEvent::SnapshotPulling { name } => {
self.setup.on_snapshot_pulling(renderer, &name);
}
ProgressEvent::SnapshotCreating { name } => {
self.setup.on_snapshot_creating(renderer, &name);
}
ProgressEvent::SnapshotReady { name, duration_ms } => {
self.setup.on_snapshot_ready(renderer, &name, duration_ms);
}
ProgressEvent::SnapshotFailed { name, error } => {
self.setup.on_snapshot_failed(renderer, &name, &error);
}
ProgressEvent::SandboxReady {
provider,
duration_ms,
@ -882,6 +897,189 @@ mod tests {
");
}
#[test]
fn plain_daytona_snapshot_creation_snapshot() {
let (mut ui, buffer) = capture_ui(false);
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Initializing {
provider: "daytona".into(),
},
});
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::SnapshotCreating {
name: "fabro-v9-test".into(),
},
});
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::SnapshotReady {
name: "fabro-v9-test".into(),
duration_ms: 210_000,
},
});
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Ready {
provider: "daytona".into(),
duration_ms: 212_000,
name: Some("sandbox-1".into()),
cpu: Some(4.0),
memory: Some(8.0),
url: None,
},
});
insta::assert_snapshot!(rendered(&buffer), @r"
Sandbox: building fabro-v9-test...
Sandbox: daytona (ready in 3m32s)
sandbox-1 (4 cpu, 8 GB)
");
}
#[test]
fn plain_docker_snapshot_pull_snapshot() {
let (mut ui, buffer) = capture_ui(false);
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Initializing {
provider: "docker".into(),
},
});
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::SnapshotPulling {
name: "buildpack-deps:noble".into(),
},
});
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::SnapshotReady {
name: "buildpack-deps:noble".into(),
duration_ms: 8_200,
},
});
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Ready {
provider: "docker".into(),
duration_ms: 9_000,
name: None,
cpu: None,
memory: None,
url: None,
},
});
insta::assert_snapshot!(rendered(&buffer), @r"
Sandbox: pulling buildpack-deps:noble...
Sandbox: docker (ready in 9s)
");
}
#[test]
fn plain_docker_skipped_snapshot_phase_snapshot() {
let (mut ui, buffer) = capture_ui(false);
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Initializing {
provider: "docker".into(),
},
});
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Ready {
provider: "docker".into(),
duration_ms: 20,
name: None,
cpu: None,
memory: None,
url: None,
},
});
insta::assert_snapshot!(rendered(&buffer), @" Sandbox: docker (ready in 20ms)");
}
#[test]
fn plain_snapshot_failure_snapshot() {
let (mut ui, buffer) = capture_ui(false);
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Initializing {
provider: "docker".into(),
},
});
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::SnapshotFailed {
name: "buildpack-deps:noble".into(),
error: "pull failed".into(),
causes: Vec::new(),
},
});
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::InitializeFailed {
provider: "docker".into(),
error: "pull failed".into(),
causes: Vec::new(),
duration_ms: 900,
},
});
insta::assert_snapshot!(rendered(&buffer), @r"
Sandbox: Snapshot buildpack-deps:noble failed: pull failed
Sandbox: docker failed: pull failed
");
}
#[test]
fn tty_snapshot_ready_keeps_sandbox_bar_until_sandbox_ready() {
let mut ui = ProgressUI::new(true, false);
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Initializing {
provider: "docker".into(),
},
});
assert!(ui.setup.sandbox_bar.is_some());
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::SnapshotReady {
name: "buildpack-deps:noble".into(),
duration_ms: 10,
},
});
assert!(ui.setup.sandbox_bar.is_some());
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Ready {
provider: "docker".into(),
duration_ms: 20,
name: None,
cpu: None,
memory: None,
url: None,
},
});
assert!(ui.setup.sandbox_bar.is_none());
}
#[test]
fn tty_sandbox_failed_finishes_sandbox_bar() {
let mut ui = ProgressUI::new(true, false);
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::Initializing {
provider: "docker".into(),
},
});
assert!(ui.setup.sandbox_bar.is_some());
emit(&mut ui, Event::Sandbox {
event: SandboxEvent::InitializeFailed {
provider: "docker".into(),
error: "pull failed".into(),
causes: Vec::new(),
duration_ms: 900,
},
});
assert!(ui.setup.sandbox_bar.is_none());
}
#[test]
fn plain_verbose_snapshot() {
let (mut ui, buffer) = capture_ui(true);

View file

@ -8,6 +8,7 @@ use crate::shared::format_duration_ms;
pub(super) struct SetupDisplay {
verbose: bool,
current_provider: Option<String>,
pub(super) sandbox_bar: Option<ProgressBar>,
pub(super) setup_bar: Option<ProgressBar>,
pub(super) setup_command_count: u64,
@ -20,6 +21,7 @@ impl SetupDisplay {
pub(super) fn new(verbose: bool) -> Self {
Self {
verbose,
current_provider: None,
sandbox_bar: None,
setup_bar: None,
setup_command_count: 0,
@ -45,10 +47,11 @@ impl SetupDisplay {
}
pub(super) fn on_sandbox_initializing(&mut self, renderer: &ProgressRenderer, provider: &str) {
self.current_provider = Some(provider.to_string());
if renderer.is_tty() {
let bar = renderer.add_spinner();
bar.set_style(styles::style_header_running());
bar.set_message(format!("Initializing {provider} sandbox..."));
bar.set_message(initializing_message(provider));
bar.enable_steady_tick(Duration::from_millis(100));
self.sandbox_bar = Some(bar);
}
@ -99,6 +102,70 @@ impl SetupDisplay {
}
}
pub(super) fn on_sandbox_failed(
&mut self,
renderer: &ProgressRenderer,
provider: &str,
error: &str,
) {
if renderer.is_tty() {
let bar = self
.sandbox_bar
.take()
.unwrap_or_else(|| renderer.add_spinner());
bar.set_style(styles::style_header_failed());
bar.finish_with_message(format!("Sandbox: {provider} failed: {error}"));
} else {
renderer.print_line(4, &format!("Sandbox: {provider} failed: {error}"));
}
}
pub(super) fn on_snapshot_pulling(&self, renderer: &ProgressRenderer, name: &str) {
if renderer.is_tty() {
if let Some(bar) = self.sandbox_bar.as_ref() {
bar.set_message(format!("Pulling {name}..."));
}
} else {
renderer.print_line(4, &format!("Sandbox: pulling {name}..."));
}
}
pub(super) fn on_snapshot_creating(&self, renderer: &ProgressRenderer, name: &str) {
if renderer.is_tty() {
if let Some(bar) = self.sandbox_bar.as_ref() {
bar.set_message(format!("Building {name}..."));
}
} else {
renderer.print_line(4, &format!("Sandbox: building {name}..."));
}
}
pub(super) fn on_snapshot_ready(
&self,
renderer: &ProgressRenderer,
_name: &str,
_duration_ms: u64,
) {
if renderer.is_tty() {
if let Some(bar) = self.sandbox_bar.as_ref() {
let provider = self.current_provider.as_deref().unwrap_or("sandbox");
bar.set_style(styles::style_header_running());
bar.set_message(initializing_message(provider));
}
}
}
pub(super) fn on_snapshot_failed(&self, renderer: &ProgressRenderer, name: &str, error: &str) {
let message = format!("Snapshot {name} failed: {error}");
if renderer.is_tty() {
if let Some(bar) = self.sandbox_bar.as_ref() {
bar.set_message(message);
}
} else {
renderer.print_line(4, &format!("Sandbox: {message}"));
}
}
pub(super) fn on_ssh_access_ready(renderer: &ProgressRenderer, ssh_command: &str) {
if renderer.is_tty() {
let bar = renderer.add_spinner();
@ -376,3 +443,11 @@ impl SetupDisplay {
}
}
}
fn initializing_message(provider: &str) -> String {
if provider == "sandbox" {
"Initializing sandbox...".to_string()
} else {
format!("Initializing {provider} sandbox...")
}
}

View file

@ -21,6 +21,7 @@ cached_style!(
" {spinner:.dim} {wide_msg} {elapsed:.dim}"
);
cached_style!(style_header_done, " {wide_msg:.dim} {prefix:.dim}");
cached_style!(style_header_failed, " {wide_msg:.red} {prefix:.dim}");
cached_style!(
style_stage_running,
" {spinner:.cyan} {wide_msg} {elapsed:.dim}"

View file

@ -437,6 +437,9 @@ impl DaytonaSandbox {
}
_ => {
// Building/Pending/Pulling — fall through to poll
self.emit(SandboxEvent::SnapshotCreating {
name: snap_cfg.name.clone(),
});
}
}
}
@ -457,6 +460,10 @@ impl DaytonaSandbox {
}
};
self.emit(SandboxEvent::SnapshotCreating {
name: snap_cfg.name.clone(),
});
let params = daytona_sdk::CreateSnapshotParams {
name: snap_cfg.name.clone(),
image: daytona_sdk::ImageSource::Custom(
@ -627,9 +634,6 @@ impl Sandbox for DaytonaSandbox {
let init_start = Instant::now();
let params = if let Some(ref snap_cfg) = self.config.snapshot {
self.emit(SandboxEvent::SnapshotEnsuring {
name: snap_cfg.name.clone(),
});
let snap_start = Instant::now();
if let Err(e) = self.ensure_snapshot(snap_cfg).await {
self.emit(SandboxEvent::SnapshotFailed {

View file

@ -93,6 +93,12 @@ pub struct DockerSandbox {
event_callback: Option<SandboxEventCallback>,
}
enum EnsureImageOutcome {
Skipped,
AlreadyLocal,
Pulled,
}
impl DockerSandbox {
pub fn new(
config: DockerSandboxOptions,
@ -441,13 +447,13 @@ impl DockerSandbox {
Ok(())
}
async fn ensure_image(&self) -> crate::Result<()> {
async fn ensure_image(&self) -> crate::Result<EnsureImageOutcome> {
if !self.config.auto_pull {
return Ok(());
return Ok(EnsureImageOutcome::Skipped);
}
match self.docker.inspect_image(&self.config.image).await {
Ok(_) => return Ok(()),
Ok(_) => return Ok(EnsureImageOutcome::AlreadyLocal),
Err(e) if docker_not_found(&e) => {}
Err(e) => {
return Err(crate::Error::docker_image_inspect(
@ -469,12 +475,15 @@ impl DockerSandbox {
..Default::default()
};
self.emit(SandboxEvent::SnapshotPulling {
name: self.config.image.clone(),
});
let mut stream = self.docker.create_image(Some(opts), None, None);
while let Some(result) = stream.next().await {
result.map_err(|e| crate::Error::docker_image_pull(self.config.image.clone(), e))?;
}
Ok(())
Ok(EnsureImageOutcome::Pulled)
}
async fn create_workspace(&self) -> crate::Result<()> {
@ -972,18 +981,26 @@ impl Sandbox for DockerSandbox {
});
let init_start = Instant::now();
self.emit(SandboxEvent::SnapshotPulling {
name: self.config.image.clone(),
});
let pull_start = Instant::now();
if let Err(e) = self.ensure_image().await {
return Err(self.fail_init(init_start, e));
match self.ensure_image().await {
Ok(EnsureImageOutcome::Skipped) => {}
Ok(EnsureImageOutcome::AlreadyLocal | EnsureImageOutcome::Pulled) => {
let pull_duration =
u64::try_from(pull_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::SnapshotReady {
name: self.config.image.clone(),
duration_ms: pull_duration,
});
}
Err(e) => {
self.emit(SandboxEvent::SnapshotFailed {
name: self.config.image.clone(),
error: e.to_string(),
causes: e.causes(),
});
return Err(self.fail_init(init_start, e));
}
}
let pull_duration = u64::try_from(pull_start.elapsed().as_millis()).unwrap_or(u64::MAX);
self.emit(SandboxEvent::SnapshotPulled {
name: self.config.image.clone(),
duration_ms: pull_duration,
});
let container_name = self
.ensure_name_available()

View file

@ -254,19 +254,10 @@ pub enum SandboxEvent {
causes: Vec<String>,
},
// -- Docker --
// -- Snapshot lifecycle --
SnapshotPulling {
name: String,
},
SnapshotPulled {
name: String,
duration_ms: u64,
},
// -- Daytona snapshots --
SnapshotEnsuring {
name: String,
},
SnapshotCreating {
name: String,
},
@ -339,12 +330,6 @@ impl SandboxEvent {
Self::SnapshotPulling { name } => {
debug!(name, "Snapshot pulling");
}
Self::SnapshotPulled { name, duration_ms } => {
debug!(name, duration_ms, "Snapshot pulled");
}
Self::SnapshotEnsuring { name } => {
debug!(name, "Snapshot ensuring");
}
Self::SnapshotCreating { name } => {
debug!(name, "Snapshot creating");
}
@ -1168,13 +1153,6 @@ mod tests {
SandboxEvent::SnapshotPulling {
name: "ubuntu:22.04".into(),
},
SandboxEvent::SnapshotPulled {
name: "ubuntu:22.04".into(),
duration_ms: 5000,
},
SandboxEvent::SnapshotEnsuring {
name: "my-snap".into(),
},
SandboxEvent::SnapshotCreating {
name: "my-snap".into(),
},
@ -1202,7 +1180,7 @@ mod tests {
},
];
assert_eq!(events.len(), 15, "should test all 15 variants");
assert_eq!(events.len(), 13, "should test all 13 variants");
for event in &events {
let json = serde_json::to_string(event).unwrap();

View file

@ -206,10 +206,6 @@ pub enum EventBody {
SandboxCleanupFailed(SandboxCleanupFailedProps),
#[serde(rename = "sandbox.snapshot.pulling")]
SnapshotPulling(SnapshotNameProps),
#[serde(rename = "sandbox.snapshot.pulled")]
SnapshotPulled(SnapshotCompletedProps),
#[serde(rename = "sandbox.snapshot.ensuring")]
SnapshotEnsuring(SnapshotNameProps),
#[serde(rename = "sandbox.snapshot.creating")]
SnapshotCreating(SnapshotNameProps),
#[serde(rename = "sandbox.snapshot.ready")]
@ -414,8 +410,6 @@ impl EventBody {
Self::SandboxCleanupCompleted(_) => "sandbox.cleanup.completed",
Self::SandboxCleanupFailed(_) => "sandbox.cleanup.failed",
Self::SnapshotPulling(_) => "sandbox.snapshot.pulling",
Self::SnapshotPulled(_) => "sandbox.snapshot.pulled",
Self::SnapshotEnsuring(_) => "sandbox.snapshot.ensuring",
Self::SnapshotCreating(_) => "sandbox.snapshot.creating",
Self::SnapshotReady(_) => "sandbox.snapshot.ready",
Self::SnapshotFailed(_) => "sandbox.snapshot.failed",
@ -548,8 +542,6 @@ fn is_known_event_name(event: &str) -> bool {
| "sandbox.cleanup.completed"
| "sandbox.cleanup.failed"
| "sandbox.snapshot.pulling"
| "sandbox.snapshot.pulled"
| "sandbox.snapshot.ensuring"
| "sandbox.snapshot.creating"
| "sandbox.snapshot.ready"
| "sandbox.snapshot.failed"
@ -1242,6 +1234,34 @@ mod tests {
));
}
#[test]
fn retired_sandbox_snapshot_events_deserialize_as_unknown() {
for (event_name, expected_properties) in [
(
"sandbox.snapshot.pulled",
json!({"name": "buildpack-deps:noble", "duration_ms": 5000}),
),
("sandbox.snapshot.ensuring", json!({"name": "fabro-v8"})),
] {
let value = json!({
"id": "evt_retired_snapshot",
"ts": "2026-04-29T12:00:00.000Z",
"run_id": fixtures::RUN_1,
"event": event_name,
"properties": expected_properties
});
let parsed = RunEvent::from_value(value).unwrap();
match parsed.body {
EventBody::Unknown { name, properties } => {
assert_eq!(name, event_name);
assert_eq!(properties, expected_properties);
}
other => panic!("expected Unknown body, got {other:?}"),
}
}
}
#[test]
fn metadata_snapshot_failed_omits_empty_optional_fields() {
let body = EventBody::MetadataSnapshotFailed(MetadataSnapshotFailedProps {

View file

@ -769,15 +769,6 @@ fn event_body_from_event(event: &Event) -> EventBody {
SandboxEvent::SnapshotPulling { name } => {
EventBody::SnapshotPulling(fabro_types::SnapshotNameProps { name: name.clone() })
}
SandboxEvent::SnapshotPulled { name, duration_ms } => {
EventBody::SnapshotPulled(fabro_types::SnapshotCompletedProps {
name: name.clone(),
duration_ms: *duration_ms,
})
}
SandboxEvent::SnapshotEnsuring { name } => {
EventBody::SnapshotEnsuring(fabro_types::SnapshotNameProps { name: name.clone() })
}
SandboxEvent::SnapshotCreating { name } => {
EventBody::SnapshotCreating(fabro_types::SnapshotNameProps { name: name.clone() })
}

View file

@ -92,8 +92,6 @@ pub fn event_name(event: &Event) -> &'static str {
SandboxEvent::CleanupCompleted { .. } => "sandbox.cleanup.completed",
SandboxEvent::CleanupFailed { .. } => "sandbox.cleanup.failed",
SandboxEvent::SnapshotPulling { .. } => "sandbox.snapshot.pulling",
SandboxEvent::SnapshotPulled { .. } => "sandbox.snapshot.pulled",
SandboxEvent::SnapshotEnsuring { .. } => "sandbox.snapshot.ensuring",
SandboxEvent::SnapshotCreating { .. } => "sandbox.snapshot.creating",
SandboxEvent::SnapshotReady { .. } => "sandbox.snapshot.ready",
SandboxEvent::SnapshotFailed { .. } => "sandbox.snapshot.failed",