feat: connect standalone host to local TUI

This commit is contained in:
2026-08-30 12:40:22 +09:00
parent 862eeb7add
commit 4b3b4fda61
10 changed files with 413 additions and 55 deletions
Generated
+1
View File
@@ -5331,6 +5331,7 @@ dependencies = [
"serde",
"serde_json",
"session-store",
"standalone",
"tempfile",
"ticket",
"tokio",
+3 -3
View File
@@ -37,9 +37,9 @@ pub use backend_workspace::{
};
pub use runtime_command::WorkerRuntimeCommand;
pub use target::{
BackendTarget, Dashboard, LocalTarget, ResolvedTarget, Target, TargetError, TargetKind,
WorkerByName, WorkerConnection, WorkerConnectionSelector, WorkerList, WorkerListRequest,
WorkerResume, WorkerSpawn,
BackendTarget, Dashboard, LocalTarget, ResolvedTarget, StandaloneTarget, Target, TargetError,
TargetKind, WorkerByName, WorkerConnection, WorkerConnectionSelector, WorkerList,
WorkerListRequest, WorkerResume, WorkerSpawn,
};
pub use spawn::{
+108 -5
View File
@@ -1,16 +1,20 @@
use std::fmt;
use std::{fmt, path::PathBuf};
use crate::{BackendRuntimeListTarget, BackendRuntimeTarget, WorkerRuntimeCommand};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetKind {
/// Legacy local Runtime process/socket authority.
Local,
/// One-process Standalone authority with no Runtime or Workspace backend.
Standalone,
Backend,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedTarget {
Local,
Standalone,
Backend {
base_url: String,
workspace_id: String,
@@ -21,6 +25,7 @@ impl ResolvedTarget {
pub fn kind(&self) -> TargetKind {
match self {
Self::Local => TargetKind::Local,
Self::Standalone => TargetKind::Standalone,
Self::Backend { .. } => TargetKind::Backend,
}
}
@@ -30,6 +35,7 @@ impl fmt::Display for TargetKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Local => f.write_str("local"),
Self::Standalone => f.write_str("Standalone"),
Self::Backend => f.write_str("Backend"),
}
}
@@ -107,8 +113,13 @@ impl WorkerConnectionSelector {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerSpawn {
pub runtime_command: WorkerRuntimeCommand,
pub enum WorkerSpawn {
LegacyLocal {
runtime_command: WorkerRuntimeCommand,
},
Standalone {
state_dir: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -213,7 +224,7 @@ impl Target for LocalTarget {
}
fn spawn_worker(&self) -> Result<WorkerSpawn, TargetError> {
Ok(WorkerSpawn {
Ok(WorkerSpawn::LegacyLocal {
runtime_command: self.runtime_command()?,
})
}
@@ -261,6 +272,65 @@ impl Target for LocalTarget {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StandaloneTarget {
state_dir: PathBuf,
}
impl StandaloneTarget {
#[must_use]
pub fn new(state_dir: impl Into<PathBuf>) -> Self {
Self {
state_dir: state_dir.into(),
}
}
}
impl Target for StandaloneTarget {
fn kind(&self) -> TargetKind {
TargetKind::Standalone
}
fn resolve(&self) -> Result<ResolvedTarget, TargetError> {
Ok(ResolvedTarget::Standalone)
}
fn spawn_worker(&self) -> Result<WorkerSpawn, TargetError> {
Ok(WorkerSpawn::Standalone {
state_dir: self.state_dir.clone(),
})
}
fn worker_by_name(&self) -> Result<WorkerByName, TargetError> {
Err(TargetError::unsupported(
"Worker name attachment",
self.kind(),
))
}
fn resume_worker(&self) -> Result<WorkerResume, TargetError> {
Err(TargetError::unsupported("Worker restore", self.kind()))
}
fn dashboard(&self) -> Result<Dashboard, TargetError> {
Err(TargetError::unsupported("Worker dashboard", self.kind()))
}
fn list_workers(&self, _request: WorkerListRequest) -> Result<WorkerList, TargetError> {
Err(TargetError::unsupported("Worker listing", self.kind()))
}
fn connect_worker(
&self,
_selector: WorkerConnectionSelector,
) -> Result<WorkerConnection, TargetError> {
Err(TargetError::unsupported(
"Backend runtime worker connection",
self.kind(),
))
}
}
impl Target for BackendTarget {
fn kind(&self) -> TargetKind {
TargetKind::Backend
@@ -303,7 +373,9 @@ impl Target for BackendTarget {
base_url,
workspace_id,
}),
ResolvedTarget::Local => unreachable!("BackendTarget cannot resolve as Local"),
ResolvedTarget::Local | ResolvedTarget::Standalone => {
unreachable!("BackendTarget cannot resolve as a local target")
}
}
}
@@ -375,6 +447,37 @@ mod tests {
assert_eq!(LocalTarget::new().resolve().unwrap(), ResolvedTarget::Local);
}
#[test]
fn standalone_target_carries_in_process_state_without_resolving_runtime_command() {
let target = StandaloneTarget::new("/tmp/yoi-standalone-state");
assert_eq!(target.kind(), TargetKind::Standalone);
assert_eq!(target.resolve().unwrap(), ResolvedTarget::Standalone);
assert_eq!(
target.spawn_worker().unwrap(),
WorkerSpawn::Standalone {
state_dir: PathBuf::from("/tmp/yoi-standalone-state"),
}
);
}
#[test]
fn standalone_target_never_falls_back_to_legacy_local_operations() {
let target = StandaloneTarget::new("/tmp/yoi-standalone-state");
assert_eq!(
target.worker_by_name().unwrap_err().to_string(),
"Worker name attachment is not supported by Standalone target"
);
assert_eq!(
target
.list_workers(WorkerListRequest::new(None))
.unwrap_err()
.to_string(),
"Worker listing is not supported by Standalone target"
);
}
#[test]
fn backend_target_builds_workspace_scoped_dashboard() {
let target = BackendTarget::new("http://127.0.0.1:8787", Some("workspace-a"));
+1
View File
@@ -10,6 +10,7 @@ e2e-test = []
[dependencies]
client = { workspace = true }
standalone = { workspace = true }
protocol = { workspace = true }
ratatui = { version = "0.30.0", features = ["scrolling-regions"] }
base64 = "0.22.1"
+114 -27
View File
@@ -22,7 +22,8 @@ use protocol::{Greeting, RewindSummary, RewindTarget, RewindTargetId, Segment};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use session_store::SegmentId;
use tokio::sync::mpsc;
use standalone::{StandaloneHost, StandaloneLaunchConfig};
use tokio::sync::{broadcast, mpsc};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use client::{BackendRuntimeClient, BackendRuntimeTarget, WorkerClient, WorkerRuntimeCommand};
@@ -174,13 +175,33 @@ pub(crate) async fn run_worker_name(
enum ConsoleConnection {
LegacySocket(WorkerClient),
BackendRuntime(BackendRuntimeClient),
Standalone {
host: Option<StandaloneHost>,
events: broadcast::Receiver<Event>,
initial_snapshot: Option<Event>,
},
}
impl ConsoleConnection {
fn standalone(host: StandaloneHost) -> Self {
let events = host.subscribe();
let initial_snapshot = Some(host.snapshot());
Self::Standalone {
host: Some(host),
events,
initial_snapshot,
}
}
fn try_next_event(&mut self) -> Option<Event> {
match self {
Self::LegacySocket(client) => client.try_next_event(),
Self::BackendRuntime(client) => client.try_next_event(),
Self::Standalone {
events,
initial_snapshot,
..
} => initial_snapshot.take().or_else(|| events.try_recv().ok()),
}
}
@@ -188,6 +209,18 @@ impl ConsoleConnection {
match self {
Self::LegacySocket(client) => client.next_event().await,
Self::BackendRuntime(client) => client.next_event().await,
Self::Standalone { host, events, .. } => loop {
match events.recv().await {
Ok(event) => break Some(event),
Err(broadcast::error::RecvError::Lagged(_)) => {
let Some(host) = host.as_ref() else {
break None;
};
break Some(host.snapshot());
}
Err(broadcast::error::RecvError::Closed) => break None,
}
},
}
}
@@ -195,8 +228,78 @@ impl ConsoleConnection {
match self {
Self::LegacySocket(client) => Ok(client.send(method).await?),
Self::BackendRuntime(client) => Ok(client.send(method).await?),
Self::Standalone { host, .. } => {
let host = host.as_ref().ok_or_else(|| {
io::Error::new(
io::ErrorKind::BrokenPipe,
"Standalone Worker has already shut down",
)
})?;
Ok(host.send(method.clone()).await?)
}
}
}
async fn shutdown(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if let Self::Standalone { host, .. } = self
&& let Some(host) = host.take()
{
host.shutdown().await?;
}
Ok(())
}
}
pub(crate) async fn run_standalone(
workspace_root: PathBuf,
state_dir: PathBuf,
worker_name: Option<String>,
profile: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let worker_name = worker_name.unwrap_or_else(|| "local".to_string());
let profile = profile.map_or(manifest::ProfileSelector::Default, |profile| {
manifest::ProfileSelector::parse_cli(&profile)
});
let history_root = workspace_root.clone();
let launch = StandaloneLaunchConfig {
state_dir,
cwd: workspace_root,
profile,
worker_name: worker_name.clone(),
}
.resolve()
.map_err(|error| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Standalone launch configuration failed: {error}"),
)
})?;
let host = StandaloneHost::start(launch)
.await
.map_err(|error| io::Error::other(format!("Standalone Worker startup failed: {error}")))?;
let mut connection = ConsoleConnection::standalone(host);
let mut terminal = match enter_fullscreen() {
Ok(terminal) => terminal,
Err(error) => {
let _ = connection.shutdown().await;
return Err(error);
}
};
let mut app = App::new_with_persistent_input_history(worker_name, &history_root);
let run_result = run_loop(&mut terminal, &mut app, &mut connection, None).await;
let shutdown_result = connection
.shutdown()
.await
.map_err(|error| io::Error::other(format!("Standalone Worker shutdown failed: {error}")));
let leave_result = leave_fullscreen(&mut terminal);
if let Err(error) = run_result {
return Err(error);
}
shutdown_result?;
leave_result?;
Ok(())
}
pub(crate) async fn run_backend_runtime(
@@ -208,13 +311,8 @@ pub(crate) async fn run_backend_runtime(
let workspace_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let mut app = App::new_with_persistent_input_history(worker_label, &workspace_root);
app.connected = true;
let result = run_loop(
&mut terminal,
&mut app,
ConsoleConnection::BackendRuntime(client),
None,
)
.await;
let mut connection = ConsoleConnection::BackendRuntime(client);
let result = run_loop(&mut terminal, &mut app, &mut connection, None).await;
let _ = leave_fullscreen(&mut terminal);
result
}
@@ -228,13 +326,8 @@ async fn run_connected_pod(
let workspace_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let mut app = App::new_with_persistent_input_history(worker_name, &workspace_root);
app.connected = true;
run_loop(
terminal,
&mut app,
ConsoleConnection::LegacySocket(client),
Some(runtime_command),
)
.await
let mut connection = ConsoleConnection::LegacySocket(client);
run_loop(terminal, &mut app, &mut connection, Some(runtime_command)).await
}
pub(crate) async fn open_from_dashboard(
@@ -460,13 +553,8 @@ async fn run(
app.connected = true;
// The Worker sends `Event::Snapshot` automatically on connect;
// no explicit method call is required to fetch history.
run_loop(
terminal,
&mut app,
ConsoleConnection::LegacySocket(client),
Some(runtime_command),
)
.await?;
let mut connection = ConsoleConnection::LegacySocket(client);
run_loop(terminal, &mut app, &mut connection, Some(runtime_command)).await?;
}
Err(e) => {
app.push_error(format!(
@@ -791,7 +879,7 @@ async fn drain_worker_events(
async fn run_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
mut client: ConsoleConnection,
client: &mut ConsoleConnection,
runtime_command: Option<WorkerRuntimeCommand>,
) -> Result<(), Box<dyn std::error::Error>> {
let (_terminal_reader, mut term_rx) = TerminalEventReader::spawn()?;
@@ -804,11 +892,11 @@ async fn run_loop(
}
let handled_term_event =
drain_terminal_events(app, &mut client, &mut term_rx, runtime_command.as_ref()).await?;
drain_terminal_events(app, client, &mut term_rx, runtime_command.as_ref()).await?;
if app.quit {
break;
}
let handled_worker_event = drain_worker_events(app, &mut client).await?;
let handled_worker_event = drain_worker_events(app, client).await?;
if handled_term_event || handled_worker_event {
terminal.draw(|f| ui::draw(f, app))?;
continue;
@@ -816,8 +904,7 @@ async fn run_loop(
match next_loop_input(&mut term_rx, app.connected, client.next_event()).await {
LoopInput::Terminal(term_event) => {
handle_terminal_event(app, &mut client, term_event?, runtime_command.as_ref())
.await?;
handle_terminal_event(app, client, term_event?, runtime_command.as_ref()).await?;
}
LoopInput::Worker(event) => match event {
Some(ev) => {
+58 -14
View File
@@ -36,7 +36,7 @@ use crossterm::execute;
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
use session_store::SegmentId;
use client::{Target, WorkerConnectionSelector, WorkerListRequest};
use client::{Target, WorkerConnectionSelector, WorkerListRequest, WorkerSpawn};
#[derive(Debug)]
pub struct LaunchOptions {
@@ -85,6 +85,49 @@ pub enum LaunchMode {
Panel { include_stopped: bool },
}
struct TerminalModeGuard {
active: bool,
}
impl TerminalModeGuard {
fn new() -> Self {
Self { active: true }
}
fn restore(&mut self) -> io::Result<()> {
if !self.active {
return Ok(());
}
self.active = false;
let mut stdout = io::stdout();
execute!(
stdout,
DisableMouseCapture,
LeaveAlternateScreen,
DisableBracketedPaste,
crossterm::cursor::Show
)?;
disable_raw_mode()
}
}
impl Drop for TerminalModeGuard {
fn drop(&mut self) {
if self.active {
let mut stdout = io::stdout();
let _ = execute!(
stdout,
DisableMouseCapture,
LeaveAlternateScreen,
DisableBracketedPaste,
crossterm::cursor::Show
);
let _ = disable_raw_mode();
self.active = false;
}
}
}
pub async fn launch(options: LaunchOptions) -> ExitCode {
let LaunchOptions {
target,
@@ -109,14 +152,19 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
eprintln!("yoi: {e}");
return ExitCode::FAILURE;
}
let mut terminal_mode = TerminalModeGuard::new();
let result = match mode {
LaunchMode::Spawn {
worker_name,
profile,
} => match target.spawn_worker() {
Ok(spawn) => {
console::run_spawn(None, worker_name, profile, spawn.runtime_command).await
Ok(WorkerSpawn::LegacyLocal { runtime_command }) => {
console::run_spawn(None, worker_name, profile, runtime_command).await
}
Ok(WorkerSpawn::Standalone { state_dir }) => {
console::run_standalone(workspace_root.clone(), state_dir, worker_name, profile)
.await
}
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
@@ -176,9 +224,13 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
LaunchMode::ResumeWithSession { id, worker_name } => match target.spawn_worker() {
Ok(spawn) => {
console::run_spawn(Some(id), worker_name, None, spawn.runtime_command).await
Ok(WorkerSpawn::LegacyLocal { runtime_command }) => {
console::run_spawn(Some(id), worker_name, None, runtime_command).await
}
Ok(WorkerSpawn::Standalone { .. }) => Err(Box::new(io::Error::new(
io::ErrorKind::Unsupported,
"Standalone session restore is not implemented",
)) as Box<dyn std::error::Error>),
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
},
LaunchMode::Panel { include_stopped } => match target.dashboard() {
@@ -198,15 +250,7 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
// alternate-screen buffer.
#[cfg(feature = "e2e-test")]
e2e_observer::emit("tui", "terminal_cleanup_started", serde_json::json!({}));
let mut stdout = io::stdout();
let _ = execute!(
stdout,
DisableMouseCapture,
LeaveAlternateScreen,
DisableBracketedPaste
);
let _ = disable_raw_mode();
let _ = execute!(stdout, crossterm::cursor::Show);
let _ = terminal_mode.restore();
#[cfg(feature = "e2e-test")]
e2e_observer::emit("tui", "terminal_cleanup_finished", serde_json::json!({}));
+24 -3
View File
@@ -1,4 +1,4 @@
use client::{BackendTarget, LocalTarget, Target, TargetKind};
use client::{BackendTarget, LocalTarget, StandaloneTarget, Target, TargetKind};
use serde::Deserialize;
use super::{ParseError, read_client_default_connection, resolve_backend_url};
@@ -107,6 +107,18 @@ pub(crate) trait CliConnectionResolver {
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct ClientConfigCliConnectionResolver;
fn standalone_target() -> Result<Box<dyn Target>, ParseError> {
let state_dir = manifest::paths::data_dir()
.ok_or_else(|| {
ParseError(
"Standalone state directory is unavailable; set YOI_DATA_DIR, YOI_HOME, XDG_DATA_HOME, or HOME"
.to_string(),
)
})?
.join("standalone");
Ok(Box::new(StandaloneTarget::new(state_dir)))
}
impl CliConnectionResolver for ClientConfigCliConnectionResolver {
fn resolve_connection(
&self,
@@ -147,6 +159,11 @@ impl CliConnectionResolver for ClientConfigCliConnectionResolver {
resolve_backend_url(explicit_backend_url, workspace_id)?,
workspace_id.map(str::to_string),
))),
(CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalTarget)
if command == CliCommand::DefaultTui =>
{
standalone_target()
}
(CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalTarget) => {
Ok(Box::new(LocalTarget::new()))
}
@@ -171,6 +188,10 @@ pub(crate) fn resolve_local_cli_connection<R: CliConnectionResolver + ?Sized>(
let target = resolver.resolve_connection(command, CliConnectionInput::LocalTarget)?;
match target.kind() {
TargetKind::Local => Ok(target),
TargetKind::Standalone => Err(ParseError(format!(
"{} resolved Standalone where a legacy local target was required",
command.display_name()
))),
TargetKind::Backend => Err(ParseError(format!(
"{} resolved a Backend target where a local target was required",
command.display_name()
@@ -193,8 +214,8 @@ pub(crate) fn resolve_backend_cli_connection<R: CliConnectionResolver + ?Sized>(
)?;
match target.kind() {
TargetKind::Backend => Ok(target),
TargetKind::Local => Err(ParseError(format!(
"{} resolved a local target where a Backend target was required",
TargetKind::Local | TargetKind::Standalone => Err(ParseError(format!(
"{} resolved a non-Backend target where a Backend target was required",
command.display_name()
))),
}
+98 -3
View File
@@ -687,6 +687,21 @@ fn parse_console_options<R: CliConnectionResolver + ?Sized>(
&workspace_root,
)?;
if target.kind() == TargetKind::Standalone {
if session.is_some() {
return Err(ParseError(
"--local starts a fresh Standalone Worker; --session restore requires the legacy local Runtime"
.to_string(),
));
}
if socket_override.is_some() {
return Err(ParseError(
"--local uses an in-process Standalone connection and does not accept --socket"
.to_string(),
));
}
}
if let (Some(runtime_id), Some(worker_id)) = (runtime_id.clone(), worker_id) {
return Ok(Mode::Tui {
target,
@@ -715,6 +730,11 @@ fn parse_console_options<R: CliConnectionResolver + ?Sized>(
worker_name,
profile: Some(profile),
}
} else if target.kind() == TargetKind::Standalone {
LaunchMode::Spawn {
worker_name,
profile: None,
}
} else if let Some(session) = session {
LaunchMode::ResumeWithSession {
id: parse_session_id(&session.to_string_lossy())?,
@@ -1650,7 +1670,7 @@ Usage:
Target selection:
Target options are top-level options and must appear before the command.
--local Use the local Worker runtime explicitly
--local Start a one-process Standalone Worker (no Server or Runtime)
--backend <URL> Use a Workspace Backend explicitly
--workspace-id <ID> Scope Backend routes to a Workspace id
@@ -1725,7 +1745,9 @@ fn print_memory_lint_help() {
mod tests {
use super::*;
use crate::cli_connection::CliConnectionInput;
use client::{BackendTarget, LocalTarget, TargetKind, WorkerListRequest};
use client::{
BackendTarget, LocalTarget, StandaloneTarget, Target, TargetKind, WorkerListRequest,
};
struct FixedCliConnectionResolver {
backend_url: &'static str,
@@ -1761,7 +1783,11 @@ mod tests {
let workspace_id = match input {
CliConnectionInput::DefaultTarget { workspace_id }
| CliConnectionInput::BackendTarget { workspace_id, .. } => workspace_id,
CliConnectionInput::LocalTarget => return Ok(Box::new(LocalTarget::new())),
CliConnectionInput::LocalTarget => {
return Ok(Box::new(StandaloneTarget::new(
"/tmp/yoi-test-standalone-state",
)));
}
};
Ok(Box::new(BackendTarget::new(
self.backend_url,
@@ -2026,6 +2052,75 @@ backend = "shared"
}
}
#[test]
fn parser_explicit_local_overrides_default_backend_with_standalone_target() {
let resolver = DefaultBackendCliConnectionResolver {
backend_url: "http://default-backend.example",
};
let args = ["--local", "--worker", "my-local-worker"]
.into_iter()
.map(String::from)
.collect::<Vec<_>>();
let mode = parse_args_slice_with_connection_resolver(&args, &resolver).unwrap();
let Mode::Tui { target, mode, .. } = mode else {
panic!("expected TUI mode")
};
assert_eq!(target.kind(), TargetKind::Standalone);
assert!(matches!(
mode,
LaunchMode::Spawn {
worker_name: Some(ref name),
profile: None,
} if name == "my-local-worker"
));
assert!(matches!(
target.spawn_worker().unwrap(),
client::WorkerSpawn::Standalone { .. }
));
}
#[test]
fn parser_rejects_standalone_and_backend_target_together() {
let err = parse_args_from(["--local", "--backend", "http://backend.example"]).unwrap_err();
assert_eq!(
err.to_string(),
"--local and --backend are mutually exclusive"
);
}
#[test]
fn parser_standalone_rejects_legacy_session_and_socket_inputs() {
let resolver = DefaultBackendCliConnectionResolver {
backend_url: "http://default-backend.example",
};
let session_args = ["--local", "--session", "session-1"]
.into_iter()
.map(String::from)
.collect::<Vec<_>>();
let err = parse_args_slice_with_connection_resolver(&session_args, &resolver).unwrap_err();
assert_eq!(
err.0,
"--local starts a fresh Standalone Worker; --session restore requires the legacy local Runtime"
);
let socket_args = [
"--local",
"--worker",
"worker-a",
"--socket",
"/tmp/worker.sock",
]
.into_iter()
.map(String::from)
.collect::<Vec<_>>();
let err = parse_args_slice_with_connection_resolver(&socket_args, &resolver).unwrap_err();
assert_eq!(
err.0,
"--local uses an in-process Standalone connection and does not accept --socket"
);
}
#[test]
fn parse_backend_runtime_target_rejects_legacy_socket_mix() {
let err = parse_args_from([
+3
View File
@@ -179,6 +179,9 @@ pub fn run(
})?;
run_in_workspace(cli, &workspace)
}
ResolvedTarget::Standalone => Err(ObjectiveCliError::new(
"Standalone is a one-shot Worker host, not Objective storage authority",
)),
ResolvedTarget::Backend {
base_url,
workspace_id,
+3
View File
@@ -214,6 +214,9 @@ pub fn run(cli: TicketCli, target: ResolvedTarget) -> Result<TicketCliOutput, Ti
})?;
run_in_workspace(cli, &workspace)
}
ResolvedTarget::Standalone => Err(TicketCliError::new(
"Standalone is a one-shot Worker host, not Ticket storage authority",
)),
ResolvedTarget::Backend {
base_url,
workspace_id,