cli: introduce connection target abstraction

This commit is contained in:
2026-07-29 02:18:02 +09:00
parent 450e0cddbd
commit 98c1599d1a
6 changed files with 903 additions and 63 deletions
+299
View File
@@ -0,0 +1,299 @@
use client::{BackendTarget, LocalTarget, Target, TargetKind};
use serde::Deserialize;
use super::{ParseError, resolve_backend_url};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CliConnectionRequirement {
LocalOnly,
BackendOnly,
ConnectionAware,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CliCommand {
DefaultTui,
Workers,
Resume,
Panel,
Keys,
SetupModel,
WorkerRuntime,
WorkerCleanup,
Ticket,
Objective,
Plugin,
Mcp,
MemoryLint,
Session,
WorkspaceServer,
Login,
}
impl CliCommand {
pub(crate) fn display_name(self) -> &'static str {
match self {
CliCommand::DefaultTui => "yoi",
CliCommand::Workers => "yoi workers",
CliCommand::Resume => "yoi resume",
CliCommand::Panel => "yoi panel",
CliCommand::Keys => "yoi keys",
CliCommand::SetupModel => "yoi setup-model",
CliCommand::WorkerRuntime => "yoi worker",
CliCommand::WorkerCleanup => "yoi worker management",
CliCommand::Ticket => "yoi ticket",
CliCommand::Objective => "yoi objective",
CliCommand::Plugin => "yoi plugin",
CliCommand::Mcp => "yoi mcp",
CliCommand::MemoryLint => "yoi memory lint",
CliCommand::Session => "yoi session",
CliCommand::WorkspaceServer => "yoi workspace/server",
CliCommand::Login => "yoi login",
}
}
pub(crate) fn connection_requirement(self) -> CliConnectionRequirement {
match self {
CliCommand::DefaultTui => CliConnectionRequirement::ConnectionAware,
CliCommand::Workers | CliCommand::Login => CliConnectionRequirement::BackendOnly,
CliCommand::Resume
| CliCommand::Panel
| CliCommand::Keys
| CliCommand::SetupModel
| CliCommand::WorkerRuntime
| CliCommand::WorkerCleanup
| CliCommand::Ticket
| CliCommand::Objective
| CliCommand::Plugin
| CliCommand::Mcp
| CliCommand::MemoryLint
| CliCommand::Session
| CliCommand::WorkspaceServer => CliConnectionRequirement::LocalOnly,
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub(crate) enum ClientDefaultConnection {
Local,
Backend,
}
impl Default for ClientDefaultConnection {
fn default() -> Self {
Self::Local
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CliConnectionInput<'a> {
LocalDefault,
BackendTarget {
explicit_backend_url: Option<String>,
workspace_id: Option<&'a str>,
},
}
pub(crate) trait CliConnectionResolver {
fn resolve_connection(
&self,
command: CliCommand,
input: CliConnectionInput<'_>,
) -> Result<Box<dyn Target>, ParseError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct ClientConfigCliConnectionResolver;
impl CliConnectionResolver for ClientConfigCliConnectionResolver {
fn resolve_connection(
&self,
command: CliCommand,
input: CliConnectionInput<'_>,
) -> Result<Box<dyn Target>, ParseError> {
match (command.connection_requirement(), input) {
(CliConnectionRequirement::LocalOnly, CliConnectionInput::LocalDefault) => {
Ok(Box::new(LocalTarget::new()))
}
(CliConnectionRequirement::LocalOnly, CliConnectionInput::BackendTarget { .. }) => {
Err(ParseError(format!(
"{} uses a local connection target and cannot accept Backend target options",
command.display_name()
)))
}
(CliConnectionRequirement::BackendOnly, CliConnectionInput::LocalDefault) => {
Err(ParseError(format!(
"{} requires a Backend connection target",
command.display_name()
)))
}
(
CliConnectionRequirement::BackendOnly | CliConnectionRequirement::ConnectionAware,
CliConnectionInput::BackendTarget {
explicit_backend_url,
workspace_id,
},
) => Ok(Box::new(BackendTarget::new(
resolve_backend_url(explicit_backend_url, workspace_id)?,
workspace_id.map(str::to_string),
))),
(CliConnectionRequirement::ConnectionAware, CliConnectionInput::LocalDefault) => {
Ok(Box::new(LocalTarget::new()))
}
}
}
}
pub(crate) fn resolve_local_cli_connection<R: CliConnectionResolver + ?Sized>(
resolver: &R,
command: CliCommand,
) -> Result<Box<dyn Target>, ParseError> {
let target = resolver.resolve_connection(command, CliConnectionInput::LocalDefault)?;
match target.kind() {
TargetKind::Local => Ok(target),
TargetKind::Backend => Err(ParseError(format!(
"{} resolved a Backend target where a local target was required",
command.display_name()
))),
}
}
pub(crate) fn resolve_backend_cli_connection<R: CliConnectionResolver + ?Sized>(
resolver: &R,
command: CliCommand,
explicit_backend_url: Option<String>,
workspace_id: Option<&str>,
) -> Result<Box<dyn Target>, ParseError> {
let target = resolver.resolve_connection(
command,
CliConnectionInput::BackendTarget {
explicit_backend_url,
workspace_id,
},
)?;
match target.kind() {
TargetKind::Backend => Ok(target),
TargetKind::Local => Err(ParseError(format!(
"{} resolved a local target where a Backend target was required",
command.display_name()
))),
}
}
pub(crate) fn backend_target_option_error_for_local_command(
command: CliCommand,
option: &str,
) -> ParseError {
ParseError(format!(
"{} uses a local connection target and cannot accept Backend target option `{option}`",
command.display_name()
))
}
pub(crate) fn is_backend_target_option(arg: &str) -> bool {
matches!(
arg,
"--backend" | "--workspace-id" | "--runtime-id" | "--runtime" | "--worker-id"
) || arg.starts_with("--backend=")
|| arg.starts_with("--workspace-id=")
|| arg.starts_with("--runtime-id=")
|| arg.starts_with("--runtime=")
|| arg.starts_with("--worker-id=")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_command_connection_requirements_are_explicit() {
assert_eq!(
CliCommand::DefaultTui.connection_requirement(),
CliConnectionRequirement::ConnectionAware
);
assert_eq!(
CliCommand::Workers.connection_requirement(),
CliConnectionRequirement::BackendOnly
);
assert_eq!(
CliCommand::Login.connection_requirement(),
CliConnectionRequirement::BackendOnly
);
for command in [
CliCommand::Resume,
CliCommand::Panel,
CliCommand::Keys,
CliCommand::SetupModel,
CliCommand::WorkerRuntime,
CliCommand::WorkerCleanup,
CliCommand::Ticket,
CliCommand::Objective,
CliCommand::Plugin,
CliCommand::Mcp,
CliCommand::MemoryLint,
CliCommand::Session,
CliCommand::WorkspaceServer,
] {
assert_eq!(
command.connection_requirement(),
CliConnectionRequirement::LocalOnly,
"{} should be local-only",
command.display_name()
);
}
}
#[test]
fn cli_connection_resolver_rejects_local_only_backend_target() {
let resolver = ClientConfigCliConnectionResolver;
let err = resolver
.resolve_connection(
CliCommand::Resume,
CliConnectionInput::BackendTarget {
explicit_backend_url: Some("http://127.0.0.1:8787".to_string()),
workspace_id: None,
},
)
.unwrap_err();
assert_eq!(
err.to_string(),
"yoi resume uses a local connection target and cannot accept Backend target options"
);
}
#[test]
fn cli_connection_resolver_rejects_backend_only_local_default() {
let resolver = ClientConfigCliConnectionResolver;
let err = resolver
.resolve_connection(CliCommand::Workers, CliConnectionInput::LocalDefault)
.unwrap_err();
assert_eq!(
err.to_string(),
"yoi workers requires a Backend connection target"
);
}
#[test]
fn cli_connection_resolver_selects_backend_for_connection_aware_target() {
let resolver = ClientConfigCliConnectionResolver;
let target = resolve_backend_cli_connection(
&resolver,
CliCommand::DefaultTui,
Some("http://127.0.0.1:8787".to_string()),
None,
)
.unwrap();
assert_eq!(target.kind(), TargetKind::Backend);
let workers = target
.list_workers(client::WorkerListRequest::new(None))
.unwrap();
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
assert_eq!(workers.target.workspace_id, None);
}
}
+235 -43
View File
@@ -1,3 +1,4 @@
mod cli_connection;
mod mcp_cli;
mod memory_lint;
mod objective_cli;
@@ -14,10 +15,12 @@ use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::time::Duration;
use client::{
BackendAuthTarget, BackendRuntimeListTarget, BackendRuntimeTarget, WorkerRuntimeCommand,
start_device_login, wait_for_device_login,
use cli_connection::{
CliCommand, CliConnectionResolver, ClientConfigCliConnectionResolver, ClientDefaultConnection,
backend_target_option_error_for_local_command, is_backend_target_option,
resolve_backend_cli_connection, resolve_local_cli_connection,
};
use client::{BackendAuthTarget, Target, start_device_login, wait_for_device_login};
use memory_lint::{LintCliOptions, LintStatus};
use serde::{Deserialize, Serialize};
use session_store::SegmentId;
@@ -48,6 +51,7 @@ enum Mode {
Keys,
SetupModel,
Tui {
target: Box<dyn Target>,
mode: LaunchMode,
workspace_root: PathBuf,
},
@@ -181,19 +185,13 @@ async fn main() -> ExitCode {
Mode::Keys => tui::keys::launch().await,
Mode::SetupModel => tui::setup_model::launch().await,
Mode::Tui {
target,
mode,
workspace_root,
} => {
let runtime_command = match WorkerRuntimeCommand::resolve() {
Ok(command) => command,
Err(e) => {
eprintln!("yoi: failed to resolve Worker runtime command: {e}");
return ExitCode::FAILURE;
}
};
tui::launch(LaunchOptions {
target,
mode,
runtime_command,
workspace_root,
})
.await
@@ -215,8 +213,18 @@ where
}
fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
let resolver = ClientConfigCliConnectionResolver;
parse_args_slice_with_connection_resolver(args, &resolver)
}
fn parse_args_slice_with_connection_resolver<R: CliConnectionResolver + ?Sized>(
args: &[String],
connection_resolver: &R,
) -> Result<Mode, ParseError> {
if args.is_empty() {
let target = resolve_local_cli_connection(connection_resolver, CliCommand::DefaultTui)?;
return Ok(Mode::Tui {
target,
mode: LaunchMode::Spawn {
worker_name: None,
profile: None,
@@ -227,69 +235,101 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
match args[0].as_str() {
"--help" | "-h" => return Ok(Mode::Help),
"resume" => return parse_resume_args(&args[1..]),
"workers" => return parse_workers_args(&args[1..]),
"resume" => return parse_resume_args(&args[1..], connection_resolver),
"workers" => return parse_workers_args(&args[1..], connection_resolver),
"worker" => {
if let Some(cli) = worker_cleanup_cli::parse_worker_management_args(&args[1..])
.map_err(|e| ParseError(e.to_string()))?
{
let _target =
resolve_local_cli_connection(connection_resolver, CliCommand::WorkerCleanup)?;
return Ok(Mode::WorkerCleanup(cli));
}
let _target =
resolve_local_cli_connection(connection_resolver, CliCommand::WorkerRuntime)?;
return Ok(Mode::WorkerRuntime(args[1..].to_vec()));
}
"objective" => {
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Objective)?;
let objective_cli = objective_cli::parse_objective_args(&args[1..])
.map_err(|e| ParseError(e.to_string()))?;
return Ok(Mode::Objective(objective_cli));
}
"session" => {
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Session)?;
let session_cli = session_cli::parse_session_args(&args[1..])
.map_err(|e| ParseError(e.to_string()))?;
return Ok(Mode::Session(session_cli));
}
"ticket" => {
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Ticket)?;
let ticket_cli =
ticket_cli::parse_ticket_args(&args[1..]).map_err(|e| ParseError(e.to_string()))?;
return Ok(Mode::Ticket(ticket_cli));
}
"plugin" => {
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Plugin)?;
let plugin_cli = parse_plugin_args(&args[1..])?;
return Ok(Mode::Plugin(plugin_cli));
}
"workspace" => {
let _target =
resolve_local_cli_connection(connection_resolver, CliCommand::WorkspaceServer)?;
return parse_workspace_args(&args[1..]);
}
"server" => {
let _target =
resolve_local_cli_connection(connection_resolver, CliCommand::WorkspaceServer)?;
return parse_workspace_args(&args[1..]);
}
"login" => {
return parse_login_args(&args[1..]);
return parse_login_args(&args[1..], connection_resolver);
}
"mcp" => {
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Mcp)?;
let mcp_cli = parse_mcp_args(&args[1..])?;
return Ok(Mode::Mcp(mcp_cli));
}
"panel" => {
let target = resolve_local_cli_connection(connection_resolver, CliCommand::Panel)?;
return Ok(Mode::Tui {
target,
mode: LaunchMode::Panel,
workspace_root: parse_panel_workspace(&args[1..])?,
});
}
"keys" => {
if args.len() != 1 {
if let Some(arg) = args[1..].iter().find(|arg| is_backend_target_option(arg)) {
return Err(backend_target_option_error_for_local_command(
CliCommand::Keys,
arg,
));
}
return Err(ParseError("yoi keys does not accept arguments".into()));
}
let _target = resolve_local_cli_connection(connection_resolver, CliCommand::Keys)?;
return Ok(Mode::Keys);
}
"setup-model" => {
if args.len() != 1 {
if let Some(arg) = args[1..].iter().find(|arg| is_backend_target_option(arg)) {
return Err(backend_target_option_error_for_local_command(
CliCommand::SetupModel,
arg,
));
}
return Err(ParseError(
"yoi setup-model does not accept arguments".into(),
));
}
let _target =
resolve_local_cli_connection(connection_resolver, CliCommand::SetupModel)?;
return Ok(Mode::SetupModel);
}
"memory" if args.get(1).map(String::as_str) == Some("lint") => {
let _target =
resolve_local_cli_connection(connection_resolver, CliCommand::MemoryLint)?;
let lint_args = &args[2..];
if lint_args.iter().any(|arg| arg == "--help" || arg == "-h") {
return Ok(Mode::MemoryLintHelp);
@@ -309,10 +349,13 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
_ => {}
}
parse_console_options(args)
parse_console_options(args, connection_resolver)
}
fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
fn parse_console_options<R: CliConnectionResolver + ?Sized>(
args: &[String],
connection_resolver: &R,
) -> Result<Mode, ParseError> {
let mut workspace_root = current_dir()?;
let mut session = None;
let mut worker_name = None;
@@ -544,25 +587,34 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
Some(workspace_id) => Some(workspace_id),
None => resolve_workspace_id_from_root(&workspace_root)?,
};
let backend_url = resolve_backend_url(backend_url, workspace_id.as_deref())?;
let target = resolve_backend_cli_connection(
connection_resolver,
CliCommand::DefaultTui,
backend_url,
workspace_id.as_deref(),
)?;
if let (Some(runtime_id), Some(worker_id)) = (runtime_id.clone(), worker_id) {
return Ok(Mode::Tui {
mode: LaunchMode::BackendRuntime {
target: BackendRuntimeTarget::new(backend_url, runtime_id, worker_id),
target,
mode: LaunchMode::OpenWorker {
runtime_id,
worker_id,
},
workspace_root,
});
}
return Ok(Mode::Tui {
mode: LaunchMode::BackendRuntimePicker {
target: BackendRuntimeListTarget::new(backend_url, workspace_id, runtime_id),
},
target,
mode: LaunchMode::Workers { runtime_id },
workspace_root,
});
}
let target = resolve_local_cli_connection(connection_resolver, CliCommand::DefaultTui)?;
if let Some(profile) = profile {
return Ok(Mode::Tui {
target,
mode: LaunchMode::Spawn {
worker_name,
profile: Some(profile),
@@ -572,12 +624,14 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
}
if let Some(id) = session {
return Ok(Mode::Tui {
target,
mode: LaunchMode::ResumeWithSession { id, worker_name },
workspace_root,
});
}
if let Some(worker_name) = worker_name {
return Ok(Mode::Tui {
target,
mode: LaunchMode::WorkerName {
worker_name,
socket_override,
@@ -586,6 +640,7 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
});
}
Ok(Mode::Tui {
target,
mode: LaunchMode::Spawn {
worker_name: None,
profile: None,
@@ -594,7 +649,10 @@ fn parse_console_options(args: &[String]) -> Result<Mode, ParseError> {
})
}
fn parse_workers_args(args: &[String]) -> Result<Mode, ParseError> {
fn parse_workers_args<R: CliConnectionResolver + ?Sized>(
args: &[String],
connection_resolver: &R,
) -> Result<Mode, ParseError> {
let mut workspace_root = current_dir()?;
let mut workspace_id = None;
let mut backend_url = None;
@@ -704,16 +762,23 @@ fn parse_workers_args(args: &[String]) -> Result<Mode, ParseError> {
Some(workspace_id) => Some(workspace_id),
None => resolve_workspace_id_from_root(&workspace_root)?,
};
let backend_url = resolve_backend_url(backend_url, workspace_id.as_deref())?;
let target = resolve_backend_cli_connection(
connection_resolver,
CliCommand::Workers,
backend_url,
workspace_id.as_deref(),
)?;
Ok(Mode::Tui {
mode: LaunchMode::BackendRuntimePicker {
target: BackendRuntimeListTarget::new(backend_url, workspace_id, runtime_id),
},
target,
mode: LaunchMode::Workers { runtime_id },
workspace_root,
})
}
fn parse_resume_args(args: &[String]) -> Result<Mode, ParseError> {
fn parse_resume_args<R: CliConnectionResolver + ?Sized>(
args: &[String],
connection_resolver: &R,
) -> Result<Mode, ParseError> {
let mut workspace_root = current_dir()?;
let mut workspace_set = false;
let mut all = false;
@@ -755,6 +820,12 @@ fn parse_resume_args(args: &[String]) -> Result<Mode, ParseError> {
i += 1;
}
arg if arg.starts_with('-') => {
if is_backend_target_option(arg) {
return Err(backend_target_option_error_for_local_command(
CliCommand::Resume,
arg,
));
}
return Err(ParseError(format!("unknown yoi resume option `{arg}`")));
}
value => {
@@ -771,7 +842,10 @@ fn parse_resume_args(args: &[String]) -> Result<Mode, ParseError> {
));
}
let target = resolve_local_cli_connection(connection_resolver, CliCommand::Resume)?;
Ok(Mode::Tui {
target,
mode: LaunchMode::Resume { all },
workspace_root,
})
@@ -791,6 +865,11 @@ struct WorkspaceIdentityFile {
#[derive(Debug, Default, Deserialize)]
struct ClientConfigFile {
default_backend: Option<String>,
// Parsed as part of the target model; applying configured connection defaults is left to
// the later user-facing connection-mode work so current `yoi` default launch stays local.
#[allow(dead_code)]
#[serde(default)]
default_connection: ClientDefaultConnection,
#[serde(default)]
backends: BTreeMap<String, ClientBackendConfig>,
#[serde(default)]
@@ -895,7 +974,10 @@ fn client_config_missing_message(workspace_id: Option<&str>) -> String {
}
}
fn parse_login_args(args: &[String]) -> Result<Mode, ParseError> {
fn parse_login_args<R: CliConnectionResolver + ?Sized>(
args: &[String],
connection_resolver: &R,
) -> Result<Mode, ParseError> {
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
return Err(ParseError(
"yoi login usage: yoi login [--backend <URL>] [--no-wait]".to_string(),
@@ -935,6 +1017,12 @@ fn parse_login_args(args: &[String]) -> Result<Mode, ParseError> {
}
}
}
let _target = resolve_backend_cli_connection(
connection_resolver,
CliCommand::Login,
backend_url.clone(),
None,
)?;
let backend_url = resolve_backend_url(backend_url, None)?;
Ok(Mode::Login {
backend_url,
@@ -1423,6 +1511,13 @@ fn mcp_usage() -> &'static str {
}
fn parse_panel_workspace(args: &[String]) -> Result<PathBuf, ParseError> {
if let Some(arg) = args.iter().find(|arg| is_backend_target_option(arg)) {
return Err(backend_target_option_error_for_local_command(
CliCommand::Panel,
arg,
));
}
match args {
[] => std::env::current_dir()
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}"))),
@@ -1484,6 +1579,77 @@ fn print_memory_lint_help() {
#[cfg(test)]
mod tests {
use super::*;
use crate::cli_connection::CliConnectionInput;
use client::{BackendTarget, LocalTarget, TargetKind, WorkerListRequest};
struct FixedCliConnectionResolver {
backend_url: &'static str,
}
impl CliConnectionResolver for FixedCliConnectionResolver {
fn resolve_connection(
&self,
_command: CliCommand,
input: CliConnectionInput<'_>,
) -> Result<Box<dyn Target>, ParseError> {
match input {
CliConnectionInput::LocalDefault => Ok(Box::new(LocalTarget::new())),
CliConnectionInput::BackendTarget { workspace_id, .. } => Ok(Box::new(
BackendTarget::new(self.backend_url, workspace_id.map(str::to_string)),
)),
}
}
}
#[test]
fn parser_uses_injected_cli_connection_resolver_for_backend_command() {
let resolver = FixedCliConnectionResolver {
backend_url: "http://fake-backend.example",
};
let args = vec!["workers".to_string()];
let mode = parse_args_slice_with_connection_resolver(&args, &resolver).unwrap();
match mode {
Mode::Tui {
target,
mode: LaunchMode::Workers { runtime_id },
..
} => {
assert_eq!(runtime_id, None);
assert_eq!(target.kind(), TargetKind::Backend);
let workers = target.list_workers(WorkerListRequest::new(None)).unwrap();
assert_eq!(workers.target.base_url, "http://fake-backend.example");
}
other => panic!("expected Workers mode, got {other:?}"),
}
}
#[test]
fn client_config_default_connection_defaults_to_local() {
let config = ClientConfigFile::default();
assert_eq!(config.default_connection, ClientDefaultConnection::Local);
}
#[test]
fn parse_local_only_commands_reject_backend_target_options() {
let err = parse_args_from(["resume", "--backend", "http://127.0.0.1:8787"]).unwrap_err();
assert_eq!(
err.to_string(),
"yoi resume uses a local connection target and cannot accept Backend target option `--backend`"
);
let err = parse_args_from(["panel", "--runtime-id", "runtime-a"]).unwrap_err();
assert_eq!(
err.to_string(),
"yoi panel uses a local connection target and cannot accept Backend target option `--runtime-id`"
);
let err = parse_args_from(["keys", "--workspace-id=workspace-a"]).unwrap_err();
assert_eq!(
err.to_string(),
"yoi keys uses a local connection target and cannot accept Backend target option `--workspace-id=workspace-a`"
);
}
#[test]
fn parse_worker_name_mode() {
@@ -1516,14 +1682,26 @@ mod tests {
.unwrap()
{
Mode::Tui {
mode: LaunchMode::BackendRuntime { target },
target,
mode:
LaunchMode::OpenWorker {
runtime_id,
worker_id,
},
..
} => {
assert_eq!(target.base_url, "http://127.0.0.1:8787");
assert_eq!(target.runtime_id, "runtime-a");
assert_eq!(target.worker_id, "worker-b");
assert_eq!(target.kind(), TargetKind::Backend);
let connection = target
.connect_worker(client::WorkerConnectionSelector::new(
runtime_id.clone(),
worker_id.clone(),
))
.unwrap();
assert_eq!(connection.target.base_url, "http://127.0.0.1:8787");
assert_eq!(runtime_id, "runtime-a");
assert_eq!(worker_id, "worker-b");
}
_ => panic!("expected BackendRuntime mode"),
_ => panic!("expected OpenWorker mode"),
}
}
@@ -1542,13 +1720,19 @@ mod tests {
match parse_args_from(["--backend", "http://127.0.0.1:8787", "--runtime-id", "r"]).unwrap()
{
Mode::Tui {
mode: LaunchMode::BackendRuntimePicker { target },
target,
mode: LaunchMode::Workers { runtime_id },
..
} => {
assert_eq!(target.base_url, "http://127.0.0.1:8787");
assert_eq!(target.runtime_id.as_deref(), Some("r"));
assert_eq!(target.kind(), TargetKind::Backend);
let workers = target
.list_workers(WorkerListRequest::new(runtime_id.clone()))
.unwrap();
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
assert_eq!(workers.target.runtime_id.as_deref(), Some("r"));
assert_eq!(runtime_id.as_deref(), Some("r"));
}
_ => panic!("expected BackendRuntimePicker mode"),
_ => panic!("expected Workers mode"),
}
}
@@ -1564,14 +1748,19 @@ mod tests {
.unwrap()
{
Mode::Tui {
mode: LaunchMode::BackendRuntimePicker { target },
target,
mode: LaunchMode::Workers { runtime_id },
..
} => {
assert_eq!(target.base_url, "http://127.0.0.1:8787");
assert_eq!(target.workspace_id.as_deref(), Some("workspace-a"));
assert_eq!(target.runtime_id, None);
assert_eq!(target.kind(), TargetKind::Backend);
let workers = target
.list_workers(WorkerListRequest::new(runtime_id.clone()))
.unwrap();
assert_eq!(workers.target.base_url, "http://127.0.0.1:8787");
assert_eq!(workers.target.workspace_id.as_deref(), Some("workspace-a"));
assert_eq!(runtime_id, None);
}
_ => panic!("expected BackendRuntimePicker mode"),
_ => panic!("expected Workers mode"),
}
}
@@ -1623,6 +1812,7 @@ mod tests {
Mode::Tui {
mode: LaunchMode::Resume { all },
workspace_root,
..
} => {
assert!(!all);
assert_eq!(workspace_root, PathBuf::from("/tmp/resume-workspace"));
@@ -1994,6 +2184,7 @@ mod tests {
profile,
},
workspace_root,
..
} => {
assert_eq!(worker_name, Some("agent".to_string()));
assert_eq!(profile, Some("project:companion".to_string()));
@@ -2039,6 +2230,7 @@ mod tests {
Mode::Tui {
mode: LaunchMode::Panel,
workspace_root,
..
} => assert_eq!(workspace_root, PathBuf::from("/tmp/other-workspace")),
_ => panic!("expected Panel mode"),
}