From 7210d3c2021d7f6e3e3dae929c3c2219606924b7 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 15 Sep 2026 00:47:12 +0900 Subject: [PATCH] fix: reject failed worker restores before attach --- crates/client/src/backend_runtime.rs | 1 + crates/client/src/lib.rs | 9 +- crates/tui/src/backend_worker_picker.rs | 109 ++++++++++++++++++++++-- 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/crates/client/src/backend_runtime.rs b/crates/client/src/backend_runtime.rs index 07057a88..8ec2ea84 100644 --- a/crates/client/src/backend_runtime.rs +++ b/crates/client/src/backend_runtime.rs @@ -16,6 +16,7 @@ pub use workspace_api::{ WorkerLaunchOptionsResponse as BackendWorkerLaunchOptions, WorkerLaunchProfileCandidate as BackendWorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption as BackendWorkerLaunchRuntimeOption, + WorkerOperationState as BackendWorkerOperationState, WorkerRestoreResponse as BackendWorkerRestoreResponse, WorkerRestoreResult as BackendWorkerRestoreResult, WorkerSummary as BackendWorkerSummary, WorkerWorkspaceSummary as BackendWorkerWorkspaceSummary, diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 538b1493..814e08cd 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -26,10 +26,11 @@ pub use backend_runtime::{ BackendRuntimeListTarget, BackendRuntimeSummary, BackendRuntimeTarget, BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary, BackendWorkerLaunchOptions, BackendWorkerLaunchProfileCandidate, BackendWorkerLaunchRuntimeOption, - BackendWorkerLaunchTarget, BackendWorkerRestoreResponse, BackendWorkerRestoreResult, - BackendWorkerSummary, BackendWorkerWorkspaceSummary, BackendWorkingDirectorySummary, - connect_backend_runtime, create_backend_worker, get_backend_worker_launch_options, - list_backend_stopped_workers, list_backend_workers, restore_backend_worker, + BackendWorkerLaunchTarget, BackendWorkerOperationState, BackendWorkerRestoreResponse, + BackendWorkerRestoreResult, BackendWorkerSummary, BackendWorkerWorkspaceSummary, + BackendWorkingDirectorySummary, connect_backend_runtime, create_backend_worker, + get_backend_worker_launch_options, list_backend_stopped_workers, list_backend_workers, + restore_backend_worker, }; pub use backend_workspace::{ BackendWorkspace, BackendWorkspaceCatalogTarget, BackendWorkspaceClientError, diff --git a/crates/tui/src/backend_worker_picker.rs b/crates/tui/src/backend_worker_picker.rs index 92278986..41302821 100644 --- a/crates/tui/src/backend_worker_picker.rs +++ b/crates/tui/src/backend_worker_picker.rs @@ -3,8 +3,9 @@ use std::io; use std::time::Duration; use client::{ - BackendRuntimeListTarget, BackendWorkerSummary, list_backend_stopped_workers, - list_backend_workers, restore_backend_worker, + BackendRuntimeListTarget, BackendWorkerOperationState, BackendWorkerRestoreResponse, + BackendWorkerSummary, list_backend_stopped_workers, list_backend_workers, + restore_backend_worker, }; use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers}; use ratatui::Frame; @@ -84,17 +85,20 @@ pub(crate) async fn run( let restore_target = target .runtime_target(selected.runtime_id.clone(), selected.worker_id.clone()) .map_err(|error| io::Error::other(error.to_string()))?; - restore_backend_worker(&restore_target) + let restore = restore_backend_worker(&restore_target) .await .map_err(|error| { io::Error::other(format!( "failed to restore Backend worker {}/{}: {error}", selected.runtime_id, selected.worker_id )) - })? - .result - .worker - .unwrap_or(selected) + })?; + restored_worker(restore).map_err(|error| { + io::Error::other(format!( + "failed to restore Backend worker {}/{}: {error}", + selected.runtime_id, selected.worker_id + )) + })? } else { selected }; @@ -105,6 +109,33 @@ pub(crate) async fn run( } } +fn restored_worker(response: BackendWorkerRestoreResponse) -> Result { + if response.result.state != BackendWorkerOperationState::Accepted { + let diagnostics = response + .result + .diagnostics + .iter() + .map(|diagnostic| format!("{}: {}", diagnostic.code, diagnostic.message)) + .collect::>() + .join("; "); + let state = match response.result.state { + BackendWorkerOperationState::Accepted => unreachable!(), + BackendWorkerOperationState::Rejected => "rejected", + BackendWorkerOperationState::Unsupported => "unsupported", + }; + return Err(if diagnostics.is_empty() { + format!("restore was {state} without a diagnostic") + } else { + format!("restore was {state}: {diagnostics}") + }); + } + + response + .result + .worker + .ok_or_else(|| "restore was accepted without a Worker snapshot".to_string()) +} + fn dedup_workers(workers: &mut Vec) { let mut seen = std::collections::HashSet::new(); workers.retain(|worker| seen.insert((worker.runtime_id.clone(), worker.worker_id.clone()))); @@ -405,7 +436,8 @@ fn working_directory_text(worker: &BackendWorkerSummary) -> String { mod tests { use super::*; use client::{ - BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary, + BackendDiagnostic, BackendDiagnosticSeverity, BackendWorkerCapabilitySummary, + BackendWorkerImplementationSummary, BackendWorkerRestoreResult, BackendWorkerWorkspaceSummary, }; @@ -463,6 +495,67 @@ mod tests { text_width(&text[..byte_offset]) } + fn restore_response( + state: BackendWorkerOperationState, + worker: Option, + diagnostics: Vec, + ) -> BackendWorkerRestoreResponse { + BackendWorkerRestoreResponse { + workspace_id: "workspace-a".to_string(), + runtime_id: "runtime-a".to_string(), + worker_id: "worker-a".to_string(), + result: BackendWorkerRestoreResult { + state, + worker, + diagnostics, + }, + } + } + + #[test] + fn rejected_restore_surfaces_diagnostic_instead_of_attaching_selected_worker() { + let error = restored_worker(restore_response( + BackendWorkerOperationState::Rejected, + None, + vec![BackendDiagnostic { + code: "working_directory_not_found".to_string(), + severity: BackendDiagnosticSeverity::Error, + message: "working directory was not found".to_string(), + }], + )) + .expect_err("rejected restore must not produce a Worker to attach"); + + assert_eq!( + error, + "restore was rejected: working_directory_not_found: working directory was not found" + ); + } + + #[test] + fn accepted_restore_requires_returned_worker_snapshot() { + let error = restored_worker(restore_response( + BackendWorkerOperationState::Accepted, + None, + Vec::new(), + )) + .expect_err("accepted restore without a Worker must not attach the stale selection"); + + assert_eq!(error, "restore was accepted without a Worker snapshot"); + } + + #[test] + fn accepted_restore_returns_authoritative_worker_snapshot() { + let worker = worker("runtime-a", "worker-a", Some("builtin:companion")); + let restored = restored_worker(restore_response( + BackendWorkerOperationState::Accepted, + Some(worker.clone()), + Vec::new(), + )) + .expect("accepted restore should return its Worker snapshot"); + + assert_eq!(restored, worker); + } + #[test] fn worker_row_orders_and_simplifies_columns() { let mut worker = worker("runtime-a", "worker-b", Some("builtin:coder"));