fix: restore backend workers from resume
This commit is contained in:
@@ -3,9 +3,10 @@ use std::io;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use client::{
|
use client::{
|
||||||
BackendRuntimeListTarget, BackendWorkerSummary, BackendWorkingDirectorySource,
|
BackendRuntimeListTarget, BackendWorkerRestoreResponse, BackendWorkerRestoreState,
|
||||||
WorkerSessionAvailability, list_backend_stopped_workers, list_backend_workers,
|
BackendWorkerSummary, BackendWorkingDirectorySource, WorkerSessionAvailability,
|
||||||
observe_backend_worker_session,
|
WorkspaceWorkerSessionResponse, list_backend_stopped_workers, list_backend_workers,
|
||||||
|
observe_backend_worker_session, restore_backend_worker,
|
||||||
};
|
};
|
||||||
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -15,6 +16,7 @@ use ratatui::text::{Line, Span};
|
|||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use crate::BackendWorkerPickerIntent;
|
||||||
use crate::backend_workspace_picker::select_backend_workspace;
|
use crate::backend_workspace_picker::select_backend_workspace;
|
||||||
use crate::console;
|
use crate::console;
|
||||||
use crate::inline_terminal::with_inline_terminal;
|
use crate::inline_terminal::with_inline_terminal;
|
||||||
@@ -22,9 +24,53 @@ use crate::inline_terminal::with_inline_terminal;
|
|||||||
const MAX_ROWS: usize = 10;
|
const MAX_ROWS: usize = 10;
|
||||||
const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4;
|
const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
trait BackendWorkerLifecycle {
|
||||||
|
async fn restore(
|
||||||
|
&self,
|
||||||
|
target: &client::BackendRuntimeTarget,
|
||||||
|
) -> Result<BackendWorkerRestoreResponse, io::Error>;
|
||||||
|
|
||||||
|
async fn observe(
|
||||||
|
&self,
|
||||||
|
target: &client::BackendRuntimeTarget,
|
||||||
|
) -> Result<WorkspaceWorkerSessionResponse, io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveBackendWorkerLifecycle;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl BackendWorkerLifecycle for LiveBackendWorkerLifecycle {
|
||||||
|
async fn restore(
|
||||||
|
&self,
|
||||||
|
target: &client::BackendRuntimeTarget,
|
||||||
|
) -> Result<BackendWorkerRestoreResponse, io::Error> {
|
||||||
|
restore_backend_worker(target).await.map_err(|error| {
|
||||||
|
io::Error::other(format!(
|
||||||
|
"failed to restore Backend Worker {}: {error}",
|
||||||
|
target.display_label()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn observe(
|
||||||
|
&self,
|
||||||
|
target: &client::BackendRuntimeTarget,
|
||||||
|
) -> Result<WorkspaceWorkerSessionResponse, io::Error> {
|
||||||
|
observe_backend_worker_session(target)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
io::Error::other(format!(
|
||||||
|
"failed to observe Worker Session {}: {error}",
|
||||||
|
target.display_label()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn run(
|
pub(crate) async fn run(
|
||||||
mut target: BackendRuntimeListTarget,
|
mut target: BackendRuntimeListTarget,
|
||||||
include_stopped: bool,
|
intent: BackendWorkerPickerIntent,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
loop {
|
loop {
|
||||||
if target.workspace_id().is_none() {
|
if target.workspace_id().is_none() {
|
||||||
@@ -40,7 +86,10 @@ pub(crate) async fn run(
|
|||||||
target.base_url
|
target.base_url
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
if include_stopped {
|
if intent == BackendWorkerPickerIntent::Resume {
|
||||||
|
response.items.retain(is_resume_candidate);
|
||||||
|
}
|
||||||
|
if intent.include_stopped() {
|
||||||
match list_backend_stopped_workers(&target).await {
|
match list_backend_stopped_workers(&target).await {
|
||||||
Ok(stopped) => {
|
Ok(stopped) => {
|
||||||
response.items.extend(stopped.items);
|
response.items.extend(stopped.items);
|
||||||
@@ -81,22 +130,80 @@ pub(crate) async fn run(
|
|||||||
}
|
}
|
||||||
WorkerPickerResult::Selected(selected) => selected,
|
WorkerPickerResult::Selected(selected) => selected,
|
||||||
};
|
};
|
||||||
let mut attach_target = target
|
let attach_target = target
|
||||||
.runtime_target(selected.runtime_id.clone(), selected.worker_id.clone())
|
.runtime_target(selected.runtime_id.clone(), selected.worker_id.clone())
|
||||||
.map_err(|error| io::Error::other(error.to_string()))?;
|
.map_err(|error| io::Error::other(error.to_string()))?;
|
||||||
let response = observe_backend_worker_session(&attach_target)
|
let attach_target =
|
||||||
.await
|
prepare_selected_worker(&LiveBackendWorkerLifecycle, attach_target, intent).await?;
|
||||||
.map_err(|error| {
|
|
||||||
io::Error::other(format!(
|
|
||||||
"failed to observe Worker Session {}/{}: {error}",
|
|
||||||
selected.runtime_id, selected.worker_id
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
apply_worker_session_observation(&mut attach_target, response.observation);
|
|
||||||
return console::run_backend_runtime(attach_target).await;
|
return console::run_backend_runtime(attach_target).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn prepare_selected_worker(
|
||||||
|
lifecycle: &impl BackendWorkerLifecycle,
|
||||||
|
mut target: client::BackendRuntimeTarget,
|
||||||
|
intent: BackendWorkerPickerIntent,
|
||||||
|
) -> Result<client::BackendRuntimeTarget, io::Error> {
|
||||||
|
if intent == BackendWorkerPickerIntent::Resume {
|
||||||
|
let response = lifecycle.restore(&target).await?;
|
||||||
|
ensure_restore_accepted(&response)?;
|
||||||
|
let observation = lifecycle.observe(&target).await?;
|
||||||
|
require_live_resume_observation(observation.observation)?;
|
||||||
|
return Ok(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
let observation = lifecycle.observe(&target).await?;
|
||||||
|
apply_worker_session_observation(&mut target, observation.observation);
|
||||||
|
Ok(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_resume_candidate(worker: &BackendWorkerSummary) -> bool {
|
||||||
|
worker.worker_state.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_restore_accepted(response: &BackendWorkerRestoreResponse) -> Result<(), io::Error> {
|
||||||
|
if response.result.state == BackendWorkerRestoreState::Accepted {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let detail = response
|
||||||
|
.result
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.take(3)
|
||||||
|
.map(|diagnostic| {
|
||||||
|
let message = diagnostic.message.chars().take(512).collect::<String>();
|
||||||
|
format!("{}: {message}", diagnostic.code)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("; ");
|
||||||
|
let detail = if detail.is_empty() {
|
||||||
|
"no Backend diagnostics".to_string()
|
||||||
|
} else {
|
||||||
|
detail
|
||||||
|
};
|
||||||
|
Err(io::Error::other(format!(
|
||||||
|
"Backend Worker restore for {}/{} returned {:?} ({detail})",
|
||||||
|
response.runtime_id, response.worker_id, response.result.state
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_live_resume_observation(
|
||||||
|
observation: WorkerSessionAvailability,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
|
match observation {
|
||||||
|
WorkerSessionAvailability::LiveProtocol => Ok(()),
|
||||||
|
WorkerSessionAvailability::RetainedSnapshot { .. } => Err(io::Error::other(
|
||||||
|
"Backend Worker restore completed without a live protocol Session; retained history was not opened because resume requires a live Worker",
|
||||||
|
)),
|
||||||
|
WorkerSessionAvailability::Unavailable { reason, message } => {
|
||||||
|
let message = message.chars().take(512).collect::<String>();
|
||||||
|
Err(io::Error::other(format!(
|
||||||
|
"Backend Worker restore completed without a live protocol Session: {reason:?}: {message}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn dedup_workers(workers: &mut Vec<BackendWorkerSummary>) {
|
fn dedup_workers(workers: &mut Vec<BackendWorkerSummary>) {
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
workers.retain(|worker| seen.insert((worker.runtime_id.clone(), worker.worker_id.clone())));
|
workers.retain(|worker| seen.insert((worker.runtime_id.clone(), worker.worker_id.clone())));
|
||||||
@@ -432,7 +539,8 @@ fn working_directory_text(worker: &BackendWorkerSummary) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use client::{
|
use client::{
|
||||||
BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary,
|
BackendDiagnostic, BackendDiagnosticSeverity, BackendWorkerCapabilitySummary,
|
||||||
|
BackendWorkerImplementationSummary, BackendWorkerRestoreResult,
|
||||||
BackendWorkerWorkspaceSummary,
|
BackendWorkerWorkspaceSummary,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -453,6 +561,7 @@ mod tests {
|
|||||||
workspace_id: Some("ws".to_string()),
|
workspace_id: Some("ws".to_string()),
|
||||||
},
|
},
|
||||||
state: "idle".to_string(),
|
state: "idle".to_string(),
|
||||||
|
availability: protocol::subscription::SubscriptionWorkerAvailability::Observed,
|
||||||
worker_state: Some(protocol::WorkerStateSnapshot {
|
worker_state: Some(protocol::WorkerStateSnapshot {
|
||||||
state: protocol::WorkerState::Busy(protocol::WorkerBusyState::Run(
|
state: protocol::WorkerState::Busy(protocol::WorkerBusyState::Run(
|
||||||
protocol::WorkerRunState::Running,
|
protocol::WorkerRunState::Running,
|
||||||
@@ -488,6 +597,178 @@ mod tests {
|
|||||||
text_width(&text[..byte_offset])
|
text_width(&text[..byte_offset])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn restore_response(state: BackendWorkerRestoreState) -> BackendWorkerRestoreResponse {
|
||||||
|
BackendWorkerRestoreResponse {
|
||||||
|
workspace_id: "workspace-a".to_string(),
|
||||||
|
runtime_id: "runtime-a".to_string(),
|
||||||
|
worker_id: "worker-a".to_string(),
|
||||||
|
result: BackendWorkerRestoreResult {
|
||||||
|
state,
|
||||||
|
worker: None,
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeBackendWorkerLifecycle {
|
||||||
|
restore_response: BackendWorkerRestoreResponse,
|
||||||
|
observation: WorkspaceWorkerSessionResponse,
|
||||||
|
calls: std::sync::Mutex<Vec<&'static str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl BackendWorkerLifecycle for FakeBackendWorkerLifecycle {
|
||||||
|
async fn restore(
|
||||||
|
&self,
|
||||||
|
_target: &client::BackendRuntimeTarget,
|
||||||
|
) -> Result<BackendWorkerRestoreResponse, io::Error> {
|
||||||
|
self.calls.lock().unwrap().push("restore");
|
||||||
|
Ok(self.restore_response.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn observe(
|
||||||
|
&self,
|
||||||
|
_target: &client::BackendRuntimeTarget,
|
||||||
|
) -> Result<WorkspaceWorkerSessionResponse, io::Error> {
|
||||||
|
self.calls.lock().unwrap().push("observe");
|
||||||
|
Ok(self.observation.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_observation(observation: serde_json::Value) -> WorkspaceWorkerSessionResponse {
|
||||||
|
let mut value = serde_json::json!({
|
||||||
|
"subject": {
|
||||||
|
"kind": "runtime_worker",
|
||||||
|
"runtime_id": "runtime-a",
|
||||||
|
"worker_id": "worker-a"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
value.as_object_mut().unwrap().extend(
|
||||||
|
observation
|
||||||
|
.as_object()
|
||||||
|
.expect("observation must be an object")
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
serde_json::from_value(value).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn resume_restores_before_requiring_live_observation() {
|
||||||
|
let lifecycle = FakeBackendWorkerLifecycle {
|
||||||
|
restore_response: restore_response(BackendWorkerRestoreState::Accepted),
|
||||||
|
observation: session_observation(serde_json::json!({
|
||||||
|
"availability": "live_protocol"
|
||||||
|
})),
|
||||||
|
calls: std::sync::Mutex::new(Vec::new()),
|
||||||
|
};
|
||||||
|
let target = client::BackendRuntimeTarget::new(
|
||||||
|
"http://127.0.0.1:3000",
|
||||||
|
"workspace-a",
|
||||||
|
"runtime-a",
|
||||||
|
"worker-a",
|
||||||
|
);
|
||||||
|
|
||||||
|
let prepared =
|
||||||
|
prepare_selected_worker(&lifecycle, target, BackendWorkerPickerIntent::Resume)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*lifecycle.calls.lock().unwrap(), ["restore", "observe"]);
|
||||||
|
assert!(prepared.initial_snapshot.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rejected_resume_does_not_observe_or_open_retained_state() {
|
||||||
|
let lifecycle = FakeBackendWorkerLifecycle {
|
||||||
|
restore_response: restore_response(BackendWorkerRestoreState::Rejected),
|
||||||
|
observation: session_observation(serde_json::json!({
|
||||||
|
"availability": "unavailable",
|
||||||
|
"reason": "retention_missing",
|
||||||
|
"message": "must not be opened"
|
||||||
|
})),
|
||||||
|
calls: std::sync::Mutex::new(Vec::new()),
|
||||||
|
};
|
||||||
|
let target = client::BackendRuntimeTarget::new(
|
||||||
|
"http://127.0.0.1:3000",
|
||||||
|
"workspace-a",
|
||||||
|
"runtime-a",
|
||||||
|
"worker-a",
|
||||||
|
);
|
||||||
|
|
||||||
|
let error = prepare_selected_worker(&lifecycle, target, BackendWorkerPickerIntent::Resume)
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
assert!(error.contains("Rejected"));
|
||||||
|
assert_eq!(*lifecycle.calls.lock().unwrap(), ["restore"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resume_candidates_have_no_current_worker_state() {
|
||||||
|
let live = worker("runtime-a", "worker-live", None);
|
||||||
|
let mut stopped = worker("runtime-a", "worker-stopped", None);
|
||||||
|
stopped.state = "stopped".to_string();
|
||||||
|
stopped.worker_state = None;
|
||||||
|
let mut unknown = worker("runtime-a", "worker-unknown", None);
|
||||||
|
unknown.worker_state = None;
|
||||||
|
|
||||||
|
assert!(!is_resume_candidate(&live));
|
||||||
|
assert!(is_resume_candidate(&stopped));
|
||||||
|
assert!(is_resume_candidate(&unknown));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backend_resume_requires_an_accepted_restore_result() {
|
||||||
|
assert!(
|
||||||
|
ensure_restore_accepted(&restore_response(BackendWorkerRestoreState::Accepted)).is_ok()
|
||||||
|
);
|
||||||
|
for state in [
|
||||||
|
BackendWorkerRestoreState::Rejected,
|
||||||
|
BackendWorkerRestoreState::RolledBack,
|
||||||
|
BackendWorkerRestoreState::ReconciliationRequired,
|
||||||
|
] {
|
||||||
|
let mut response = restore_response(state);
|
||||||
|
response.result.diagnostics.push(BackendDiagnostic {
|
||||||
|
code: "restore_failed".to_string(),
|
||||||
|
severity: BackendDiagnosticSeverity::Error,
|
||||||
|
message: "bounded reason".to_string(),
|
||||||
|
});
|
||||||
|
let error = ensure_restore_accepted(&response).unwrap_err().to_string();
|
||||||
|
assert!(error.contains("restore_failed: bounded reason"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backend_resume_requires_live_protocol_after_restore() {
|
||||||
|
assert!(require_live_resume_observation(WorkerSessionAvailability::LiveProtocol).is_ok());
|
||||||
|
|
||||||
|
let retained =
|
||||||
|
require_live_resume_observation(WorkerSessionAvailability::RetainedSnapshot {
|
||||||
|
identity: client::RetainedSessionIdentity {
|
||||||
|
session_id: "session-a".to_string(),
|
||||||
|
segment_id: "segment-a".to_string(),
|
||||||
|
entry_count: 1,
|
||||||
|
},
|
||||||
|
snapshot: protocol::SessionSnapshot {
|
||||||
|
pending_submissions: protocol::PendingSubmissionsSnapshot::default(),
|
||||||
|
entries: Vec::new(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
assert!(retained.contains("resume requires a live Worker"));
|
||||||
|
|
||||||
|
let unavailable = require_live_resume_observation(WorkerSessionAvailability::Unavailable {
|
||||||
|
reason: client::WorkerSessionUnavailableReason::RetentionMissing,
|
||||||
|
message: "retained state missing".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
assert!(unavailable.contains("RetentionMissing"));
|
||||||
|
assert!(unavailable.contains("retained state missing"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn authoritative_observation_selects_live_retained_and_unavailable_detail_modes() {
|
fn authoritative_observation_selects_live_retained_and_unavailable_detail_modes() {
|
||||||
let mut live = client::BackendRuntimeTarget::new(
|
let mut live = client::BackendRuntimeTarget::new(
|
||||||
|
|||||||
+28
-15
@@ -42,6 +42,21 @@ pub struct LaunchOptions {
|
|||||||
pub workspace_root: PathBuf,
|
pub workspace_root: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BackendWorkerPickerIntent {
|
||||||
|
Attach { include_stopped: bool },
|
||||||
|
Resume,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackendWorkerPickerIntent {
|
||||||
|
pub(crate) fn include_stopped(self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Attach { include_stopped } => include_stopped,
|
||||||
|
Self::Resume => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum LaunchMode {
|
pub enum LaunchMode {
|
||||||
/// Start one client-owned in-process Standalone Worker.
|
/// Start one client-owned in-process Standalone Worker.
|
||||||
@@ -54,10 +69,10 @@ pub enum LaunchMode {
|
|||||||
StandaloneResume { include_all: bool },
|
StandaloneResume { include_all: bool },
|
||||||
/// Create one Backend Worker and attach to it.
|
/// Create one Backend Worker and attach to it.
|
||||||
BackendSpawn,
|
BackendSpawn,
|
||||||
/// List Backend Workers and attach to the selected Worker.
|
/// List Backend Workers and open the selected Worker according to the caller's intent.
|
||||||
Workers {
|
Workers {
|
||||||
runtime_id: Option<String>,
|
runtime_id: Option<String>,
|
||||||
include_stopped: bool,
|
intent: BackendWorkerPickerIntent,
|
||||||
},
|
},
|
||||||
/// Open one Backend Worker through the selected connection target.
|
/// Open one Backend Worker through the selected connection target.
|
||||||
OpenWorker {
|
OpenWorker {
|
||||||
@@ -212,20 +227,18 @@ async fn launch_mode(
|
|||||||
Ok(launch) => backend_spawn::run(launch.target).await,
|
Ok(launch) => backend_spawn::run(launch.target).await,
|
||||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||||
},
|
},
|
||||||
LaunchMode::Workers {
|
LaunchMode::Workers { runtime_id, intent } => {
|
||||||
runtime_id,
|
match target.list_workers(if intent.include_stopped() {
|
||||||
include_stopped,
|
WorkerListRequest::with_stopped(runtime_id)
|
||||||
} => match target.list_workers(if include_stopped {
|
} else {
|
||||||
WorkerListRequest::with_stopped(runtime_id)
|
WorkerListRequest::new(runtime_id)
|
||||||
} else {
|
}) {
|
||||||
WorkerListRequest::new(runtime_id)
|
Ok(worker_list) => {
|
||||||
}) {
|
backend_worker_picker::run(worker_list.backend_target, intent).await
|
||||||
Ok(worker_list) => {
|
}
|
||||||
backend_worker_picker::run(worker_list.backend_target, worker_list.include_stopped)
|
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
}
|
||||||
},
|
|
||||||
LaunchMode::OpenWorker {
|
LaunchMode::OpenWorker {
|
||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
|
|||||||
+60
-4
@@ -23,7 +23,7 @@ use cli_connection::{
|
|||||||
};
|
};
|
||||||
use client::{BackendAuthTarget, Target, TargetKind, start_device_login, wait_for_device_login};
|
use client::{BackendAuthTarget, Target, TargetKind, start_device_login, wait_for_device_login};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tui::{LaunchMode, LaunchOptions};
|
use tui::{BackendWorkerPickerIntent, LaunchMode, LaunchOptions};
|
||||||
use workspace_bootstrap::{
|
use workspace_bootstrap::{
|
||||||
InitOptions, discover_repository_root, run_init, select_backend_workspace_for_repository,
|
InitOptions, discover_repository_root, run_init, select_backend_workspace_for_repository,
|
||||||
};
|
};
|
||||||
@@ -1028,7 +1028,9 @@ fn parse_console_options<R: CliConnectionResolver + ?Sized>(
|
|||||||
target,
|
target,
|
||||||
mode: LaunchMode::Workers {
|
mode: LaunchMode::Workers {
|
||||||
runtime_id,
|
runtime_id,
|
||||||
include_stopped: false,
|
intent: BackendWorkerPickerIntent::Attach {
|
||||||
|
include_stopped: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
workspace_root,
|
workspace_root,
|
||||||
});
|
});
|
||||||
@@ -1154,7 +1156,7 @@ fn parse_workers_args<R: CliConnectionResolver + ?Sized>(
|
|||||||
target,
|
target,
|
||||||
mode: LaunchMode::Workers {
|
mode: LaunchMode::Workers {
|
||||||
runtime_id,
|
runtime_id,
|
||||||
include_stopped,
|
intent: BackendWorkerPickerIntent::Attach { include_stopped },
|
||||||
},
|
},
|
||||||
workspace_root,
|
workspace_root,
|
||||||
})
|
})
|
||||||
@@ -1272,7 +1274,7 @@ fn parse_resume_args<R: CliConnectionResolver + ?Sized>(
|
|||||||
}
|
}
|
||||||
LaunchMode::Workers {
|
LaunchMode::Workers {
|
||||||
runtime_id,
|
runtime_id,
|
||||||
include_stopped: true,
|
intent: BackendWorkerPickerIntent::Resume,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2614,6 +2616,60 @@ backend = "shared"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_backend_resume_uses_restore_picker_intent() {
|
||||||
|
match parse_args_from([
|
||||||
|
"--backend",
|
||||||
|
"http://127.0.0.1:8787",
|
||||||
|
"--workspace-id",
|
||||||
|
"workspace-a",
|
||||||
|
"resume",
|
||||||
|
"--runtime-id",
|
||||||
|
"arcadia",
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
Mode::Tui {
|
||||||
|
mode:
|
||||||
|
LaunchMode::Workers {
|
||||||
|
runtime_id,
|
||||||
|
intent: BackendWorkerPickerIntent::Resume,
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} => assert_eq!(runtime_id.as_deref(), Some("arcadia")),
|
||||||
|
other => panic!("expected Backend resume picker, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_workers_stopped_keeps_retained_attach_intent() {
|
||||||
|
match parse_args_from([
|
||||||
|
"--backend",
|
||||||
|
"http://127.0.0.1:8787",
|
||||||
|
"--workspace-id",
|
||||||
|
"workspace-a",
|
||||||
|
"workers",
|
||||||
|
"--stopped",
|
||||||
|
"--runtime-id",
|
||||||
|
"arcadia",
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
Mode::Tui {
|
||||||
|
mode:
|
||||||
|
LaunchMode::Workers {
|
||||||
|
runtime_id,
|
||||||
|
intent: BackendWorkerPickerIntent::Attach { include_stopped },
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(runtime_id.as_deref(), Some("arcadia"));
|
||||||
|
assert!(include_stopped);
|
||||||
|
}
|
||||||
|
other => panic!("expected retained Backend worker picker, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_resume_subcommand_uses_standalone_store_by_default() {
|
fn parse_resume_subcommand_uses_standalone_store_by_default() {
|
||||||
match parse_args_from(["resume"]).unwrap() {
|
match parse_args_from(["resume"]).unwrap() {
|
||||||
|
|||||||
Reference in New Issue
Block a user