use std::io::Write;
use crossterm::event::KeyCode;
use ratatui::{
Frame,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use runner_manager_domain::attempt::active_count_for;
use runner_manager_domain::capacity::HostAllocator;
use runner_manager_domain::execution::{Backend, ExecutionPolicy};
use runner_manager_domain::model::{
Arch, CachePolicy, HostLabel, Os, PolicyId, ProfileName, RefreshInterval, ScaleTarget,
StartMode, TargetScope,
};
use runner_manager_domain::path::LocalAbsolutePath;
#[cfg(test)]
use runner_manager_domain::policy::ScalePolicy;
use runner_manager_domain::policy::{PolicyMode, RoutingLabels};
use runner_manager_domain::store::{Store, StoreError};
use runner_manager_domain::workspace::WorkspaceKind;
use runner_manager_platform::runner_root::RootOwner;
use runner_manager_platform::service::ServiceError;
use super::path_field::PathField;
use crate::cli::workspace::{AffectedAttempts, HostRoot, RepositoryWorkspace};
use crate::cli::{
self, BackendMode, CliError, Context, ExecutionMode, Failure, HostSetCapacityArgs,
IsolationArgs, RepoProfileAddArgs, RepoProfileCommand, RepoProfileExecutionArgs,
RepoProfileSelectArgs, RepoProfileWorkspaceArgs, WorkspaceMode,
};
#[cfg(test)]
use crate::cli::{
OrgCommand, OrgSetCapacityArgs, OrgSetScaleArgs, RepoCommand, RepoSetCapacityArgs,
RepoSetScaleArgs, RepoSetWorkspaceArgs,
};
pub const MAX_FOCUSED_FORM_ACTIONS: u8 = 5;
pub const FORK_TRUST_WARNING: &str = "warning: fork and untrusted pull-request workflows must not run on a personal host until you explicitly accept that trust boundary.";
pub const STATIC_SELECTOR_WARNING: &str = "Static runs-on labels required; selector optional. Overlapping profiles start no runner; matrix expressions are not resolved.";
pub const ORGANIZATION_WORKSPACE_MODE: &str =
"Workspace mode: ephemeral, and it cannot be changed for an organization.";
pub const ORGANIZATION_WORKSPACE_EXPLANATION: &str = "An organization runner can accept jobs from more than one repository, so a retained _work \
directory would cross a repository boundary. Persistent workspaces are repository-scoped.";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostSettings {
pub current_capacity: u16,
pub current_in_use: u16,
pub current_start_mode: StartMode,
pub current_refresh_interval: RefreshInterval,
pub projected_requests_per_hour: u32,
pub maximum_repository_targets: u32,
pub projection_is_floor: bool,
pub runner_root: HostRoot,
pub affected: AffectedAttempts,
targets: Vec<ScaleTarget>,
}
impl HostSettings {
pub fn load(context: &Context) -> Result<Self, CliError> {
let store = context.store()?;
let host = cli::host::local_host_or_create(context, &store)?;
let attempts = store.attempts().map_err(store_failure)?;
let current_in_use = HostAllocator::from_attempts(&host, attempts.iter()).active_total();
let targets = store
.policies()
.map_err(store_failure)?
.into_iter()
.map(|policy| policy.target)
.collect::<Vec<_>>();
let budget = cli::host::HostBudget::of(host.refresh_interval, &targets);
Ok(Self {
current_capacity: host.host_capacity(),
current_in_use,
current_start_mode: host.service_start_mode,
current_refresh_interval: host.refresh_interval,
projected_requests_per_hour: budget.requests_per_hour(),
maximum_repository_targets: budget.max_repository_targets(),
projection_is_floor: budget.is_floor(),
runner_root: cli::workspace::host_root(context.paths(), Some(&host)),
affected: cli::workspace::host_affected_attempts(&store)?,
targets,
})
}
#[must_use]
pub fn configured_root_text(&self) -> String {
self.runner_root
.configured
.as_ref()
.map_or_else(String::new, |root| root.as_str().to_owned())
}
pub fn save_runner_root(
context: &Context,
raw: Option<&str>,
out: &mut dyn Write,
) -> Result<(), CliError> {
cli::host::runtime_root(context, raw, cli::Styling::plain_for_buffer(), out)
}
#[must_use]
pub const fn workspace_action_count() -> u8 {
3
}
pub fn preview_interval(&self, seconds: u16) -> Result<HostIntervalPreview, CliError> {
let interval = RefreshInterval::from_secs(seconds).map_err(invalid)?;
let budget = cli::host::HostBudget::of(interval, &self.targets);
Ok(HostIntervalPreview {
interval,
projected_requests_per_hour: budget.requests_per_hour(),
maximum_repository_targets: budget.max_repository_targets(),
projection_is_floor: budget.is_floor(),
over_budget: budget.exceeds_allowance(),
})
}
pub fn set_capacity(
context: &Context,
capacity: u16,
out: &mut dyn Write,
) -> Result<(), CliError> {
cli::host::set_capacity(context, &HostSetCapacityArgs { capacity }, out)
}
pub fn set_refresh_interval(
&self,
context: &Context,
seconds: u16,
) -> Result<HostIntervalPreview, CliError> {
let preview = self.accepted_interval_preview(seconds)?;
let store = context.store()?;
let mut host = cli::host::local_host_or_create(context, &store)?;
host.refresh_interval = preview.interval;
store.put_host(&host).map_err(store_failure)?;
Ok(preview)
}
fn accepted_interval_preview(&self, seconds: u16) -> Result<HostIntervalPreview, CliError> {
let preview = self.preview_interval(seconds)?;
if preview.over_budget {
return Err(CliError::with_remedy(
Failure::BudgetRefused,
format!(
"a {}-second refresh projects {} requests/hour for the configured targets; nothing was changed",
preview.interval.as_secs(),
preview.projected_requests_per_hour
),
"choose a longer refresh interval or remove a target",
));
}
Ok(preview)
}
pub fn set_start_mode(context: &Context, mode: StartMode) -> Result<(), CliError> {
Self::set_start_mode_with(context, mode, |mode| {
cli::service::operations(context)
.set_start_mode(mode)
.map(|_| ())
.map_err(service_failure)
})
}
fn set_start_mode_with(
context: &Context,
mode: StartMode,
switch: impl FnOnce(StartMode) -> Result<(), CliError>,
) -> Result<(), CliError> {
switch(mode)?;
let store = context.store()?;
let mut host = cli::host::local_host_or_create(context, &store)?;
host.service_start_mode = mode;
store.put_host(&host).map_err(store_failure)
}
#[must_use]
pub const fn focused_action_count(&self) -> u8 {
4
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostIntervalPreview {
pub interval: RefreshInterval,
pub projected_requests_per_hour: u32,
pub maximum_repository_targets: u32,
pub projection_is_floor: bool,
pub over_budget: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicySettings {
pub policy_id: PolicyId,
pub target: ScaleTarget,
pub profile_name: String,
pub host_identity: String,
pub requested_host_label: HostLabel,
pub host_os: Os,
pub host_arch: Arch,
pub mode: SettingsPolicyMode,
pub enabled: bool,
pub current_max_capacity: Option<u16>,
pub host_label: Option<String>,
pub extra_labels: Vec<String>,
pub copyable_runs_on: Option<String>,
pub cache_policy: CachePolicy,
pub active_runners: u16,
pub execution_policy: ExecutionPolicy,
pub provider_diagnostics: Vec<cli::host::IsolationCapability>,
pub workspace: RepositoryWorkspace,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingsPolicyMode {
Autoscale,
MonitorOnly,
}
impl PolicySettings {
#[cfg(test)]
pub fn load(context: &Context, target: &ScaleTarget) -> Result<Self, CliError> {
Self::load_selected(context, target, None)
}
pub fn load_selected(
context: &Context,
target: &ScaleTarget,
profile: Option<&str>,
) -> Result<Self, CliError> {
let store = context.store()?;
let policy = cli::policy::find_policy_selected(&store, target, profile)?;
let host = store
.host(policy.host_id)
.map_err(store_failure)?
.ok_or_else(|| {
CliError::new(
Failure::LocalState,
"the policy refers to a missing local host",
)
})?;
let attempts = store
.attempts_for_policy(policy.id)
.map_err(store_failure)?;
let active_runners = active_count_for(policy.id, attempts.iter());
let host_label = policy
.routing_labels()
.map(|labels| labels.host_label().to_string());
let extra_labels = policy.routing_labels().map_or_else(Vec::new, |labels| {
labels.additional().map(ToString::to_string).collect()
});
let copyable_runs_on = policy.routing_labels().map(|labels| {
labels
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
});
let workspace = cli::workspace::repository_workspace(
&store,
&cli::workspace::host_root(context.paths(), Some(&host)),
&policy,
)?;
Ok(Self {
policy_id: policy.id,
target: policy.target.clone(),
profile_name: policy.profile_name().to_string(),
host_identity: host.display_name,
requested_host_label: policy.requested_host_label.clone(),
host_os: host.os,
host_arch: host.architecture,
mode: match policy.mode() {
PolicyMode::Autoscale(_) => SettingsPolicyMode::Autoscale,
PolicyMode::MonitorOnly => SettingsPolicyMode::MonitorOnly,
},
enabled: policy.enabled(),
current_max_capacity: policy.max_capacity().map(std::num::NonZeroU16::get),
host_label,
extra_labels,
copyable_runs_on,
cache_policy: policy.cache_policy,
active_runners,
execution_policy: policy.execution_policy().clone(),
provider_diagnostics: cli::host::isolation_capabilities(),
workspace,
})
}
#[must_use]
pub fn configured_root_text(&self) -> String {
self.workspace
.policy
.root()
.map_or_else(String::new, |root| root.as_str().to_owned())
}
#[must_use]
pub fn is_organization(&self) -> bool {
self.target.scope() == TargetScope::Organization
}
pub fn save_workspace(
&self,
context: &Context,
kind: WorkspaceKind,
raw_path: Option<&str>,
out: &mut dyn Write,
) -> Result<(), CliError> {
cli::profile::dispatch(
context,
&RepoProfileCommand::SetWorkspace(RepoProfileWorkspaceArgs {
selection: RepoProfileSelectArgs {
repository: self.target.to_string(),
profile: Some(self.profile_name.clone()),
},
mode: match kind {
WorkspaceKind::Ephemeral => WorkspaceMode::Ephemeral,
WorkspaceKind::Persistent => WorkspaceMode::Persistent,
},
path: raw_path.map(ToOwned::to_owned),
}),
out,
)
}
#[must_use]
pub fn editable_labels_text(&self) -> String {
self.extra_labels.join(", ")
}
pub fn save_labels(
&self,
context: &Context,
typed: &str,
out: &mut dyn Write,
) -> Result<(), CliError> {
let desired: Vec<String> = typed
.split(',')
.map(str::trim)
.filter(|part| !part.is_empty())
.map(ToOwned::to_owned)
.collect();
cli::policy::replace_optional_labels_selected(
context,
&self.target,
Some(&self.profile_name),
&desired,
out,
)
}
#[must_use]
pub const fn label_action_count(&self) -> u8 {
match self.mode {
SettingsPolicyMode::Autoscale => 2,
SettingsPolicyMode::MonitorOnly => 0,
}
}
#[must_use]
pub fn workspace_action_count(&self, draft: WorkspaceKind) -> u8 {
if self.is_organization() {
0
} else if draft.is_persistent() {
3
} else {
2
}
}
#[must_use]
pub fn preview(&self, draft: &PolicyDraft) -> PolicyPreview {
let enabling = !self.enabled && draft.enabled == Some(true);
let disabling = self.enabled && draft.enabled == Some(false);
PolicyPreview {
target: self.target.to_string(),
from_capacity: self.current_max_capacity,
to_capacity: draft.max_capacity.or(self.current_max_capacity),
from_enabled: self.enabled,
to_enabled: draft.enabled.unwrap_or(self.enabled),
cache_policy: draft.cache_policy.unwrap_or(self.cache_policy),
promotion: self.mode == SettingsPolicyMode::MonitorOnly && draft.max_capacity.is_some(),
drain_confirmation_required: disabling && self.active_runners > 0,
active_runners: self.active_runners,
state_after_disable: disabling.then_some(if self.active_runners == 0 {
"disabled"
} else {
"draining"
}),
trust_warning: enabling.then_some(FORK_TRUST_WARNING),
}
}
pub fn apply(
&self,
context: &Context,
draft: &PolicyDraft,
drain_confirmation: Option<cli::policy::ScaleObservation>,
out: &mut dyn Write,
) -> Result<(), CliError> {
cli::policy::apply_policy_mutation_selected(
context,
&self.target,
Some(&self.profile_name),
cli::policy::PolicyMutation {
max_capacity: draft.max_capacity,
enabled: draft.enabled,
cache_policy: draft.cache_policy,
},
drain_confirmation,
out,
)
}
#[must_use]
pub const fn exposes_scale_toggle(&self) -> bool {
matches!(self.mode, SettingsPolicyMode::Autoscale)
}
pub fn preview_selector(&self, raw_name: &str) -> Result<String, CliError> {
let name = ProfileName::new(raw_name).map_err(invalid)?;
if name.is_default() {
return Err(CliError::new(
Failure::InvalidArgument,
"profile `default` is reserved for repo add",
));
}
Ok(RoutingLabels::derive_for_profile(
&self.requested_host_label,
self.host_os,
self.host_arch,
&name,
)
.host_label()
.to_string())
}
#[must_use]
pub const fn focused_action_count(&self) -> u8 {
match self.mode {
SettingsPolicyMode::Autoscale => 4,
SettingsPolicyMode::MonitorOnly => 2,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PolicyDraft {
pub enabled: Option<bool>,
pub max_capacity: Option<u16>,
pub cache_policy: Option<CachePolicy>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyPreview {
pub target: String,
pub from_capacity: Option<u16>,
pub to_capacity: Option<u16>,
pub from_enabled: bool,
pub to_enabled: bool,
pub cache_policy: CachePolicy,
pub promotion: bool,
pub drain_confirmation_required: bool,
pub active_runners: u16,
pub state_after_disable: Option<&'static str>,
pub trust_warning: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Control {
HostCapacity,
HostStartMode,
HostInterval,
HostSave,
HostRunnerRoot,
HostRootReset,
HostRootSave,
PolicyScale,
PolicyCapacity,
PolicyCache,
PolicySave,
PolicyLabels,
PolicyLabelsSave,
PolicyExecutionMode,
PolicyExecutionBackend,
PolicyExecutionImage,
PolicyExecutionCpu,
PolicyExecutionMemory,
PolicyExecutionDisk,
PolicyExecutionSave,
PolicyDrain,
PolicyRemove,
PolicyAddProfile,
ProfileName,
ProfileCapacity,
ProfileExecutionMode,
ProfileExecutionBackend,
ProfileExecutionImage,
ProfileExecutionCpu,
ProfileExecutionMemory,
ProfileExecutionDisk,
ProfileCreate,
ProfileCreateCancel,
WorkspaceMode,
WorkspacePath,
WorkspaceSave,
}
impl Control {
const fn is_adjustable(self) -> bool {
matches!(
self,
Control::HostCapacity
| Control::HostStartMode
| Control::HostInterval
| Control::PolicyScale
| Control::PolicyCapacity
| Control::PolicyCache
| Control::PolicyExecutionMode
| Control::PolicyExecutionBackend
| Control::PolicyExecutionCpu
| Control::PolicyExecutionMemory
| Control::PolicyExecutionDisk
| Control::ProfileCapacity
| Control::ProfileExecutionMode
| Control::ProfileExecutionBackend
| Control::ProfileExecutionCpu
| Control::ProfileExecutionMemory
| Control::ProfileExecutionDisk
| Control::WorkspaceMode
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct FormLine {
text: String,
control: Option<usize>,
copy: Option<String>,
essential: bool,
}
impl FormLine {
fn text(text: impl Into<String>) -> Self {
Self {
text: text.into(),
control: None,
copy: None,
essential: false,
}
}
fn keep(text: impl Into<String>) -> Self {
Self {
essential: true,
..Self::text(text)
}
}
const fn at(mut self, control: usize) -> Self {
self.control = Some(control);
self
}
fn copyable(mut self, text: Option<String>) -> Self {
self.copy = text;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SettingsUi {
pub view: SettingsView,
pub focus: usize,
pub host_capacity: u16,
pub host_mode: StartMode,
pub host_interval_secs: u16,
pub policy_draft: PolicyDraft,
pub awaiting_drain_confirmation: bool,
pub drain_observation: Option<cli::policy::ScaleObservation>,
pub message: Option<String>,
pub host_root: PathField,
pub host_root_notice: Option<String>,
pub policy_labels: PathField,
pub policy_labels_notice: Option<String>,
pub execution_mode: ExecutionMode,
pub execution_backend: BackendMode,
pub execution_image: PathField,
pub execution_cpu: u32,
pub execution_memory: u32,
pub execution_disk: u32,
pub execution_notice: Option<String>,
pub awaiting_remove_confirmation: bool,
pub profile_create_open: bool,
pub create_name: PathField,
pub create_capacity: u16,
pub create_execution_mode: ExecutionMode,
pub create_execution_backend: BackendMode,
pub create_execution_image: PathField,
pub create_execution_cpu: u32,
pub create_execution_memory: u32,
pub create_execution_disk: u32,
pub create_notice: Option<String>,
pub workspace_mode: WorkspaceKind,
pub workspace_path: PathField,
pub workspace_notice: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum SettingsView {
#[default]
Empty,
Notice(String),
Host(HostSettings),
Policy(Box<PolicySettings>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SettingsCommand {
LoadHost,
#[cfg(test)]
LoadPolicy(String),
LoadProfile {
target: String,
profile: String,
},
ApplyHost,
ApplyPolicy,
Copy(String),
CheckHostRoot,
ResetHostRoot,
SaveHostRoot,
CheckWorkspaceRoot,
SaveWorkspace,
SaveLabels,
SaveExecution,
DrainProfile,
RemoveProfile,
CreateProfile,
}
impl SettingsUi {
pub fn show_notice(&mut self, notice: impl Into<String>) {
self.view = SettingsView::Notice(notice.into());
self.message = None;
self.focus = 0;
}
#[must_use]
pub const fn is_editing(&self) -> bool {
self.host_root.is_editing()
|| self.workspace_path.is_editing()
|| self.policy_labels.is_editing()
|| self.execution_image.is_editing()
|| self.create_name.is_editing()
|| self.create_execution_image.is_editing()
}
pub fn paste(&mut self, text: &str) {
if self.host_root.is_editing() {
self.host_root.paste(text);
} else if self.workspace_path.is_editing() {
self.workspace_path.paste(text);
} else if self.policy_labels.is_editing() {
self.policy_labels.paste(text);
} else if self.execution_image.is_editing() {
self.execution_image.paste(text);
} else if self.create_name.is_editing() {
self.create_name.paste(text);
} else if self.create_execution_image.is_editing() {
self.create_execution_image.paste(text);
}
}
#[must_use]
pub fn copy_text(&self) -> Option<String> {
match self.focused()? {
Control::HostRunnerRoot => match &self.view {
SettingsView::Host(form) => Some(if self.host_root.is_blank() {
form.runner_root.rendered()
} else {
self.host_root.text()
}),
_ => None,
},
Control::WorkspacePath => match &self.view {
SettingsView::Policy(form) if self.workspace_path.is_blank() => {
form.workspace.effective_root().map(ToOwned::to_owned)
}
SettingsView::Policy(_) => Some(self.workspace_path.text()),
_ => None,
},
Control::PolicyLabels => match &self.view {
SettingsView::Policy(form) => form.copyable_runs_on.clone(),
_ => None,
},
Control::PolicyExecutionImage => Some(self.execution_image.text()),
Control::ProfileName => Some(self.create_name.text()),
Control::ProfileExecutionImage => Some(self.create_execution_image.text()),
_ => None,
}
}
pub fn cancel_editing(&mut self) {
if self.host_root.is_editing() {
self.host_root.cancel();
self.host_root_notice = None;
}
if self.workspace_path.is_editing() {
self.workspace_path.cancel();
self.workspace_notice = None;
}
if self.policy_labels.is_editing() {
self.policy_labels.cancel();
self.policy_labels_notice = None;
}
if self.execution_image.is_editing() {
self.execution_image.cancel();
self.execution_notice = None;
}
if self.create_name.is_editing() {
self.create_name.cancel();
}
if self.create_execution_image.is_editing() {
self.create_execution_image.cancel();
}
}
fn accept_editing(&mut self) {
if self.host_root.is_editing() {
self.host_root.accept();
}
if self.workspace_path.is_editing() {
self.workspace_path.accept();
}
if self.policy_labels.is_editing() {
self.policy_labels.accept();
}
if self.execution_image.is_editing() {
self.execution_image.accept();
}
if self.create_name.is_editing() {
self.create_name.accept();
}
if self.create_execution_image.is_editing() {
self.create_execution_image.accept();
}
}
pub fn execute(&mut self, context: &Context, command: SettingsCommand) -> Option<String> {
let is_load = matches!(
&command,
SettingsCommand::LoadHost | SettingsCommand::LoadProfile { .. }
);
#[cfg(test)]
let is_load = is_load || matches!(&command, SettingsCommand::LoadPolicy(_));
if is_load {
self.view = SettingsView::Empty;
self.message = None;
}
let result = match command {
SettingsCommand::LoadHost => self.load_host(context).map(|()| {
self.focus = 0;
self.message = None;
}),
#[cfg(test)]
SettingsCommand::LoadPolicy(raw) => ScaleTarget::repository(&raw)
.or_else(|_| ScaleTarget::organization(&raw))
.map_err(invalid)
.and_then(|target| self.load_policy(context, &target, None))
.map(|()| {
self.focus = 0;
self.message = None;
}),
SettingsCommand::LoadProfile {
target: raw,
profile,
} => ScaleTarget::repository(&raw)
.or_else(|_| ScaleTarget::organization(&raw))
.map_err(invalid)
.and_then(|target| self.load_policy(context, &target, Some(&profile)))
.map(|()| {
self.focus = 0;
self.message = None;
}),
SettingsCommand::ApplyHost => self.apply_host(context),
SettingsCommand::ApplyPolicy => self.apply_policy(context),
SettingsCommand::Copy(text) => return Some(text),
SettingsCommand::CheckHostRoot => self.check_host_root(context),
SettingsCommand::ResetHostRoot => self.save_host_root(context, None),
SettingsCommand::SaveHostRoot => {
let draft = self.host_root.text();
self.save_host_root(context, Some(draft))
}
SettingsCommand::CheckWorkspaceRoot => self.check_workspace_root(context),
SettingsCommand::SaveWorkspace => self.save_workspace(context),
SettingsCommand::SaveLabels => self.save_labels(context),
SettingsCommand::SaveExecution => self.save_execution(context),
SettingsCommand::DrainProfile => {
self.policy_draft.enabled = Some(false);
self.apply_policy(context)
}
SettingsCommand::RemoveProfile => self.remove_profile(context),
SettingsCommand::CreateProfile => self.create_profile(context),
};
if let Err(error) = result {
self.message = Some(format!("error: {error}"));
}
None
}
fn load_host(&mut self, context: &Context) -> Result<(), CliError> {
let form = HostSettings::load(context)?;
self.host_capacity = form.current_capacity;
self.host_mode = form.current_start_mode;
self.host_interval_secs = form.current_refresh_interval.as_secs();
self.host_root.reset_to(&form.configured_root_text());
self.host_root_notice = None;
self.view = SettingsView::Host(form);
Ok(())
}
fn load_policy(
&mut self,
context: &Context,
target: &ScaleTarget,
profile: Option<&str>,
) -> Result<(), CliError> {
let form = PolicySettings::load_selected(context, target, profile)?;
self.policy_draft = PolicyDraft::default();
self.workspace_mode = form.workspace.kind();
self.workspace_path.reset_to(&form.configured_root_text());
self.workspace_notice = None;
self.policy_labels.reset_to(&form.editable_labels_text());
self.policy_labels_notice = None;
match &form.execution_policy {
ExecutionPolicy::Native => {
self.execution_mode = ExecutionMode::Native;
self.execution_backend = BackendMode::Auto;
self.execution_image.reset_to("");
self.execution_cpu = 1000;
self.execution_memory = 1024;
self.execution_disk = 4096;
}
ExecutionPolicy::Isolated {
backend,
image,
resources,
} => {
self.execution_mode = ExecutionMode::Isolated;
self.execution_backend = match backend {
Backend::Auto => BackendMode::Auto,
Backend::Oci => BackendMode::Oci,
Backend::WindowsHyperVContainer => BackendMode::WindowsHyperVContainer,
Backend::VirtualMachine => BackendMode::VirtualMachine,
};
self.execution_image.reset_to(image.as_str());
self.execution_cpu = resources.cpu_millis;
self.execution_memory = resources.memory_mib;
self.execution_disk = resources.disk_mib;
}
}
self.execution_notice = None;
self.profile_create_open = false;
self.create_name.reset_to("");
self.create_capacity = 1;
self.create_execution_mode = ExecutionMode::Native;
self.create_execution_backend = BackendMode::Auto;
self.create_execution_image.reset_to("");
self.create_execution_cpu = 1000;
self.create_execution_memory = 1024;
self.create_execution_disk = 4096;
self.create_notice = None;
self.awaiting_remove_confirmation = false;
self.awaiting_drain_confirmation = false;
self.drain_observation = None;
self.view = SettingsView::Policy(Box::new(form));
Ok(())
}
fn apply_host(&mut self, context: &Context) -> Result<(), CliError> {
let SettingsView::Host(form) = &self.view else {
return Ok(());
};
if self.host_capacity == 0 {
return Err(CliError::new(
Failure::InvalidArgument,
"host capacity must be at least 1; nothing was changed",
));
}
form.accepted_interval_preview(self.host_interval_secs)?;
let mut output = Vec::new();
if self.host_mode != form.current_start_mode {
HostSettings::set_start_mode(context, self.host_mode)?;
}
if self.host_capacity != form.current_capacity {
HostSettings::set_capacity(context, self.host_capacity, &mut output)?;
}
if self.host_interval_secs != form.current_refresh_interval.as_secs() {
form.set_refresh_interval(context, self.host_interval_secs)?;
}
self.message = Some(if output.is_empty() {
"Host settings saved.".to_owned()
} else {
String::from_utf8_lossy(&output).trim().to_owned()
});
self.load_host(context)
}
fn check_host_root(&mut self, context: &Context) -> Result<(), CliError> {
let SettingsView::Host(form) = &self.view else {
return Ok(());
};
let affected = form.affected;
let draft = self.host_root.text();
self.host_root_notice = Some(if draft.trim().is_empty() {
"no override typed: saving is refused, and `Reset to platform default` is what \
clears an existing one."
.to_owned()
} else {
let store = context.store()?;
notice_for(
cli::workspace::check_root(context, &store, &RootOwner::Host, &draft),
affected,
"the host runner root",
"new ephemeral attempts would be created under",
)
});
Ok(())
}
fn save_host_root(&mut self, context: &Context, draft: Option<String>) -> Result<(), CliError> {
if let Some(draft) = &draft
&& draft.trim().is_empty()
{
return Err(CliError::with_remedy(
Failure::InvalidArgument,
"type an absolute path before saving a runner root; nothing was changed",
"focus `Reset to platform default` to clear the override instead",
));
}
let mut output = Vec::new();
let result = HostSettings::save_runner_root(context, draft.as_deref(), &mut output);
let draft = (self.host_root.clone(), self.host_root_notice.clone());
let reloaded = self.load_host(context);
if result.is_err() {
(self.host_root, self.host_root_notice) = draft;
}
result?;
reloaded?;
self.host_root_notice = None;
self.message = Some(String::from_utf8_lossy(&output).trim().to_owned());
Ok(())
}
fn check_workspace_root(&mut self, context: &Context) -> Result<(), CliError> {
let SettingsView::Policy(form) = &self.view else {
return Ok(());
};
let affected = form.workspace.attempts;
let owner = RootOwner::Repository(form.target.slug());
let subject = format!("the workspace setting for {}", form.target);
let draft = self.workspace_path.text();
self.workspace_notice = Some(if draft.trim().is_empty() {
"a persistent workspace needs the directory its slots live in".to_owned()
} else {
let store = context.store()?;
notice_for(
cli::workspace::check_root(context, &store, &owner, &draft),
affected,
&subject,
"slots s1, s2, ... would be created under",
)
});
Ok(())
}
fn save_workspace(&mut self, context: &Context) -> Result<(), CliError> {
let SettingsView::Policy(form) = &self.view else {
return Ok(());
};
let form = form.clone();
let path = self
.workspace_mode
.is_persistent()
.then(|| self.workspace_path.text())
.filter(|draft| !draft.trim().is_empty());
let mut output = Vec::new();
let result =
form.save_workspace(context, self.workspace_mode, path.as_deref(), &mut output);
let draft = (
self.workspace_mode,
self.workspace_path.clone(),
self.workspace_notice.clone(),
);
let reloaded = self.load_policy(context, &form.target, Some(&form.profile_name));
if result.is_err() {
(
self.workspace_mode,
self.workspace_path,
self.workspace_notice,
) = draft;
}
result?;
reloaded?;
self.workspace_notice = None;
self.message = Some(String::from_utf8_lossy(&output).trim().to_owned());
Ok(())
}
fn save_labels(&mut self, context: &Context) -> Result<(), CliError> {
let SettingsView::Policy(form) = &self.view else {
return Ok(());
};
let form = form.clone();
let typed = self.policy_labels.text();
let mut output = Vec::new();
let result = form.save_labels(context, &typed, &mut output);
let draft = (
self.policy_labels.clone(),
self.policy_labels_notice.clone(),
);
let reloaded = self.load_policy(context, &form.target, Some(&form.profile_name));
if result.is_err() {
(self.policy_labels, self.policy_labels_notice) = draft;
}
result?;
reloaded?;
let rendered = String::from_utf8_lossy(&output);
let mut lines = rendered.trim().lines();
self.message = lines.next().map(ToOwned::to_owned);
let rest = lines.map(str::trim).collect::<Vec<_>>().join(" ");
self.policy_labels_notice = (!rest.is_empty()).then_some(rest);
Ok(())
}
fn create_profile(&mut self, context: &Context) -> Result<(), CliError> {
let SettingsView::Policy(form) = &self.view else {
return Ok(());
};
let form = form.clone();
let name = self.create_name.text();
form.preview_selector(&name)?;
let isolation = isolation_args(
self.create_execution_mode,
self.create_execution_backend,
&self.create_execution_image,
self.create_execution_cpu,
self.create_execution_memory,
self.create_execution_disk,
);
let mut output = Vec::new();
cli::profile::dispatch(
context,
&RepoProfileCommand::Add(RepoProfileAddArgs {
repository: form.target.to_string(),
name: name.clone(),
host_label: None,
max_capacity: Some(self.create_capacity),
labels: Vec::new(),
execution: self.create_execution_mode,
isolation,
enable: false,
}),
&mut output,
)?;
self.load_policy(context, &form.target, Some(&name))?;
self.message = Some(String::from_utf8_lossy(&output).trim().to_owned());
Ok(())
}
fn save_execution(&mut self, context: &Context) -> Result<(), CliError> {
let SettingsView::Policy(form) = &self.view else {
return Ok(());
};
let form = form.clone();
let isolation = isolation_args(
self.execution_mode,
self.execution_backend,
&self.execution_image,
self.execution_cpu,
self.execution_memory,
self.execution_disk,
);
let mut output = Vec::new();
let result = cli::profile::dispatch(
context,
&RepoProfileCommand::SetExecution(RepoProfileExecutionArgs {
selection: RepoProfileSelectArgs {
repository: form.target.to_string(),
profile: Some(form.profile_name.clone()),
},
mode: self.execution_mode,
isolation,
}),
&mut output,
);
let draft = (
self.execution_mode,
self.execution_backend,
self.execution_image.clone(),
self.execution_cpu,
self.execution_memory,
self.execution_disk,
);
let reloaded = self.load_policy(context, &form.target, Some(&form.profile_name));
if result.is_err() {
(
self.execution_mode,
self.execution_backend,
self.execution_image,
self.execution_cpu,
self.execution_memory,
self.execution_disk,
) = draft;
}
result?;
reloaded?;
self.message = Some(String::from_utf8_lossy(&output).trim().to_owned());
Ok(())
}
fn remove_profile(&mut self, context: &Context) -> Result<(), CliError> {
let SettingsView::Policy(form) = &self.view else {
return Ok(());
};
let form = form.clone();
if form.enabled || form.active_runners > 0 {
return Err(CliError::with_remedy(
Failure::Conflict,
format!(
"profile {} must be drained before removal; nothing was changed",
form.profile_name
),
"focus `Drain selected profile`, wait for active runners to finish, then remove it",
));
}
if !self.awaiting_remove_confirmation {
self.awaiting_remove_confirmation = true;
self.message = Some(format!(
"Confirm removal of profile {} only. Press Enter again.",
form.profile_name
));
return Ok(());
}
let mut output = Vec::new();
cli::policy::remove_selected(
context,
form.target.clone(),
Some(&form.profile_name),
false,
&mut output,
)?;
self.show_notice(format!(
"{}
Select another profile in Repositories or create one with the CLI.",
String::from_utf8_lossy(&output).trim()
));
Ok(())
}
fn apply_policy(&mut self, context: &Context) -> Result<(), CliError> {
let SettingsView::Policy(form) = &self.view else {
return Ok(());
};
let form = form.clone();
if self.policy_draft.enabled == Some(false) && self.drain_observation.is_none() {
let observed = cli::policy::observe_scale_selected(
context,
&form.target,
Some(&form.profile_name),
)?;
if observed.active > 0 {
self.awaiting_drain_confirmation = true;
self.drain_observation = Some(observed);
self.message = Some(format!(
"Confirm drain: {} active runner(s) will be left to finish. Press Enter again.",
observed.active
));
return Ok(());
}
self.drain_observation = Some(observed);
}
let target = form.target.clone();
let mut output = Vec::new();
let result = form.apply(
context,
&self.policy_draft,
self.drain_observation,
&mut output,
);
if let Err(error) = result {
self.awaiting_drain_confirmation = false;
self.drain_observation = None;
self.view = SettingsView::Policy(Box::new(PolicySettings::load_selected(
context,
&target,
Some(&form.profile_name),
)?));
return Err(error);
}
self.message = Some(String::from_utf8_lossy(&output).trim().to_owned());
self.load_policy(context, &target, Some(&form.profile_name))
}
#[must_use]
pub fn key(&mut self, code: KeyCode) -> Option<SettingsCommand> {
if self.is_editing() {
return self.edit_key(code);
}
let controls = self.control_count();
match code {
KeyCode::Up | KeyCode::BackTab => {
self.focus = self.focus.saturating_sub(1);
}
KeyCode::Down | KeyCode::Tab => {
self.focus = (self.focus + 1).min(controls.saturating_sub(1));
}
KeyCode::Left | KeyCode::Char('-') => self.adjust(false),
KeyCode::Right | KeyCode::Char('+') | KeyCode::Char(' ') => self.adjust(true),
KeyCode::Enter => return self.activate(),
_ => return None,
}
None
}
fn edit_key(&mut self, code: KeyCode) -> Option<SettingsCommand> {
let owner = if self.host_root.is_editing() {
Control::HostRunnerRoot
} else if self.workspace_path.is_editing() {
Control::WorkspacePath
} else if self.execution_image.is_editing() {
Control::PolicyExecutionImage
} else if self.create_name.is_editing() {
Control::ProfileName
} else if self.create_execution_image.is_editing() {
Control::ProfileExecutionImage
} else {
Control::PolicyLabels
};
let field = match owner {
Control::HostRunnerRoot => &mut self.host_root,
Control::WorkspacePath => &mut self.workspace_path,
Control::PolicyExecutionImage => &mut self.execution_image,
Control::ProfileName => &mut self.create_name,
Control::ProfileExecutionImage => &mut self.create_execution_image,
_ => &mut self.policy_labels,
};
match code {
KeyCode::Char(character) => field.insert(character),
KeyCode::Backspace => field.backspace(),
KeyCode::Delete => field.delete(),
KeyCode::Left => field.left(),
KeyCode::Right => field.right(),
KeyCode::Home => field.home(),
KeyCode::End => field.end(),
KeyCode::Esc => {
field.cancel();
match owner {
Control::HostRunnerRoot => self.host_root_notice = None,
Control::WorkspacePath => self.workspace_notice = None,
Control::PolicyExecutionImage => self.execution_notice = None,
Control::ProfileName | Control::ProfileExecutionImage => {
self.create_notice = None
}
_ => self.policy_labels_notice = None,
}
}
KeyCode::Enter | KeyCode::Tab | KeyCode::BackTab => {
field.accept();
return match owner {
Control::HostRunnerRoot => Some(SettingsCommand::CheckHostRoot),
Control::WorkspacePath => Some(SettingsCommand::CheckWorkspaceRoot),
_ => None,
};
}
_ => {}
}
None
}
#[must_use]
pub fn click(
&mut self,
content_row: u16,
width: usize,
compact: bool,
) -> Option<SettingsCommand> {
let row = self
.rows(width, compact)
.get(usize::from(content_row))?
.clone();
self.accept_editing();
if let Some(control) = row.control {
self.focus = control;
return if self.focused().is_some_and(Control::is_adjustable) {
self.adjust(true);
None
} else {
self.activate()
};
}
row.copy.map(SettingsCommand::Copy)
}
#[must_use]
pub fn controls(&self) -> Vec<Control> {
match &self.view {
SettingsView::Host(_) => vec![
Control::HostCapacity,
Control::HostStartMode,
Control::HostInterval,
Control::HostSave,
Control::HostRunnerRoot,
Control::HostRootReset,
Control::HostRootSave,
],
SettingsView::Policy(form) => {
let mut controls = Vec::with_capacity(7);
if form.exposes_scale_toggle() {
controls.push(Control::PolicyScale);
}
controls.push(Control::PolicyCapacity);
controls.push(Control::PolicyCache);
controls.push(Control::PolicySave);
if form.exposes_scale_toggle() {
controls.push(Control::PolicyLabels);
controls.push(Control::PolicyLabelsSave);
}
if !form.is_organization() {
controls.push(Control::PolicyExecutionMode);
if self.execution_mode == ExecutionMode::Isolated {
controls.push(Control::PolicyExecutionBackend);
controls.push(Control::PolicyExecutionImage);
controls.push(Control::PolicyExecutionCpu);
controls.push(Control::PolicyExecutionMemory);
controls.push(Control::PolicyExecutionDisk);
}
controls.push(Control::PolicyExecutionSave);
controls.push(Control::PolicyDrain);
controls.push(Control::PolicyRemove);
}
if !form.is_organization() {
controls.push(Control::WorkspaceMode);
if self.workspace_mode.is_persistent() {
controls.push(Control::WorkspacePath);
}
controls.push(Control::WorkspaceSave);
}
if !form.is_organization() {
controls.push(Control::PolicyAddProfile);
if self.profile_create_open {
controls.push(Control::ProfileName);
controls.push(Control::ProfileCapacity);
controls.push(Control::ProfileExecutionMode);
if self.create_execution_mode == ExecutionMode::Isolated {
controls.push(Control::ProfileExecutionBackend);
controls.push(Control::ProfileExecutionImage);
controls.push(Control::ProfileExecutionCpu);
controls.push(Control::ProfileExecutionMemory);
controls.push(Control::ProfileExecutionDisk);
}
controls.push(Control::ProfileCreate);
controls.push(Control::ProfileCreateCancel);
}
}
controls
}
SettingsView::Empty | SettingsView::Notice(_) => Vec::new(),
}
}
#[must_use]
pub fn focused(&self) -> Option<Control> {
self.controls().get(self.focus).copied()
}
#[cfg(test)]
#[must_use]
pub fn control_rows(&self, width: usize, compact: bool) -> Vec<Option<usize>> {
self.rows(width, compact)
.into_iter()
.map(|row| row.control)
.collect()
}
fn control_count(&self) -> usize {
self.controls().len()
}
fn adjust(&mut self, increase: bool) {
let Some(control) = self.focused() else {
return;
};
let form = match &self.view {
SettingsView::Policy(form) => Some((**form).clone()),
_ => None,
};
match control {
Control::HostCapacity => {
self.host_capacity = if increase {
self.host_capacity.saturating_add(1)
} else {
self.host_capacity.saturating_sub(1)
};
}
Control::HostStartMode => {
self.host_mode = if self.host_mode == StartMode::Boot {
StartMode::Login
} else {
StartMode::Boot
};
}
Control::HostInterval => {
self.host_interval_secs = if increase {
self.host_interval_secs.saturating_add(30)
} else {
self.host_interval_secs
.saturating_sub(30)
.max(RefreshInterval::MIN_SECS)
};
}
Control::PolicyScale => {
if let Some(form) = &form {
self.policy_draft.enabled =
Some(!self.policy_draft.enabled.unwrap_or(form.enabled));
}
}
Control::PolicyCapacity => {
if let Some(form) = &form {
let current = self
.policy_draft
.max_capacity
.or(form.current_max_capacity)
.unwrap_or(1);
self.policy_draft.max_capacity = Some(if increase {
current.saturating_add(1)
} else {
current.saturating_sub(1)
});
}
}
Control::PolicyCache => {
if let Some(form) = &form {
let current = self.policy_draft.cache_policy.unwrap_or(form.cache_policy);
self.policy_draft.cache_policy = Some(match current {
CachePolicy::RetainRunnerPackage => CachePolicy::DiscardRunnerPackage,
CachePolicy::DiscardRunnerPackage => CachePolicy::RetainRunnerPackage,
});
}
}
Control::PolicyExecutionMode => {
self.execution_mode = if self.execution_mode == ExecutionMode::Native {
ExecutionMode::Isolated
} else {
ExecutionMode::Native
};
self.execution_notice = None;
}
Control::PolicyExecutionBackend => {
self.execution_backend = step_backend(self.execution_backend, increase);
}
Control::PolicyExecutionCpu => {
self.execution_cpu = step_u32(self.execution_cpu, increase, 100, 100);
}
Control::PolicyExecutionMemory => {
self.execution_memory = step_u32(self.execution_memory, increase, 256, 256);
}
Control::PolicyExecutionDisk => {
self.execution_disk = step_u32(self.execution_disk, increase, 1024, 1024);
}
Control::ProfileCapacity => {
self.create_capacity = if increase {
self.create_capacity.saturating_add(1)
} else {
self.create_capacity.saturating_sub(1).max(1)
};
}
Control::ProfileExecutionMode => {
self.create_execution_mode = if self.create_execution_mode == ExecutionMode::Native
{
ExecutionMode::Isolated
} else {
ExecutionMode::Native
};
}
Control::ProfileExecutionBackend => {
self.create_execution_backend =
step_backend(self.create_execution_backend, increase);
}
Control::ProfileExecutionCpu => {
self.create_execution_cpu = step_u32(self.create_execution_cpu, increase, 100, 100)
}
Control::ProfileExecutionMemory => {
self.create_execution_memory =
step_u32(self.create_execution_memory, increase, 256, 256)
}
Control::ProfileExecutionDisk => {
self.create_execution_disk =
step_u32(self.create_execution_disk, increase, 1024, 1024)
}
Control::WorkspaceMode => {
self.workspace_mode = match self.workspace_mode {
WorkspaceKind::Ephemeral => WorkspaceKind::Persistent,
WorkspaceKind::Persistent => WorkspaceKind::Ephemeral,
};
self.workspace_notice = None;
}
Control::HostSave
| Control::HostRunnerRoot
| Control::HostRootReset
| Control::HostRootSave
| Control::PolicySave
| Control::PolicyLabels
| Control::PolicyLabelsSave
| Control::PolicyExecutionImage
| Control::PolicyExecutionSave
| Control::PolicyDrain
| Control::PolicyRemove
| Control::PolicyAddProfile
| Control::ProfileName
| Control::ProfileExecutionImage
| Control::ProfileCreate
| Control::ProfileCreateCancel
| Control::WorkspacePath
| Control::WorkspaceSave => {}
}
self.focus = self.focus.min(self.control_count().saturating_sub(1));
}
fn activate(&mut self) -> Option<SettingsCommand> {
match self.focused()? {
Control::HostSave => Some(SettingsCommand::ApplyHost),
Control::HostRunnerRoot => {
self.host_root.begin();
None
}
Control::HostRootReset => Some(SettingsCommand::ResetHostRoot),
Control::HostRootSave => Some(SettingsCommand::SaveHostRoot),
Control::PolicySave => Some(SettingsCommand::ApplyPolicy),
Control::PolicyLabels => {
self.policy_labels.begin();
None
}
Control::PolicyLabelsSave => Some(SettingsCommand::SaveLabels),
Control::PolicyExecutionImage => {
self.execution_image.begin();
None
}
Control::PolicyExecutionSave => Some(SettingsCommand::SaveExecution),
Control::PolicyDrain => Some(SettingsCommand::DrainProfile),
Control::PolicyRemove => Some(SettingsCommand::RemoveProfile),
Control::PolicyAddProfile => {
self.profile_create_open = true;
self.create_notice = None;
self.focus = self
.controls()
.iter()
.position(|control| *control == Control::ProfileName)
.unwrap_or(self.focus);
None
}
Control::ProfileName => {
self.create_name.begin();
None
}
Control::ProfileExecutionImage => {
self.create_execution_image.begin();
None
}
Control::ProfileCreate => Some(SettingsCommand::CreateProfile),
Control::ProfileCreateCancel => {
self.profile_create_open = false;
self.create_notice = None;
self.focus = self.focus.min(self.control_count().saturating_sub(1));
None
}
Control::WorkspacePath => {
self.workspace_path.begin();
None
}
Control::WorkspaceSave => Some(SettingsCommand::SaveWorkspace),
Control::HostCapacity
| Control::HostStartMode
| Control::HostInterval
| Control::PolicyScale
| Control::PolicyCapacity
| Control::PolicyCache
| Control::PolicyExecutionMode
| Control::PolicyExecutionBackend
| Control::PolicyExecutionCpu
| Control::PolicyExecutionMemory
| Control::PolicyExecutionDisk
| Control::ProfileCapacity
| Control::ProfileExecutionMode
| Control::ProfileExecutionBackend
| Control::ProfileExecutionCpu
| Control::ProfileExecutionMemory
| Control::ProfileExecutionDisk
| Control::WorkspaceMode => None,
}
}
fn rows(&self, width: usize, compact: bool) -> Vec<FormLine> {
Self::lay_out(self.form_lines(width), width, compact, self.focus)
}
fn lay_out(
mut logical: Vec<FormLine>,
width: usize,
compact: bool,
focus: usize,
) -> Vec<FormLine> {
if compact {
logical.retain(|line| line.essential || line.control == Some(focus));
}
let mut physical = Vec::with_capacity(logical.len());
for line in logical {
for (index, text) in wrap(&line.text, width).into_iter().enumerate() {
physical.push(FormLine {
text,
control: if index == 0 { line.control } else { None },
copy: if index == 0 { line.copy.clone() } else { None },
essential: line.essential,
});
}
}
physical
}
fn form_lines(&self, width: usize) -> Vec<FormLine> {
match &self.view {
SettingsView::Empty => vec![FormLine::keep("Loading settings...")],
SettingsView::Notice(notice) => notice.lines().map(FormLine::text).collect(),
SettingsView::Host(form) => self.host_lines(form, width),
SettingsView::Policy(form) => self.policy_lines(form, width),
}
}
fn host_lines(&self, form: &HostSettings, width: usize) -> Vec<FormLine> {
let preview = form.preview_interval(self.host_interval_secs).ok();
let mut lines = vec![
FormLine::text("Host settings"),
FormLine::keep(format!("Current capacity: {}", form.current_capacity)),
FormLine::text(format!("Currently in use: {}", form.current_in_use)),
FormLine::keep(format!("Capacity: {} [-/+ or click]", self.host_capacity)).at(0),
FormLine::text(format!("Service start: {} [toggle]", self.host_mode)).at(1),
FormLine::text(format!(
"Refresh interval: {}s [-/+ 30s]",
self.host_interval_secs
))
.at(2),
FormLine::text(format!(
"Projected requests/hour: {}{}",
preview.map_or(form.projected_requests_per_hour, |p| p
.projected_requests_per_hour),
if preview.map_or(form.projection_is_floor, |p| p.projection_is_floor) {
" (a floor: organization targets present)"
} else {
""
}
)),
FormLine::text(format!(
"Maximum repository targets: about {}",
preview.map_or(form.maximum_repository_targets, |p| p
.maximum_repository_targets)
)),
FormLine::text("30-second floor; over-budget changes are refused."),
FormLine::keep("Save host settings [Enter/click]").at(3),
FormLine::text(""),
FormLine::keep(format!(
"Runner root: {} ({})",
form.runner_root.rendered(),
form.runner_root.source()
)),
FormLine::text(format!(
"Affected attempts: {} active, {} awaiting cleanup",
form.affected.active, form.affected.cleanup_blocked
)),
FormLine::keep(self.field_row(
"Override",
&self.host_root,
width,
"(none - platform default)",
))
.at(4),
];
if let Some(notice) = &self.host_root_notice {
lines.push(FormLine::keep(notice.clone()));
}
lines.push(FormLine::text("Reset to platform default [Enter/click]").at(5));
lines.push(FormLine::keep("Save runner root [Enter/click]").at(6));
lines.push(FormLine::text(format!(
"Focused form actions: {}/{} capacity, {}/{} runner root",
form.focused_action_count(),
MAX_FOCUSED_FORM_ACTIONS,
HostSettings::workspace_action_count(),
MAX_FOCUSED_FORM_ACTIONS
)));
lines
}
fn policy_lines(&self, form: &PolicySettings, width: usize) -> Vec<FormLine> {
let preview = form.preview(&self.policy_draft);
let mut lines = vec![
FormLine::keep(format!("Target: {}", form.target)),
FormLine::keep(format!("Profile: {}", form.profile_name)),
FormLine::text(format!("Mode: {:?}", form.mode)),
FormLine::text(format!("Local host: {}", form.host_identity)),
FormLine::text(format!(
"Current max_capacity: {}",
form.current_max_capacity
.map_or_else(|| "monitor-only".into(), |v| v.to_string())
)),
FormLine::text(format!(
"runs-on: {} [click to copy]",
form.copyable_runs_on
.as_deref()
.unwrap_or("not reserved until promotion")
))
.copyable(form.copyable_runs_on.clone()),
FormLine::keep(STATIC_SELECTOR_WARNING)
.copyable(Some(STATIC_SELECTOR_WARNING.to_owned())),
FormLine::keep(FORK_TRUST_WARNING).copyable(Some(FORK_TRUST_WARNING.to_owned())),
];
let mut next = 0;
if form.exposes_scale_toggle() {
lines.push(
FormLine::text(format!("Scaling enabled: {} [toggle]", preview.to_enabled))
.at(next),
);
next += 1;
}
lines.push(
FormLine::keep(format!(
"max_capacity: {} [-/+; setting promotes monitor-only]",
preview
.to_capacity
.map_or_else(|| "unset".into(), |v| v.to_string())
))
.at(next),
);
next += 1;
lines.push(
FormLine::text(format!(
"Cache policy: {:?} [toggle]",
preview.cache_policy
))
.at(next),
);
next += 1;
if let Some(warning) = preview.trust_warning {
lines.push(FormLine::text(warning));
} else if preview.drain_confirmation_required {
lines.push(FormLine::text(format!(
"Disabling means draining {} active runner(s); none will be terminated.",
preview.active_runners
)));
} else {
lines.push(FormLine::text(
"Preview: no runner is terminated immediately.",
));
}
lines.push(FormLine::keep("Confirm policy [Enter/click]").at(next));
next += 1;
lines.push(FormLine::text(""));
lines.extend(self.label_lines(form, width, &mut next));
lines.push(FormLine::text(""));
if !form.is_organization() {
lines.extend(self.execution_lines(form, width, &mut next));
lines.push(FormLine::text(""));
}
lines.extend(self.workspace_lines(form, width, &mut next));
if !form.is_organization() {
lines.push(FormLine::text(""));
lines.extend(self.profile_create_lines(form, width, &mut next));
}
lines.push(FormLine::text(format!(
"Focused form actions: {}/{} scaling, {}/{} labels, {}/{} workspace",
form.focused_action_count(),
MAX_FOCUSED_FORM_ACTIONS,
form.label_action_count(),
MAX_FOCUSED_FORM_ACTIONS,
form.workspace_action_count(self.workspace_mode),
MAX_FOCUSED_FORM_ACTIONS
)));
lines
}
fn label_lines(
&self,
form: &PolicySettings,
width: usize,
control: &mut usize,
) -> Vec<FormLine> {
let Some(host_label) = &form.host_label else {
return vec![FormLine::text(
"Routing labels: none until this policy is promoted; set a max_capacity above.",
)];
};
let mut lines = vec![
FormLine::keep(format!(
"Host label: {host_label} (fixed: it is this machine's routing identity)"
)),
FormLine::keep(self.field_row(
"Extra labels",
&self.policy_labels,
width,
"(none - this policy answers its host label only)",
))
.at(*control),
];
*control += 1;
lines.push(FormLine::text(
"Comma-separated. Saving makes the stored set equal this line.",
));
if let Some(notice) = &self.policy_labels_notice {
lines.push(FormLine::keep(notice.clone()));
}
lines.push(FormLine::keep("Save labels [Enter/click]").at(*control));
*control += 1;
lines
}
fn execution_lines(
&self,
form: &PolicySettings,
width: usize,
control: &mut usize,
) -> Vec<FormLine> {
let stored = match &form.execution_policy {
ExecutionPolicy::Native => "native".to_owned(),
ExecutionPolicy::Isolated {
backend,
image,
resources,
} => format!(
"isolated backend={} image={} cpu={}m memory={}MiB disk={}MiB",
backend_name(*backend),
image.as_str(),
resources.cpu_millis,
resources.memory_mib,
resources.disk_mib,
),
};
let mut lines = vec![
FormLine::keep(format!("Execution: {stored}")),
FormLine::keep(format!(
"Execution mode: {} [toggle]",
execution_mode_name(self.execution_mode)
))
.at(*control),
];
*control += 1;
if self.execution_mode == ExecutionMode::Isolated {
lines.push(
FormLine::keep(format!(
"Backend: {} [cycle]",
backend_mode_name(self.execution_backend)
))
.at(*control),
);
*control += 1;
lines.push(
FormLine::keep(self.field_row(
"Pinned image",
&self.execution_image,
width,
"(required: sha256, OCI digest, or vm-version)",
))
.at(*control),
);
*control += 1;
for (label, value, suffix) in [
("CPU", self.execution_cpu, "m"),
("Memory", self.execution_memory, "MiB"),
("Disk", self.execution_disk, "MiB"),
] {
lines.push(FormLine::keep(format!("{label}: {value}{suffix} [-/+]")).at(*control));
*control += 1;
}
let capability = backend_capability(self.execution_backend, &form.provider_diagnostics);
lines.push(FormLine::keep(format!(
"Selected provider: {}; remedy: {}",
capability.state.display_name(),
capability.remedy.unwrap_or("none required")
)));
if let Some(notice) = capability.support_notice {
lines.push(FormLine::keep(format!("Support: {notice}")));
}
if !capability.unsupported_workflow_capabilities.is_empty() {
lines.push(FormLine::keep(format!(
"Unsupported workflows: {}",
capability.unsupported_workflow_capabilities.join(", ")
)));
}
}
if let Some(notice) = &self.execution_notice {
lines.push(FormLine::keep(notice.clone()));
}
lines.push(FormLine::keep("Save execution [Enter/click]").at(*control));
*control += 1;
lines.extend(form.provider_diagnostics.iter().map(|provider| {
FormLine::text(format!(
"{} provider: {}",
provider.backend.display_name(),
provider.state.display_name()
))
}));
lines.push(FormLine::text(
"Provider readiness and the pinned image are checked again before JIT registration.",
));
lines.push(FormLine::keep("Drain selected profile [Enter/click]").at(*control));
*control += 1;
lines
.push(FormLine::keep("Remove selected profile [Enter twice/click twice]").at(*control));
*control += 1;
lines
}
fn workspace_lines(
&self,
form: &PolicySettings,
width: usize,
control: &mut usize,
) -> Vec<FormLine> {
let mut lines = vec![
FormLine::keep(format!(
"Workspace: {} ({})",
form.workspace.kind(),
form.workspace.root_source_badge()
)),
FormLine::keep(format!(
"Workspace root: {}",
form.workspace.effective_root().unwrap_or("unavailable")
)),
FormLine::text(lease_summary(&form.workspace.leases)),
FormLine::text(format!(
"Affected attempts: {} active, {} awaiting cleanup",
form.workspace.attempts.active, form.workspace.attempts.cleanup_blocked
)),
];
if form.is_organization() {
lines.push(FormLine::keep(ORGANIZATION_WORKSPACE_MODE));
lines.push(FormLine::text(ORGANIZATION_WORKSPACE_EXPLANATION));
lines.push(FormLine::text(format!(
"Configure it per repository instead: {}",
RootOwner::Repository("OWNER/REPO".to_owned()).remediation()
)));
return lines;
}
lines.push(
FormLine::keep(format!("Workspace mode: {} [toggle]", self.workspace_mode))
.at(*control),
);
*control += 1;
if self.workspace_mode.is_persistent() {
lines.push(
FormLine::keep(self.field_row(
"Persistent root",
&self.workspace_path,
width,
"(none - platform default)",
))
.at(*control),
);
*control += 1;
}
if let Some(notice) = &self.workspace_notice {
lines.push(FormLine::keep(notice.clone()));
}
if self.workspace_mode.is_persistent() {
for (index, line) in cli::workspace::PERSISTENT_TRUST_WARNING.iter().enumerate() {
lines.push(if index == 0 {
FormLine::keep(*line)
} else {
FormLine::text(*line)
});
}
}
if let Some(retained) = self.retained_notice(form) {
lines.push(FormLine::keep(retained));
}
lines.push(FormLine::keep("Save workspace [Enter/click]").at(*control));
*control += 1;
lines
}
fn profile_create_lines(
&self,
form: &PolicySettings,
width: usize,
control: &mut usize,
) -> Vec<FormLine> {
let mut lines = vec![FormLine::keep("Add sibling profile [Enter/click]").at(*control)];
*control += 1;
if !self.profile_create_open {
return lines;
}
lines.push(
FormLine::keep(self.field_row(
"New profile name",
&self.create_name,
width,
"(required)",
))
.at(*control),
);
*control += 1;
let preview = form
.preview_selector(&self.create_name.text())
.unwrap_or_else(|_| "type a valid name to preview".to_owned());
lines.push(FormLine::keep(format!("Selector preview: {preview}")));
lines.push(
FormLine::keep(format!("Capacity: {} [-/+]", self.create_capacity)).at(*control),
);
*control += 1;
lines.push(
FormLine::keep(format!(
"Execution mode: {} [toggle]",
execution_mode_name(self.create_execution_mode)
))
.at(*control),
);
*control += 1;
if self.create_execution_mode == ExecutionMode::Isolated {
lines.push(
FormLine::keep(format!(
"Backend: {} [cycle]",
backend_mode_name(self.create_execution_backend)
))
.at(*control),
);
*control += 1;
lines.push(
FormLine::keep(self.field_row(
"Pinned image",
&self.create_execution_image,
width,
"(required)",
))
.at(*control),
);
*control += 1;
for (label, value, suffix) in [
("CPU", self.create_execution_cpu, "m"),
("Memory", self.create_execution_memory, "MiB"),
("Disk", self.create_execution_disk, "MiB"),
] {
lines.push(FormLine::keep(format!("{label}: {value}{suffix} [-/+]")).at(*control));
*control += 1;
}
let capability =
backend_capability(self.create_execution_backend, &form.provider_diagnostics);
lines.push(FormLine::keep(format!(
"Selected provider: {}; remedy: {}",
capability.state.display_name(),
capability.remedy.unwrap_or("none required")
)));
if let Some(notice) = capability.support_notice {
lines.push(FormLine::keep(format!("Support: {notice}")));
}
if !capability.unsupported_workflow_capabilities.is_empty() {
lines.push(FormLine::keep(format!(
"Unsupported workflows: {}",
capability.unsupported_workflow_capabilities.join(", ")
)));
}
}
if let Some(notice) = &self.create_notice {
lines.push(FormLine::keep(notice.clone()));
}
lines.push(FormLine::keep("Create profile [Enter/click]").at(*control));
*control += 1;
lines.push(FormLine::text("Cancel profile creation [Enter/click]").at(*control));
*control += 1;
lines
}
fn retained_notice(&self, form: &PolicySettings) -> Option<String> {
let current = form.workspace.policy.root()?;
let moving_away = !self.workspace_mode.is_persistent()
|| self.workspace_path.text().trim() != current.as_str();
moving_away.then(|| {
format!(
"Left in place: every slot under {current} remains on disk, including its _work \
directory. Nothing is moved or deleted."
)
})
}
fn field_row(&self, label: &str, field: &PathField, width: usize, empty: &str) -> String {
let hint = if field.is_editing() {
" [Esc cancel | Enter accept]"
} else {
" [Enter to edit]"
};
let budget = width
.saturating_sub(label.chars().count() + 2 + hint.chars().count() + 2)
.max(8);
let view = field.view(budget);
let rendered = view.rendered();
let shown = if rendered.is_empty() {
empty.to_owned()
} else {
rendered
};
format!("{label}: {shown}{hint}")
}
}
fn notice_for(
checked: Result<LocalAbsolutePath, CliError>,
affected: AffectedAttempts,
subject: &str,
accepted_prefix: &str,
) -> String {
match checked {
Err(error) => error.remedy().map_or_else(
|| format!("error: {error}"),
|remedy| format!("error: {error} ({remedy})"),
),
Ok(_) if !affected.is_empty() => format!("refused: {}", affected.refusal(subject)),
Ok(root) => format!("ready: {accepted_prefix} {root}."),
}
}
fn lease_summary(leases: &[cli::workspace::SlotLease]) -> String {
if leases.is_empty() {
return "Slot leases: none".to_owned();
}
let rendered = leases
.iter()
.map(|lease| {
format!(
"s{} {}",
lease.slot,
if lease.cleanup_blocked {
"cleanup-blocked"
} else {
"active"
}
)
})
.collect::<Vec<_>>()
.join(", ");
format!("Slot leases: {rendered}")
}
fn wrap(text: &str, width: usize) -> Vec<String> {
let width = width.max(1);
if text.chars().count() <= width {
return vec![text.to_owned()];
}
let mut rows: Vec<String> = Vec::new();
let mut current = String::new();
let mut used = 0usize;
for word in text.split(' ') {
let length = word.chars().count();
if used > 0 && used + 1 + length > width {
rows.push(std::mem::take(&mut current));
used = 0;
}
if length > width {
if used > 0 {
rows.push(std::mem::take(&mut current));
}
let characters: Vec<char> = word.chars().collect();
for chunk in characters.chunks(width) {
rows.push(chunk.iter().collect());
}
current = rows.pop().unwrap_or_default();
used = current.chars().count();
continue;
}
if used > 0 {
current.push(' ');
used += 1;
}
current.push_str(word);
used += length;
}
rows.push(current);
rows
}
pub fn render(frame: &mut Frame<'_>, area: Rect, ui: &SettingsUi, compact: bool) {
let width = content_width(area.width);
let mut lines: Vec<Line<'_>> = ui
.rows(width, compact)
.into_iter()
.map(|row| {
let focused = row.control == Some(ui.focus);
styled_form_line(ui, &row, focused)
})
.collect();
if let Some(message) = &ui.message {
lines.extend(
message
.lines()
.flat_map(|line| wrap(line, width))
.map(|line| {
let colour = if line.to_ascii_lowercase().starts_with("error") {
Color::Red
} else {
Color::Green
};
Line::from(Span::styled(line, Style::default().fg(colour)))
}),
);
}
frame.render_widget(
Paragraph::new(lines).block(
Block::default()
.title(Span::styled(
"Settings",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)),
),
area,
);
}
#[must_use]
pub fn content_width(area_width: u16) -> usize {
usize::from(area_width.saturating_sub(2)).max(1)
}
fn styled_form_line<'a>(ui: &SettingsUi, row: &FormLine, focused: bool) -> Line<'a> {
let control = row
.control
.and_then(|index| ui.controls().into_iter().nth(index));
let mut style = match control {
Some(
Control::HostSave
| Control::HostRootSave
| Control::PolicySave
| Control::PolicyLabelsSave
| Control::WorkspaceSave,
) => Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
Some(Control::HostRootReset) => Style::default().fg(Color::Yellow),
Some(_) => Style::default().fg(Color::Cyan),
None => Style::default(),
};
if focused {
style = style.add_modifier(Modifier::BOLD | Modifier::REVERSED);
}
if control.is_some() {
return Line::from(Span::styled(row.text.clone(), style));
}
let lower = row.text.to_ascii_lowercase();
if row.text == "Host settings" || row.text.starts_with("Target: ") {
return Line::from(Span::styled(
row.text.clone(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
}
if lower.contains("warning:")
|| lower.starts_with("disabling means")
|| lower.starts_with("left in place:")
|| lower.contains("trust boundary")
{
return Line::from(Span::styled(
row.text.clone(),
Style::default().fg(Color::Yellow),
));
}
if let Some((label, value)) = row.text.split_once(':') {
return Line::from(vec![
Span::styled(format!("{label}:"), Style::default().fg(Color::DarkGray)),
Span::styled(value.to_owned(), Style::default().fg(Color::Gray)),
]);
}
Line::from(Span::styled(
row.text.clone(),
Style::default().fg(Color::DarkGray),
))
}
#[cfg(test)]
fn dispatch_capacity(
context: &Context,
target: &ScaleTarget,
maximum: u16,
out: &mut dyn Write,
) -> Result<(), CliError> {
match target.scope() {
TargetScope::Repository => cli::policy::dispatch_repo(
context,
&RepoCommand::SetCapacity(RepoSetCapacityArgs {
repository: target.to_string(),
profile: None,
max_capacity: maximum,
}),
out,
),
TargetScope::Organization => cli::policy::dispatch_org(
context,
&OrgCommand::SetCapacity(OrgSetCapacityArgs {
organization: target.to_string(),
max_capacity: maximum,
}),
out,
),
}
}
#[cfg(test)]
fn dispatch_scale(
context: &Context,
target: &ScaleTarget,
enabled: bool,
out: &mut dyn Write,
) -> Result<(), CliError> {
match target.scope() {
TargetScope::Repository => cli::policy::dispatch_repo(
context,
&RepoCommand::SetScale(RepoSetScaleArgs {
repository: target.to_string(),
profile: None,
enabled,
}),
out,
),
TargetScope::Organization => cli::policy::dispatch_org(
context,
&OrgCommand::SetScale(OrgSetScaleArgs {
organization: target.to_string(),
enabled,
}),
out,
),
}
}
#[cfg(test)]
fn find_policy(store: &dyn Store, target: &ScaleTarget) -> Result<ScalePolicy, CliError> {
store
.policies()
.map_err(store_failure)?
.into_iter()
.find(|policy| &policy.target == target)
.ok_or_else(|| CliError::new(Failure::NotFound, format!("no policy for {target} exists")))
}
fn step_u32(current: u32, increase: bool, step: u32, minimum: u32) -> u32 {
if increase {
current.saturating_add(step)
} else {
current.saturating_sub(step).max(minimum)
}
}
fn step_backend(current: BackendMode, increase: bool) -> BackendMode {
match (current, increase) {
(BackendMode::Auto, true) | (BackendMode::WindowsHyperVContainer, false) => {
BackendMode::Oci
}
(BackendMode::Oci, true) | (BackendMode::VirtualMachine, false) => {
BackendMode::WindowsHyperVContainer
}
(BackendMode::WindowsHyperVContainer, true) | (BackendMode::Auto, false) => {
BackendMode::VirtualMachine
}
(BackendMode::VirtualMachine, true) | (BackendMode::Oci, false) => BackendMode::Auto,
}
}
fn isolation_args(
mode: ExecutionMode,
backend: BackendMode,
image: &PathField,
cpu: u32,
memory: u32,
disk: u32,
) -> IsolationArgs {
match mode {
ExecutionMode::Native => IsolationArgs {
backend: None,
image: None,
cpu: None,
memory: None,
disk: None,
},
ExecutionMode::Isolated => IsolationArgs {
backend: Some(backend),
image: Some(image.text()),
cpu: Some(cpu),
memory: Some(memory),
disk: Some(disk),
},
}
}
const fn execution_mode_name(mode: ExecutionMode) -> &'static str {
match mode {
ExecutionMode::Native => "native",
ExecutionMode::Isolated => "isolated",
}
}
const fn backend_mode_name(backend: BackendMode) -> &'static str {
match backend {
BackendMode::Auto => "auto",
BackendMode::Oci => "oci",
BackendMode::WindowsHyperVContainer => "windows-hyper-v-container",
BackendMode::VirtualMachine => "virtual-machine",
}
}
const fn backend_name(backend: Backend) -> &'static str {
match backend {
Backend::Auto => "auto",
Backend::Oci => "oci",
Backend::WindowsHyperVContainer => "windows-hyper-v-container",
Backend::VirtualMachine => "virtual-machine",
}
}
fn backend_capability(
backend: BackendMode,
capabilities: &[cli::host::IsolationCapability],
) -> cli::host::IsolationCapability {
use cli::host::{IsolationBackend, IsolationObservation, IsolationReadiness};
let (backend, state) = match backend {
BackendMode::Auto => (IsolationBackend::Auto, IsolationReadiness::NotInstalled),
BackendMode::Oci => (IsolationBackend::Oci, IsolationReadiness::NotInstalled),
BackendMode::WindowsHyperVContainer => (
IsolationBackend::WindowsHyperVContainer,
IsolationReadiness::Unsupported,
),
BackendMode::VirtualMachine => (
IsolationBackend::VirtualMachine,
IsolationReadiness::NotInstalled,
),
};
let fallback = cli::host::sanitize_isolation_observation(IsolationObservation {
backend,
state,
raw_output: None,
});
let wanted = if matches!(backend, IsolationBackend::Auto) && cfg!(windows) {
IsolationBackend::WindowsHyperVContainer
} else {
backend
};
capabilities
.iter()
.copied()
.find(|capability| capability.backend == wanted)
.unwrap_or(fallback)
}
fn invalid(source: impl std::fmt::Display) -> CliError {
CliError::new(Failure::InvalidArgument, source.to_string())
}
fn store_failure(source: StoreError) -> CliError {
if source.is_conflict() {
CliError::with_remedy(
Failure::Conflict,
source.to_string(),
"re-open settings, then retry",
)
} else {
CliError::new(Failure::LocalState, source.to_string())
}
}
fn service_failure(source: ServiceError) -> CliError {
CliError::with_remedy(
Failure::LocalState,
source.to_string(),
"runner-manager service status",
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::io::{BufRead as _, BufReader};
use std::net::{TcpListener, TcpStream};
use std::num::NonZeroU16;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use runner_manager_domain::attempt::RunnerAttempt;
use runner_manager_domain::execution::{ImageReference, ResourceLimits};
use runner_manager_domain::model::{
Arch, AttemptId, Host, HostId, HostLabel, Label, Os, PolicyId,
};
use runner_manager_domain::policy::{PolicyState, RoutingLabels};
use runner_manager_github::Endpoints;
use secrecy::SecretString;
use tempfile::TempDir;
fn nz(value: u16) -> NonZeroU16 {
NonZeroU16::new(value).unwrap()
}
fn fixture(monitor_only: bool) -> (TempDir, Context, ScaleTarget) {
let dir = TempDir::new().unwrap();
let context = Context::resolve(Some(dir.path()), &mut Vec::new()).unwrap();
let store = context.store().unwrap();
let host = Host::new(
HostId::from_u128(1),
"local-home",
Os::Linux,
Arch::X64,
nz(4),
chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap(),
)
.unwrap();
store.put_host(&host).unwrap();
let target = ScaleTarget::repository("octo/repo").unwrap();
let mode = if monitor_only {
PolicyMode::MonitorOnly
} else {
PolicyMode::autoscale(
RoutingLabels::derive(&HostLabel::new("home").unwrap(), Os::Linux, Arch::X64),
0,
nz(2),
)
.unwrap()
};
let policy = ScalePolicy::new_for_host_label(
PolicyId::from_u128(2),
target.clone(),
7,
host.id,
HostLabel::new("home").unwrap(),
mode,
CachePolicy::default(),
);
store.insert_policy(&policy).unwrap();
drop(store);
(dir, context, target)
}
struct ProfileGithub {
base_url: String,
stop: Arc<AtomicBool>,
worker: Option<std::thread::JoinHandle<()>>,
}
impl ProfileGithub {
fn start() -> Self {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
listener.set_nonblocking(true).unwrap();
let base_url = format!("http://{}/", listener.local_addr().unwrap());
let stop = Arc::new(AtomicBool::new(false));
let worker_stop = Arc::clone(&stop);
let worker = std::thread::spawn(move || {
while !worker_stop.load(Ordering::Relaxed) {
match listener.accept() {
Ok((stream, _)) => {
std::thread::spawn(move || answer_profile_github(stream))
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(std::time::Duration::from_millis(2));
continue;
}
Err(_) => break,
};
}
});
Self {
base_url,
stop,
worker: Some(worker),
}
}
}
impl Drop for ProfileGithub {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(worker) = self.worker.take() {
worker.join().unwrap();
}
}
}
fn answer_profile_github(mut stream: TcpStream) {
stream.set_nonblocking(false).unwrap();
let mut request = String::new();
{
let mut reader = BufReader::new(&mut stream);
reader.read_line(&mut request).unwrap();
}
let path = request.split_whitespace().nth(1).unwrap_or("/");
let body = if path.starts_with("/user/installations/7/repositories") {
r#"{"total_count":1,"repositories":[{"full_name":"octo/repo"}]}"#
} else if path.starts_with("/user/installations") {
r#"{"total_count":1,"installations":[{"id":7,"account":{"login":"octo","type":"Organization"},"repository_selection":"selected","permissions":{"administration":"write","actions":"read"}}]}"#
} else {
r#"{"message":"not found"}"#
};
let status = if path.starts_with("/user/installations") {
"200 OK"
} else {
"404 Not Found"
};
write!(
stream,
"HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
)
.unwrap();
}
fn authenticated_fixture() -> (TempDir, ProfileGithub, Context, ScaleTarget) {
let dir = TempDir::new().unwrap();
let github = ProfileGithub::start();
let endpoints = Endpoints::for_test_server(&github.base_url).unwrap();
let context = Context::rooted_against(dir.path(), endpoints).unwrap();
let store = context.store().unwrap();
let host = Host::new(
HostId::from_u128(1),
"local-home",
Os::Linux,
Arch::X64,
nz(4),
chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap(),
)
.unwrap();
store.put_host(&host).unwrap();
let target = ScaleTarget::repository("octo/repo").unwrap();
let policy = ScalePolicy::new_for_host_label(
PolicyId::from_u128(2),
target.clone(),
7,
host.id,
HostLabel::new("home").unwrap(),
PolicyMode::autoscale(
RoutingLabels::derive(&HostLabel::new("home").unwrap(), Os::Linux, Arch::X64),
0,
nz(2),
)
.unwrap(),
CachePolicy::default(),
);
store.insert_policy(&policy).unwrap();
let mode = context.recorded_start_mode(&store).unwrap();
drop(store);
context
.secret_store(mode)
.unwrap()
.store(&SecretString::from("ghu_profile_fixture_token"))
.unwrap();
(dir, github, context, target)
}
#[test]
fn settings_command_creates_drains_and_removes_one_profile() {
let (_dir, _github, context, target) = authenticated_fixture();
let mut ui = SettingsUi::default();
ui.execute(
&context,
SettingsCommand::LoadProfile {
target: target.to_string(),
profile: "default".into(),
},
);
ui.profile_create_open = true;
ui.create_name.reset_to("build");
ui.create_capacity = 3;
ui.execute(&context, SettingsCommand::CreateProfile);
let SettingsView::Policy(form) = &ui.view else {
panic!("created profile must load")
};
assert_eq!(form.profile_name, "build", "{:?}", ui.message);
assert_eq!(form.current_max_capacity, Some(3));
assert_ne!(form.policy_id, PolicyId::from_u128(2));
ui.policy_draft.enabled = Some(true);
ui.execute(&context, SettingsCommand::ApplyPolicy);
assert!(
cli::policy::find_policy_selected(&context.store().unwrap(), &target, Some("build"))
.unwrap()
.enabled()
);
ui.execute(&context, SettingsCommand::DrainProfile);
assert!(
!cli::policy::find_policy_selected(&context.store().unwrap(), &target, Some("build"))
.unwrap()
.enabled()
);
ui.execute(&context, SettingsCommand::RemoveProfile);
assert!(ui.awaiting_remove_confirmation);
ui.execute(&context, SettingsCommand::RemoveProfile);
let store = context.store().unwrap();
assert!(cli::policy::find_policy_selected(&store, &target, Some("default")).is_ok());
assert!(cli::policy::find_policy_selected(&store, &target, Some("build")).is_err());
}
#[test]
fn every_profile_and_isolation_control_has_keyboard_mouse_focus_and_compact_reachability() {
let (_dir, context, target) = fixture(false);
let isolated_ui = || {
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
ui.execution_mode = ExecutionMode::Isolated;
ui.profile_create_open = true;
ui.create_execution_mode = ExecutionMode::Isolated;
ui
};
let new_controls = [
Control::PolicyLabels,
Control::PolicyLabelsSave,
Control::PolicyExecutionMode,
Control::PolicyExecutionBackend,
Control::PolicyExecutionImage,
Control::PolicyExecutionCpu,
Control::PolicyExecutionMemory,
Control::PolicyExecutionDisk,
Control::PolicyExecutionSave,
Control::PolicyDrain,
Control::PolicyRemove,
Control::PolicyAddProfile,
Control::ProfileName,
Control::ProfileCapacity,
Control::ProfileExecutionMode,
Control::ProfileExecutionBackend,
Control::ProfileExecutionImage,
Control::ProfileExecutionCpu,
Control::ProfileExecutionMemory,
Control::ProfileExecutionDisk,
Control::ProfileCreate,
Control::ProfileCreateCancel,
];
for control in new_controls {
let mut keyboard = isolated_ui();
if control == Control::PolicyAddProfile {
keyboard.profile_create_open = false;
}
focus_by_keyboard(&mut keyboard, control);
assert_eq!(keyboard.focused(), Some(control));
let focus = keyboard.focus;
let normal = keyboard.control_rows(100, false);
assert!(
normal.contains(&Some(focus)),
"{control:?} has no normal-width row"
);
let compact = keyboard.control_rows(42, true);
assert!(
compact.contains(&Some(focus)),
"{control:?} has no compact row"
);
let keyboard_result = if control.is_adjustable() {
keyboard.key(KeyCode::Right)
} else {
keyboard.key(KeyCode::Enter)
};
assert_control_activated(&keyboard, control, keyboard_result.as_ref());
let mut mouse = isolated_ui();
if control == Control::PolicyAddProfile {
mouse.profile_create_open = false;
}
let mouse_focus = mouse
.controls()
.iter()
.position(|item| *item == control)
.unwrap();
let row = mouse
.control_rows(100, false)
.iter()
.position(|mapped| *mapped == Some(mouse_focus))
.unwrap();
let mouse_result = mouse.click(row as u16, 100, false);
assert_eq!(
mouse.focused(),
Some(if control == Control::PolicyAddProfile {
Control::ProfileName
} else if control == Control::ProfileCreateCancel {
Control::PolicyAddProfile
} else {
control
})
);
assert_control_activated(&mouse, control, mouse_result.as_ref());
}
}
fn assert_control_activated(
ui: &SettingsUi,
control: Control,
command: Option<&SettingsCommand>,
) {
match control {
Control::PolicyLabels => assert!(ui.policy_labels.is_editing()),
Control::PolicyLabelsSave => assert_eq!(command, Some(&SettingsCommand::SaveLabels)),
Control::PolicyExecutionMode => assert_eq!(ui.execution_mode, ExecutionMode::Native),
Control::PolicyExecutionBackend => assert_ne!(ui.execution_backend, BackendMode::Auto),
Control::PolicyExecutionImage => assert!(ui.execution_image.is_editing()),
Control::PolicyExecutionCpu => assert_eq!(ui.execution_cpu, 1100),
Control::PolicyExecutionMemory => assert_eq!(ui.execution_memory, 1280),
Control::PolicyExecutionDisk => assert_eq!(ui.execution_disk, 5120),
Control::PolicyExecutionSave => {
assert_eq!(command, Some(&SettingsCommand::SaveExecution))
}
Control::PolicyDrain => assert_eq!(command, Some(&SettingsCommand::DrainProfile)),
Control::PolicyRemove => assert_eq!(command, Some(&SettingsCommand::RemoveProfile)),
Control::PolicyAddProfile => assert!(ui.profile_create_open),
Control::ProfileName => assert!(ui.create_name.is_editing()),
Control::ProfileCapacity => assert_eq!(ui.create_capacity, 2),
Control::ProfileExecutionMode => {
assert_eq!(ui.create_execution_mode, ExecutionMode::Native)
}
Control::ProfileExecutionBackend => {
assert_ne!(ui.create_execution_backend, BackendMode::Auto)
}
Control::ProfileExecutionImage => assert!(ui.create_execution_image.is_editing()),
Control::ProfileExecutionCpu => assert_eq!(ui.create_execution_cpu, 1100),
Control::ProfileExecutionMemory => assert_eq!(ui.create_execution_memory, 1280),
Control::ProfileExecutionDisk => assert_eq!(ui.create_execution_disk, 5120),
Control::ProfileCreate => assert_eq!(command, Some(&SettingsCommand::CreateProfile)),
Control::ProfileCreateCancel => assert!(!ui.profile_create_open),
_ => unreachable!(),
}
}
#[test]
fn complete_selector_copy_includes_sorted_extra_labels() {
let (_dir, context, target) = fixture(false);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
ui.policy_labels.reset_to("trusted, gpu");
ui.execute(&context, SettingsCommand::SaveLabels);
let SettingsView::Policy(form) = &ui.view else {
panic!("policy remains loaded")
};
assert_eq!(
form.copyable_runs_on.as_deref(),
Some("rm-home-linux-x64, gpu, trusted")
);
focus_by_keyboard(&mut ui, Control::PolicyLabels);
assert_eq!(
ui.copy_text().as_deref(),
Some("rm-home-linux-x64, gpu, trusted")
);
let copy_row = ui
.rows(100, false)
.iter()
.position(|row| row.copy.as_deref() == Some("rm-home-linux-x64, gpu, trusted"))
.unwrap();
assert_eq!(
ui.click(copy_row as u16, 100, false),
Some(SettingsCommand::Copy(
"rm-home-linux-x64, gpu, trusted".to_owned()
))
);
}
#[test]
fn execution_save_rejects_invalid_image_and_resources_without_persistence() {
let (_dir, context, target) = fixture(false);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
ui.execution_mode = ExecutionMode::Isolated;
ui.execution_backend = BackendMode::Oci;
ui.execution_image
.reset_to("registry.example/runner:latest");
ui.execute(&context, SettingsCommand::SaveExecution);
assert!(ui.message.as_deref().unwrap().contains("pinned"));
assert!(
cli::policy::find_policy_selected(&context.store().unwrap(), &target, Some("default"))
.unwrap()
.execution_policy()
.is_native()
);
ui.execution_image.reset_to(&format!(
"registry.example/runner@sha256:{}",
"a".repeat(64)
));
ui.execution_cpu = 99;
ui.execute(&context, SettingsCommand::SaveExecution);
assert!(
ui.message
.as_deref()
.unwrap()
.contains("below the required floor")
);
assert!(
cli::policy::find_policy_selected(&context.store().unwrap(), &target, Some("default"))
.unwrap()
.execution_policy()
.is_native()
);
ui.execution_backend = BackendMode::WindowsHyperVContainer;
let rendered = rows_text(&ui, 100, false);
assert!(
rendered.contains("Selected provider: unsupported"),
"{rendered}"
);
assert!(
rendered.contains("remedy: use Windows 11 Pro or Enterprise with Docker Desktop"),
"{rendered}"
);
assert!(!rendered.contains("Education"), "{rendered}");
}
#[test]
fn execution_save_persists_custom_backend_image_and_resources() {
let (_dir, context, target) = fixture(false);
let image = format!("registry.example/runner@sha256:{}", "b".repeat(64));
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
ui.execution_mode = ExecutionMode::Isolated;
ui.execution_backend = BackendMode::Oci;
ui.execution_image.reset_to(&image);
ui.execution_cpu = 2300;
ui.execution_memory = 3328;
ui.execution_disk = 12288;
ui.execute(&context, SettingsCommand::SaveExecution);
let stored =
cli::policy::find_policy_selected(&context.store().unwrap(), &target, Some("default"))
.unwrap();
assert_eq!(
stored.execution_policy(),
&ExecutionPolicy::Isolated {
backend: Backend::Oci,
image: ImageReference::new(image).unwrap(),
resources: ResourceLimits {
cpu_millis: 2300,
memory_mib: 3328,
disk_mib: 12288,
},
}
);
}
#[test]
fn provider_raw_output_is_discarded_before_render_and_copy_surfaces() {
use cli::host::{
IsolationBackend, IsolationObservation, IsolationReadiness,
sanitize_isolation_observation,
};
let sentinel = format!("{}{}", "ghu_", "raw_provider_secret_never_rendered");
let capability = sanitize_isolation_observation(IsolationObservation {
backend: IsolationBackend::WindowsHyperVContainer,
state: IsolationReadiness::Unsupported,
raw_output: Some(&sentinel),
});
assert_eq!(capability.state, IsolationReadiness::Unsupported);
assert_eq!(capability.remedy, Some("use a supported host and provider"));
let (_dir, context, target) = fixture(false);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
let SettingsView::Policy(form) = &mut ui.view else {
panic!("policy loaded")
};
form.provider_diagnostics = vec![capability];
let rows = ui.rows(100, false);
let rendered = rows
.iter()
.map(|row| row.text.as_str())
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains("Hyper-V container provider: unsupported"));
assert!(!rendered.contains(&sentinel));
assert!(
rows.iter()
.filter_map(|row| row.copy.as_deref())
.all(|copy| !copy.contains(&sentinel))
);
for control in ui.controls() {
focus_by_keyboard(&mut ui, control);
assert!(ui.copy_text().is_none_or(|copy| !copy.contains(&sentinel)));
}
}
#[test]
fn forms_stay_inside_the_five_action_budget_and_monitor_only_has_no_noop_toggle() {
let (_dir, context, target) = fixture(false);
let autoscale = PolicySettings::load(&context, &target).unwrap();
assert!(autoscale.focused_action_count() <= MAX_FOCUSED_FORM_ACTIONS);
assert!(autoscale.exposes_scale_toggle());
let (_dir, context, target) = fixture(true);
let monitor = PolicySettings::load(&context, &target).unwrap();
assert!(monitor.focused_action_count() <= MAX_FOCUSED_FORM_ACTIONS);
assert!(!monitor.exposes_scale_toggle());
assert_eq!(monitor.current_max_capacity, None);
}
#[test]
fn views_show_current_values_identity_and_copy_safe_labels() {
let (_dir, context, target) = fixture(false);
let policy = PolicySettings::load(&context, &target).unwrap();
assert_eq!(policy.current_max_capacity, Some(2));
assert_eq!(policy.host_identity, "local-home");
assert_eq!(policy.host_label.as_deref(), Some("rm-home-linux-x64"));
assert!(policy.extra_labels.is_empty());
assert_eq!(
policy.copyable_runs_on.as_deref(),
Some("rm-home-linux-x64")
);
let host = HostSettings::load(&context).unwrap();
assert_eq!((host.current_capacity, host.current_in_use), (4, 0));
assert!(host.focused_action_count() <= MAX_FOCUSED_FORM_ACTIONS);
}
#[test]
fn host_and_repository_settings_use_semantic_colours() {
let (_dir, context, target) = fixture(false);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadHost);
let host_rows = ui.rows(118, false);
let host_heading = host_rows
.iter()
.find(|row| row.text == "Host settings")
.unwrap();
let host_save = host_rows
.iter()
.find(|row| row.text.starts_with("Save host settings"))
.unwrap();
assert_eq!(
styled_form_line(&ui, host_heading, false).spans[0].style.fg,
Some(Color::Cyan)
);
assert_eq!(
styled_form_line(&ui, host_save, false).spans[0].style.fg,
Some(Color::Green)
);
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
let repository_rows = ui.rows(118, false);
let target = repository_rows
.iter()
.find(|row| row.text.starts_with("Target:"))
.unwrap();
let policy_save = repository_rows
.iter()
.find(|row| row.text.starts_with("Confirm policy"))
.unwrap();
assert_eq!(
styled_form_line(&ui, target, false).spans[0].style.fg,
Some(Color::Cyan)
);
assert_eq!(
styled_form_line(&ui, policy_save, false).spans[0].style.fg,
Some(Color::Green)
);
}
#[test]
fn interval_preview_enforces_floor_and_updates_both_budget_numbers() {
let (_dir, context, _) = fixture(false);
let form = HostSettings::load(&context).unwrap();
assert_eq!(
form.preview_interval(29).unwrap_err().class(),
Failure::InvalidArgument
);
let faster = form.preview_interval(30).unwrap();
let slower = form.preview_interval(120).unwrap();
assert!(faster.projected_requests_per_hour > slower.projected_requests_per_hour);
assert!(faster.maximum_repository_targets < slower.maximum_repository_targets);
}
#[test]
fn tui_host_capacity_is_the_cli_handler() {
let (_dir, context, _) = fixture(false);
HostSettings::set_capacity(&context, 7, &mut Vec::new()).unwrap();
assert_eq!(
cli::host::local_host(&context.store().unwrap())
.unwrap()
.unwrap()
.host_capacity(),
7
);
assert_eq!(
HostSettings::set_capacity(&context, 0, &mut Vec::new())
.unwrap_err()
.class(),
Failure::InvalidArgument
);
}
#[test]
fn cli_and_tui_persist_byte_equivalent_host_policy_enable_disable_and_promotion_state() {
let (_a, tui, target) = fixture(false);
let (_b, cli_context, cli_target) = fixture(false);
HostSettings::set_capacity(&tui, 7, &mut Vec::new()).unwrap();
cli::host::set_capacity(
&cli_context,
&HostSetCapacityArgs { capacity: 7 },
&mut Vec::new(),
)
.unwrap();
assert_eq!(
cli::host::local_host(&tui.store().unwrap()).unwrap(),
cli::host::local_host(&cli_context.store().unwrap()).unwrap()
);
let form = PolicySettings::load(&tui, &target).unwrap();
form.apply(
&tui,
&PolicyDraft {
max_capacity: Some(5),
enabled: Some(true),
cache_policy: None,
},
None,
&mut Vec::new(),
)
.unwrap();
dispatch_capacity(&cli_context, &cli_target, 5, &mut Vec::new()).unwrap();
dispatch_scale(&cli_context, &cli_target, true, &mut Vec::new()).unwrap();
assert_eq!(
find_policy(&tui.store().unwrap(), &target).unwrap(),
find_policy(&cli_context.store().unwrap(), &cli_target).unwrap()
);
let form = PolicySettings::load(&tui, &target).unwrap();
form.apply(
&tui,
&PolicyDraft {
enabled: Some(false),
..PolicyDraft::default()
},
None,
&mut Vec::new(),
)
.unwrap();
dispatch_scale(&cli_context, &cli_target, false, &mut Vec::new()).unwrap();
assert_eq!(
find_policy(&tui.store().unwrap(), &target).unwrap(),
find_policy(&cli_context.store().unwrap(), &cli_target).unwrap()
);
let (_a, tui, target) = fixture(true);
let (_b, cli_context, cli_target) = fixture(true);
PolicySettings::load(&tui, &target)
.unwrap()
.apply(
&tui,
&PolicyDraft {
max_capacity: Some(3),
..PolicyDraft::default()
},
None,
&mut Vec::new(),
)
.unwrap();
dispatch_capacity(&cli_context, &cli_target, 3, &mut Vec::new()).unwrap();
assert_eq!(
find_policy(&tui.store().unwrap(), &target).unwrap(),
find_policy(&cli_context.store().unwrap(), &cli_target).unwrap()
);
}
#[test]
fn policy_capacity_and_monitor_promotion_use_the_cli_handler() {
for monitor_only in [false, true] {
let (_dir, context, target) = fixture(monitor_only);
let form = PolicySettings::load(&context, &target).unwrap();
form.apply(
&context,
&PolicyDraft {
max_capacity: Some(5),
..PolicyDraft::default()
},
None,
&mut Vec::new(),
)
.unwrap();
let stored = find_policy(&context.store().unwrap(), &target).unwrap();
assert_eq!(stored.max_capacity().map(NonZeroU16::get), Some(5));
assert_eq!(stored.state(), PolicyState::Pending);
assert!(!stored.enabled());
}
}
#[test]
fn enablement_previews_warning_and_dispatches_cli_output() {
let (_dir, context, target) = fixture(false);
let form = PolicySettings::load(&context, &target).unwrap();
let draft = PolicyDraft {
enabled: Some(true),
..PolicyDraft::default()
};
assert_eq!(form.preview(&draft).trust_warning, Some(FORK_TRUST_WARNING));
let mut output = Vec::new();
form.apply(&context, &draft, None, &mut output).unwrap();
assert!(
String::from_utf8(output)
.unwrap()
.contains(FORK_TRUST_WARNING)
);
assert!(
find_policy(&context.store().unwrap(), &target)
.unwrap()
.enabled()
);
}
#[test]
fn disable_preview_is_honest_and_never_promises_termination() {
let (_dir, context, target) = fixture(false);
dispatch_scale(&context, &target, true, &mut Vec::new()).unwrap();
let form = PolicySettings::load(&context, &target).unwrap();
let draft = PolicyDraft {
enabled: Some(false),
..PolicyDraft::default()
};
assert_eq!(form.preview(&draft).state_after_disable, Some("disabled"));
form.apply(&context, &draft, None, &mut Vec::new()).unwrap();
assert_eq!(
find_policy(&context.store().unwrap(), &target)
.unwrap()
.state(),
PolicyState::Disabled
);
}
#[test]
fn active_disable_uses_only_the_drain_confirmation_and_keeps_work_alive() {
let (_dir, context, target) = fixture(false);
dispatch_scale(&context, &target, true, &mut Vec::new()).unwrap();
let store = context.store().unwrap();
let policy = find_policy(&store, &target).unwrap();
let attempt = RunnerAttempt::allocate(
AttemptId::from_u128(3),
policy.id,
"active-runtime",
chrono::DateTime::from_timestamp(1_700_000_001, 0).unwrap(),
);
store.record_attempt(&attempt).unwrap();
drop(store);
let form = PolicySettings::load(&context, &target).unwrap();
let draft = PolicyDraft {
enabled: Some(false),
..PolicyDraft::default()
};
let preview = form.preview(&draft);
assert_eq!(preview.active_runners, 1);
assert_eq!(preview.state_after_disable, Some("draining"));
assert!(preview.drain_confirmation_required);
assert_eq!(
form.apply(&context, &draft, None, &mut Vec::new())
.unwrap_err()
.class(),
Failure::Conflict
);
let mut output = Vec::new();
let observation = cli::policy::observe_scale(&context, &target).unwrap();
form.apply(&context, &draft, Some(observation), &mut output)
.unwrap();
let output = String::from_utf8(output).unwrap();
assert!(
output.contains("draining with 1 active runner(s)"),
"{output}"
);
assert!(output.contains("not terminated"), "{output}");
let store = context.store().unwrap();
assert_eq!(
find_policy(&store, &target).unwrap().state(),
PolicyState::Draining
);
assert!(store.attempt(AttemptId::from_u128(3)).unwrap().is_some());
}
#[test]
fn rejected_capacity_does_not_change_persisted_state_across_reload() {
let (_dir, context, target) = fixture(false);
let form = PolicySettings::load(&context, &target).unwrap();
let error = form
.apply(
&context,
&PolicyDraft {
max_capacity: Some(0),
..PolicyDraft::default()
},
None,
&mut Vec::new(),
)
.unwrap_err();
assert_eq!(error.class(), Failure::InvalidArgument);
assert_eq!(
PolicySettings::load(&context, &target)
.unwrap()
.current_max_capacity,
Some(2)
);
}
#[test]
fn invalid_combined_draft_persists_none_of_its_fields() {
let (_dir, context, target) = fixture(false);
let before = find_policy(&context.store().unwrap(), &target).unwrap();
let form = PolicySettings::load(&context, &target).unwrap();
let error = form
.apply(
&context,
&PolicyDraft {
max_capacity: Some(0),
enabled: Some(true),
cache_policy: Some(CachePolicy::DiscardRunnerPackage),
},
None,
&mut Vec::new(),
)
.unwrap_err();
assert_eq!(error.class(), Failure::InvalidArgument);
assert_eq!(
find_policy(&context.store().unwrap(), &target).unwrap(),
before
);
}
#[test]
fn monitor_precondition_failure_persists_no_cache_or_scale_change() {
let (_dir, context, target) = fixture(true);
let before = find_policy(&context.store().unwrap(), &target).unwrap();
let form = PolicySettings::load(&context, &target).unwrap();
let error = form
.apply(
&context,
&PolicyDraft {
max_capacity: None,
enabled: Some(true),
cache_policy: Some(CachePolicy::DiscardRunnerPackage),
},
None,
&mut Vec::new(),
)
.unwrap_err();
assert_eq!(error.class(), Failure::InvalidArgument);
assert_eq!(
find_policy(&context.store().unwrap(), &target).unwrap(),
before
);
}
#[test]
fn stale_combined_draft_persists_none_of_its_fields() {
let (_dir, context, target) = fixture(false);
let confirmation = cli::policy::observe_scale(&context, &target).unwrap();
cli::policy::apply_policy_mutation(
&context,
&target,
cli::policy::PolicyMutation {
max_capacity: Some(3),
..cli::policy::PolicyMutation::default()
},
None,
&mut Vec::new(),
)
.unwrap();
let before = find_policy(&context.store().unwrap(), &target).unwrap();
let form = PolicySettings::load(&context, &target).unwrap();
let error = form
.apply(
&context,
&PolicyDraft {
max_capacity: Some(4),
enabled: Some(false),
cache_policy: Some(CachePolicy::DiscardRunnerPackage),
},
Some(confirmation),
&mut Vec::new(),
)
.unwrap_err();
assert_eq!(error.class(), Failure::Conflict);
assert_eq!(
find_policy(&context.store().unwrap(), &target).unwrap(),
before
);
}
#[test]
fn organization_projection_floor_survives_load_and_live_preview() {
let (_dir, context, _) = fixture(false);
let store = context.store().unwrap();
let host = cli::host::local_host(&store).unwrap().unwrap();
let org = ScalePolicy::new_for_host_label(
PolicyId::from_u128(30),
ScaleTarget::organization("octo-org").unwrap(),
8,
host.id,
HostLabel::new("home").unwrap(),
PolicyMode::MonitorOnly,
CachePolicy::default(),
);
store.insert_policy(&org).unwrap();
drop(store);
let form = HostSettings::load(&context).unwrap();
assert!(form.projection_is_floor);
assert!(form.preview_interval(90).unwrap().projection_is_floor);
}
#[test]
fn over_budget_interval_is_refused_without_mutating_the_host() {
let (_dir, context, _) = fixture(false);
let store = context.store().unwrap();
let host = cli::host::local_host(&store).unwrap().unwrap();
for id in 40..70 {
let policy = ScalePolicy::new_for_host_label(
PolicyId::from_u128(id),
ScaleTarget::repository(format!("octo/repo-{id}")).unwrap(),
id as u64,
host.id,
HostLabel::new("home").unwrap(),
PolicyMode::MonitorOnly,
CachePolicy::default(),
);
store.insert_policy(&policy).unwrap();
}
drop(store);
let form = HostSettings::load(&context).unwrap();
let before = form.current_refresh_interval;
let error = form.set_refresh_interval(&context, 30).unwrap_err();
assert_eq!(error.class(), Failure::BudgetRefused);
assert_eq!(
HostSettings::load(&context)
.unwrap()
.current_refresh_interval,
before
);
}
#[test]
fn inverted_min_max_is_rejected_through_the_production_policy_command() {
let (_dir, context, target) = fixture(false);
let store = context.store().unwrap();
let old = find_policy(&store, &target).unwrap();
store.remove_policy(old.id, old.revision()).unwrap();
let replacement = ScalePolicy::new_for_host_label(
old.id,
target.clone(),
7,
old.host_id,
old.requested_host_label,
PolicyMode::autoscale(
RoutingLabels::derive(&HostLabel::new("home").unwrap(), Os::Linux, Arch::X64),
5,
nz(6),
)
.unwrap(),
CachePolicy::default(),
);
store.insert_policy(&replacement).unwrap();
drop(store);
let form = PolicySettings::load(&context, &target).unwrap();
let error = form
.apply(
&context,
&PolicyDraft {
max_capacity: Some(4),
..PolicyDraft::default()
},
None,
&mut Vec::new(),
)
.unwrap_err();
assert_eq!(error.class(), Failure::InvalidArgument);
assert_eq!(
PolicySettings::load(&context, &target)
.unwrap()
.current_max_capacity,
Some(6)
);
}
#[test]
fn service_mode_switch_persists_and_host_show_reads_the_same_mode() {
#[derive(Default)]
struct RecordingService {
requested_modes: RefCell<Vec<StartMode>>,
}
impl RecordingService {
fn switch(&self, mode: StartMode) -> Result<(), CliError> {
self.requested_modes.borrow_mut().push(mode);
Ok(())
}
}
let (_dir, context, _) = fixture(false);
let service = RecordingService::default();
HostSettings::set_start_mode_with(&context, StartMode::Login, |mode| service.switch(mode))
.unwrap();
assert_eq!(*service.requested_modes.borrow(), [StartMode::Login]);
let mut shown = Vec::new();
cli::host::show(&context, &mut shown).unwrap();
let shown = String::from_utf8(shown).unwrap();
assert!(shown.contains("service start mode login"), "{shown}");
assert_eq!(
HostSettings::load(&context).unwrap().current_start_mode,
StartMode::Login
);
}
#[test]
fn cache_policy_survives_a_fresh_context_and_settings_errors_render() {
let (dir, context, target) = fixture(false);
PolicySettings::load(&context, &target)
.unwrap()
.apply(
&context,
&PolicyDraft {
cache_policy: Some(CachePolicy::DiscardRunnerPackage),
..PolicyDraft::default()
},
None,
&mut Vec::new(),
)
.unwrap();
drop(context);
let restarted = Context::resolve(Some(dir.path()), &mut Vec::new()).unwrap();
assert_eq!(
PolicySettings::load(&restarted, &target)
.unwrap()
.cache_policy,
CachePolicy::DiscardRunnerPackage
);
let mut ui = SettingsUi::default();
ui.execute(&restarted, SettingsCommand::LoadPolicy(String::new()));
let mut terminal = Terminal::new(TestBackend::new(100, 20)).unwrap();
terminal
.draw(|frame| render(frame, frame.area(), &ui, false))
.unwrap();
let text = super::super::buffer_text(terminal.backend().buffer());
assert!(text.contains("error:"), "{text}");
}
use runner_manager_domain::attempt::{AttemptOutcome, FailureReason};
use runner_manager_domain::workspace::AttemptWorkspace;
use crate::cli::{HostCommand, HostSetRuntimeRootArgs};
struct Workspaces {
data: TempDir,
roots: TempDir,
context: Context,
platform_default: String,
target: ScaleTarget,
}
impl Workspaces {
fn new() -> Self {
Self::sharing(TempDir::new().expect("a temporary directory"))
}
fn sharing(roots: TempDir) -> Self {
let (data, context, target) = fixture(false);
let platform_default = HostSettings::load(&context)
.expect("the host row loads")
.runner_root
.rendered();
Self {
data,
roots,
context,
platform_default,
target,
}
}
fn root(&self, leaf: &str) -> String {
self.roots
.path()
.join(leaf)
.to_str()
.expect("a temporary path must be UTF-8")
.to_owned()
}
fn volatile_markers(&self) -> [String; 2] {
[self.roots.path(), self.data.path()].map(|path| {
path.file_name()
.expect("a temporary directory has a name")
.to_str()
.expect("a temporary path must be UTF-8")
.to_owned()
})
}
fn host_screen(&self) -> SettingsUi {
let mut ui = SettingsUi::default();
ui.execute(&self.context, SettingsCommand::LoadHost);
assert!(matches!(ui.view, SettingsView::Host(_)), "{:?}", ui.message);
ui
}
fn policy_screen(&self) -> SettingsUi {
let mut ui = SettingsUi::default();
ui.execute(
&self.context,
SettingsCommand::LoadPolicy(self.target.to_string()),
);
assert!(
matches!(ui.view, SettingsView::Policy(_)),
"{:?}",
ui.message
);
ui
}
fn stored_policy(&self) -> ScalePolicy {
find_policy(&self.context.store().unwrap(), &self.target).expect("the seeded policy")
}
fn stored_host(&self) -> runner_manager_domain::model::Host {
cli::host::local_host(&self.context.store().unwrap())
.unwrap()
.expect("the seeded host")
}
fn attempt(&self, id: u128, terminal: bool) {
self.attempt_in(id, terminal, AttemptWorkspace::Ephemeral);
}
fn attempt_in(&self, id: u128, terminal: bool, workspace: AttemptWorkspace) {
let store = self.context.store().unwrap();
let policy = find_policy(&store, &self.target).unwrap();
let mut attempt = RunnerAttempt::allocate_in(
AttemptId::from_u128(id),
policy.id,
"runtime",
workspace,
chrono::DateTime::from_timestamp(1_700_000_100, 0).unwrap(),
);
if terminal {
attempt
.conclude(
AttemptOutcome::failed(FailureReason::ProcessStartFailed),
chrono::DateTime::from_timestamp(1_700_000_200, 0).unwrap(),
)
.expect("allocated -> failed is a legal conclusion");
}
store.record_attempt(&attempt).unwrap();
}
}
fn focus_by_keyboard(ui: &mut SettingsUi, control: Control) {
ui.focus = 0;
for _ in 0..=ui.controls().len() {
if ui.focused() == Some(control) {
return;
}
let _ = ui.key(KeyCode::Down);
}
panic!("{control:?} is not reachable with Down from the first control");
}
fn type_path(ui: &mut SettingsUi, text: &str) {
for character in text.chars() {
assert!(
ui.key(KeyCode::Char(character)).is_none(),
"typing must not dispatch a command"
);
}
}
fn edit_path(ui: &mut SettingsUi, context: &Context, control: Control, text: &str) {
focus_by_keyboard(ui, control);
assert!(ui.key(KeyCode::Enter).is_none(), "Enter opens the editor");
assert!(ui.is_editing());
let _ = ui.key(KeyCode::End);
for _ in 0..field_of(ui, control).text().chars().count() {
let _ = ui.key(KeyCode::Backspace);
}
assert!(field_of(ui, control).text().is_empty());
type_path(ui, text);
let command = ui
.key(KeyCode::Enter)
.expect("Enter accepts and revalidates");
assert!(!ui.is_editing());
ui.execute(context, command);
}
fn field_of(ui: &SettingsUi, control: Control) -> &PathField {
match control {
Control::HostRunnerRoot => &ui.host_root,
Control::WorkspacePath => &ui.workspace_path,
Control::PolicyLabels => &ui.policy_labels,
other => panic!("{other:?} is not a text control"),
}
}
fn edit_labels(ui: &mut SettingsUi, context: &Context, text: &str) {
focus_by_keyboard(ui, Control::PolicyLabels);
assert!(ui.key(KeyCode::Enter).is_none(), "Enter opens the editor");
assert!(ui.is_editing());
let _ = ui.key(KeyCode::End);
for _ in 0..field_of(ui, Control::PolicyLabels).text().chars().count() {
let _ = ui.key(KeyCode::Backspace);
}
assert!(field_of(ui, Control::PolicyLabels).text().is_empty());
type_path(ui, text);
assert!(
ui.key(KeyCode::Enter).is_none(),
"accepting a label line previews nothing"
);
assert!(!ui.is_editing());
activate(ui, context, Control::PolicyLabelsSave);
}
fn stored_labels(context: &Context, target: &ScaleTarget) -> (String, Vec<String>) {
let form = PolicySettings::load(context, target).unwrap();
(form.host_label.unwrap(), form.extra_labels)
}
fn activate(ui: &mut SettingsUi, context: &Context, control: Control) {
focus_by_keyboard(ui, control);
let Some(command) = ui.key(KeyCode::Enter) else {
panic!("{control:?} must dispatch a command");
};
ui.execute(context, command);
}
fn rows_paragraph(ui: &SettingsUi, width: usize, compact: bool) -> String {
rows_text(ui, width, compact).replace('\n', " ")
}
fn rows_text(ui: &SettingsUi, width: usize, compact: bool) -> String {
ui.rows(width, compact)
.into_iter()
.map(|row| row.text)
.collect::<Vec<_>>()
.join("\n")
}
fn stable_rows(ui: &SettingsUi, width: usize, compact: bool, fixture: &Workspaces) -> String {
let redact = |text: &str| {
text.replace(&fixture.root("slots"), "<SLOTS>")
.replace(&fixture.root("configured"), "<CONFIGURED>")
.replace(&fixture.root("elsewhere"), "<ELSEWHERE>")
.replace(&fixture.platform_default, "<DEFAULT>")
};
let mut ui = ui.clone();
let host_root = redact(&ui.host_root.text());
ui.host_root.reset_to(&host_root);
let workspace_path = redact(&ui.workspace_path.text());
ui.workspace_path.reset_to(&workspace_path);
let logical = ui
.form_lines(width)
.into_iter()
.map(|line| FormLine {
text: redact(&line.text),
..line
})
.collect();
let text = SettingsUi::lay_out(logical, width, compact, ui.focus)
.into_iter()
.map(|row| row.text)
.collect::<Vec<_>>()
.join("\n");
for marker in fixture.volatile_markers() {
assert!(
!text.contains(&marker),
"a host path outlived the substitution -- a path control \
scrolled it, so only a window of the value was ever drawn and \
there was no whole path left to replace:\n{text}"
);
}
text
}
#[test]
fn every_screen_task_stays_inside_the_five_action_budget() {
let fixture = Workspaces::new();
assert!(HostSettings::workspace_action_count() <= MAX_FOCUSED_FORM_ACTIONS);
let form = PolicySettings::load(&fixture.context, &fixture.target).unwrap();
assert_eq!(form.workspace_action_count(WorkspaceKind::Ephemeral), 2);
assert_eq!(form.workspace_action_count(WorkspaceKind::Persistent), 3);
assert!(form.workspace_action_count(WorkspaceKind::Persistent) <= MAX_FOCUSED_FORM_ACTIONS);
assert!(!form.is_organization());
assert_eq!(fixture.host_screen().controls().len(), 7);
let mut policy = fixture.policy_screen();
assert!(!policy.controls().contains(&Control::WorkspacePath));
focus_by_keyboard(&mut policy, Control::WorkspaceMode);
let _ = policy.key(KeyCode::Right);
assert_eq!(policy.workspace_mode, WorkspaceKind::Persistent);
assert!(policy.controls().contains(&Control::WorkspacePath));
}
#[test]
fn drawn_rows_and_focusable_controls_agree_on_every_view() {
let fixture = Workspaces::new();
let mut persistent = fixture.policy_screen();
focus_by_keyboard(&mut persistent, Control::WorkspaceMode);
let _ = persistent.key(KeyCode::Right);
let screens = vec![
SettingsUi::default(),
fixture.host_screen(),
fixture.policy_screen(),
persistent,
];
for ui in &screens {
for compact in [false, true] {
for width in [40usize, 118] {
let drawn: Vec<usize> = ui
.rows(width, compact)
.into_iter()
.filter_map(|row| row.control)
.collect();
assert!(
drawn.iter().all(|index| *index < ui.controls().len()),
"compact={compact} width={width}: a drawn row points past the end of \
the control list"
);
assert!(
drawn.windows(2).all(|pair| pair[0] < pair[1]),
"compact={compact} width={width}: each control is drawn once, in focus \
order, or Tab and the mouse disagree"
);
if !compact {
assert_eq!(
drawn,
(0..ui.controls().len()).collect::<Vec<_>>(),
"width={width}: every focusable control must be on screen"
);
}
}
}
}
}
#[test]
fn host_settings_show_the_effective_root_its_source_and_the_override_separately() {
let fixture = Workspaces::new();
let ui = fixture.host_screen();
let SettingsView::Host(form) = &ui.view else {
unreachable!()
};
assert_eq!(
form.runner_root.source(),
cli::workspace::RootSource::PlatformDefault
);
assert!(form.configured_root_text().is_empty());
assert!(ui.host_root.is_blank(), "nothing to edit until one is set");
let text = rows_text(&ui, 118, false);
assert!(text.contains("Runner root:"), "{text}");
assert!(text.contains("(platform-default)"), "{text}");
assert!(text.contains("(none - platform default)"), "{text}");
assert!(
text.contains("Affected attempts: 0 active, 0 awaiting cleanup"),
"{text}"
);
}
#[test]
fn tui_and_cli_store_byte_identical_workspace_values_and_render_one_message() {
let roots = TempDir::new().unwrap();
let host_root = roots.path().join("shared-root");
let slots = roots.path().join("slots");
std::fs::create_dir_all(&host_root).unwrap();
std::fs::create_dir_all(&slots).unwrap();
let host_root = host_root.to_str().unwrap().to_owned();
let slots = slots.to_str().unwrap().to_owned();
let through_tui = Workspaces::sharing(TempDir::new().unwrap());
let through_cli = Workspaces::sharing(TempDir::new().unwrap());
let rewrote_a_default = std::cell::Cell::new(false);
let one_surface = |rendered: Vec<u8>| {
let text = String::from_utf8(rendered).unwrap().trim().to_owned();
rewrote_a_default
.set(rewrote_a_default.get() || text.contains(&through_cli.platform_default));
text.replace(&through_cli.platform_default, &through_tui.platform_default)
};
let mut ui = through_tui.host_screen();
edit_path(
&mut ui,
&through_tui.context,
Control::HostRunnerRoot,
&host_root,
);
activate(&mut ui, &through_tui.context, Control::HostRootSave);
let mut cli_out = Vec::new();
cli::host::dispatch(
&through_cli.context,
&HostCommand::SetRuntimeRoot(HostSetRuntimeRootArgs {
path: host_root.clone(),
}),
cli::Styling::plain_for_buffer(),
&mut cli_out,
)
.unwrap();
assert_eq!(
through_tui.stored_host().runner_root_override,
through_cli.stored_host().runner_root_override,
);
assert_eq!(ui.message.as_deref().unwrap(), one_surface(cli_out));
activate(&mut ui, &through_tui.context, Control::HostRootReset);
let mut cli_out = Vec::new();
cli::host::dispatch(
&through_cli.context,
&HostCommand::ResetRuntimeRoot,
cli::Styling::plain_for_buffer(),
&mut cli_out,
)
.unwrap();
assert_eq!(through_tui.stored_host().runner_root_override, None);
assert_eq!(through_cli.stored_host().runner_root_override, None);
assert_eq!(ui.message.as_deref().unwrap(), one_surface(cli_out));
let mut ui = through_tui.policy_screen();
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
edit_path(
&mut ui,
&through_tui.context,
Control::WorkspacePath,
&slots,
);
activate(&mut ui, &through_tui.context, Control::WorkspaceSave);
let mut cli_out = Vec::new();
cli::policy::dispatch_repo(
&through_cli.context,
&RepoCommand::SetWorkspace(RepoSetWorkspaceArgs {
repository: through_cli.target.to_string(),
profile: None,
mode: WorkspaceMode::Persistent,
path: Some(slots.clone()),
}),
&mut cli_out,
)
.unwrap();
assert_eq!(
through_tui.stored_policy(),
through_cli.stored_policy(),
"one mutation from two surfaces must persist one policy, revision included"
);
assert_eq!(ui.message.as_deref().unwrap(), one_surface(cli_out));
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
activate(&mut ui, &through_tui.context, Control::WorkspaceSave);
let mut cli_out = Vec::new();
cli::policy::dispatch_repo(
&through_cli.context,
&RepoCommand::SetWorkspace(RepoSetWorkspaceArgs {
repository: through_cli.target.to_string(),
profile: None,
mode: WorkspaceMode::Ephemeral,
path: None,
}),
&mut cli_out,
)
.unwrap();
assert_eq!(through_tui.stored_policy(), through_cli.stored_policy());
assert_eq!(ui.message.as_deref().unwrap(), one_surface(cli_out));
assert!(
rewrote_a_default.get(),
"no success block named the platform default, so nothing checked \
that the two surfaces render it the same way"
);
}
#[test]
fn every_validation_fixture_is_refused_with_the_same_reason_from_both_surfaces() {
let fixture = Workspaces::new();
let existing_file = fixture.roots.path().join("a-file");
std::fs::write(&existing_file, b"not a directory").unwrap();
let candidates = [
"build/runners".to_owned(),
if cfg!(windows) {
"C:\\".to_owned()
} else {
"/".to_owned()
},
fixture
.context
.paths()
.config_dir()
.to_str()
.unwrap()
.to_owned(),
existing_file.to_str().unwrap().to_owned(),
];
for candidate in candidates {
let mut ui = fixture.host_screen();
edit_path(
&mut ui,
&fixture.context,
Control::HostRunnerRoot,
&candidate,
);
let inline = ui.host_root_notice.clone().expect("an inline answer");
activate(&mut ui, &fixture.context, Control::HostRootSave);
let saved = ui.message.clone().unwrap_or_default();
let refused = cli::host::dispatch(
&fixture.context,
&HostCommand::SetRuntimeRoot(HostSetRuntimeRootArgs {
path: candidate.clone(),
}),
cli::Styling::plain_for_buffer(),
&mut Vec::new(),
)
.expect_err("every fixture in this table is refused");
assert!(
inline.contains(&refused.to_string()),
"{candidate:?}: the inline answer {inline:?} does not carry the command's \
reason {refused}"
);
assert_eq!(saved, format!("error: {refused}"), "{candidate:?}");
assert_eq!(
fixture.stored_host().runner_root_override,
None,
"{candidate:?} must persist nothing"
);
assert_eq!(
ui.host_root.text(),
candidate,
"the draft stays on screen for correction"
);
}
}
#[test]
fn persistent_mode_previews_the_whole_trust_warning_and_the_retained_directories() {
let fixture = Workspaces::new();
let mut ui = fixture.policy_screen();
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
let previewed = rows_paragraph(&ui, 118, false);
for line in cli::workspace::PERSISTENT_TRUST_WARNING {
assert!(previewed.contains(line.trim()), "missing {line:?}");
}
edit_path(
&mut ui,
&fixture.context,
Control::WorkspacePath,
&fixture.root("slots"),
);
activate(&mut ui, &fixture.context, Control::WorkspaceSave);
assert_eq!(
fixture.stored_policy().workspace_policy().kind(),
WorkspaceKind::Persistent
);
let sentinel = std::path::Path::new(&fixture.root("slots")).join("s1");
std::fs::create_dir_all(&sentinel).unwrap();
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
assert_eq!(ui.workspace_mode, WorkspaceKind::Ephemeral);
let leaving = rows_paragraph(&ui, 118, false);
assert!(leaving.contains("Left in place"), "{leaving}");
assert!(
leaving.contains("Nothing is moved or deleted."),
"{leaving}"
);
activate(&mut ui, &fixture.context, Control::WorkspaceSave);
assert_eq!(
fixture.stored_policy().workspace_policy().kind(),
WorkspaceKind::Ephemeral
);
assert!(
sentinel.is_dir(),
"returning to ephemeral must leave every old slot directory on disk"
);
let saved = ui.message.as_deref().unwrap_or_default();
assert!(saved.contains("remains on disk"), "{saved}");
assert!(
saved.contains("No existing directory was moved or deleted."),
"{saved}"
);
}
#[test]
fn reset_clears_the_override_and_nothing_else_on_the_host_row() {
let fixture = Workspaces::new();
let mut ui = fixture.host_screen();
edit_path(
&mut ui,
&fixture.context,
Control::HostRunnerRoot,
&fixture.root("configured"),
);
activate(&mut ui, &fixture.context, Control::HostRootSave);
focus_by_keyboard(&mut ui, Control::HostCapacity);
let _ = ui.key(KeyCode::Right);
activate(&mut ui, &fixture.context, Control::HostSave);
let before = fixture.stored_host();
assert!(before.runner_root_override.is_some());
activate(&mut ui, &fixture.context, Control::HostRootReset);
let after = fixture.stored_host();
assert_eq!(after.runner_root_override, None);
assert_eq!(after.host_capacity(), before.host_capacity());
assert_eq!(after.service_start_mode, before.service_start_mode);
assert_eq!(after.refresh_interval, before.refresh_interval);
assert!(
std::path::Path::new(&fixture.root("configured")).is_dir(),
"a reset must not remove the directory the override named"
);
assert!(
ui.host_root.is_blank(),
"the field follows the stored value"
);
}
#[test]
fn a_blank_draft_points_at_reset_rather_than_at_a_path_parser() {
let fixture = Workspaces::new();
let mut ui = fixture.host_screen();
focus_by_keyboard(&mut ui, Control::HostRunnerRoot);
let _ = ui.key(KeyCode::Enter);
let command = ui.key(KeyCode::Enter).expect("accepting revalidates");
ui.execute(&fixture.context, command);
let blank = ui.host_root_notice.clone().unwrap();
assert!(blank.contains("Reset to platform default"), "{blank}");
activate(&mut ui, &fixture.context, Control::HostRootSave);
assert_eq!(fixture.stored_host().runner_root_override, None);
let message = ui.message.clone().unwrap_or_default();
assert!(message.contains("type an absolute path"), "{message}");
}
#[test]
fn active_and_cleanup_blocked_attempts_are_previewed_and_refuse_the_save() {
for (terminal, expected) in [
(false, "1 active and 0 awaiting"),
(true, "0 active and 1 awaiting"),
] {
let fixture = Workspaces::new();
fixture.attempt(0x51, terminal);
let mut ui = fixture.policy_screen();
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
edit_path(
&mut ui,
&fixture.context,
Control::WorkspacePath,
&fixture.root("slots"),
);
let notice = ui.workspace_notice.clone().expect("a refusal preview");
assert!(notice.starts_with("refused:"), "{notice}");
assert!(notice.contains(expected), "{notice}");
activate(&mut ui, &fixture.context, Control::WorkspaceSave);
assert_eq!(
fixture.stored_policy().workspace_policy().kind(),
WorkspaceKind::Ephemeral,
"a refused change must persist nothing"
);
let message = ui.message.clone().unwrap_or_default();
assert!(message.contains(expected), "{message}");
}
}
#[test]
fn an_uncleaned_ephemeral_attempt_blocks_and_is_shown_on_the_host_screen() {
let fixture = Workspaces::new();
fixture.attempt(0x61, false);
let mut ui = fixture.host_screen();
let text = rows_paragraph(&ui, 118, false);
assert!(
text.contains("Affected attempts: 1 active, 0 awaiting cleanup"),
"{text}"
);
edit_path(
&mut ui,
&fixture.context,
Control::HostRunnerRoot,
&fixture.root("elsewhere"),
);
assert!(
ui.host_root_notice
.as_deref()
.unwrap_or_default()
.starts_with("refused:"),
"{:?}",
ui.host_root_notice
);
activate(&mut ui, &fixture.context, Control::HostRootSave);
assert_eq!(fixture.stored_host().runner_root_override, None);
assert!(
!std::path::Path::new(&fixture.root("elsewhere")).exists(),
"a refusal that never reached the preflight must create nothing"
);
}
#[test]
fn a_path_control_types_moves_deletes_pastes_cancels_accepts_and_scrolls() {
let fixture = Workspaces::new();
let mut ui = fixture.host_screen();
focus_by_keyboard(&mut ui, Control::HostRunnerRoot);
assert!(ui.key(KeyCode::Enter).is_none());
type_path(&mut ui, "C:/rman");
assert_eq!(ui.host_root.text(), "C:/rman");
let _ = ui.key(KeyCode::Backspace);
assert_eq!(ui.host_root.text(), "C:/rma");
let _ = ui.key(KeyCode::Home);
let _ = ui.key(KeyCode::Delete);
assert_eq!(ui.host_root.text(), ":/rma");
let _ = ui.key(KeyCode::Right);
type_path(&mut ui, "!");
assert_eq!(ui.host_root.text(), ":!/rma");
let _ = ui.key(KeyCode::Left);
let _ = ui.key(KeyCode::Delete);
assert_eq!(ui.host_root.text(), ":/rma");
let _ = ui.key(KeyCode::End);
ui.paste("\"D:/pasted\"\r\nsecond line");
assert_eq!(ui.host_root.text(), ":/rmaD:/pasted");
let _ = ui.key(KeyCode::Esc);
assert!(!ui.is_editing());
assert!(ui.host_root.is_blank());
assert!(ui.host_root_notice.is_none());
let long = format!("{}/{}", fixture.root("deep"), "segment".repeat(20));
focus_by_keyboard(&mut ui, Control::HostRunnerRoot);
let _ = ui.key(KeyCode::Enter);
type_path(&mut ui, &long);
let tail = ui.host_root.view(30);
assert!(tail.clipped_left && !tail.clipped_right, "{tail:?}");
assert!(tail.text.ends_with("segment|"), "{tail:?}");
let _ = ui.key(KeyCode::Home);
let head = ui.host_root.view(30);
assert!(!head.clipped_left && head.clipped_right, "{head:?}");
assert!(head.text.starts_with('|'), "{head:?}");
for width in [24usize, 40, 80, 118] {
for row in ui.rows(width, false) {
assert!(
row.text.chars().count() <= width,
"width={width} row={:?}",
row.text
);
}
}
}
#[test]
fn the_focused_path_control_is_the_one_that_is_copied() {
let fixture = Workspaces::new();
let mut ui = fixture.host_screen();
focus_by_keyboard(&mut ui, Control::HostRunnerRoot);
let effective = match &ui.view {
SettingsView::Host(form) => form.runner_root.rendered(),
_ => unreachable!(),
};
assert_eq!(
ui.copy_text(),
Some(effective),
"with no override the copyable value is the effective root"
);
edit_path(
&mut ui,
&fixture.context,
Control::HostRunnerRoot,
&fixture.root("copied"),
);
focus_by_keyboard(&mut ui, Control::HostRunnerRoot);
assert_eq!(ui.copy_text(), Some(fixture.root("copied")));
focus_by_keyboard(&mut ui, Control::HostSave);
assert_eq!(ui.copy_text(), None, "a save action has nothing to copy");
}
#[test]
fn saved_values_survive_a_recreated_context_and_a_reloaded_screen() {
let data = TempDir::new().unwrap();
let roots = TempDir::new().unwrap();
let slots = roots.path().join("slots").to_str().unwrap().to_owned();
let host_root = roots.path().join("host").to_str().unwrap().to_owned();
let target = ScaleTarget::repository("octo/repo").unwrap();
{
let context = Context::resolve(Some(data.path()), &mut Vec::new()).unwrap();
let store = context.store().unwrap();
let host = Host::new(
HostId::from_u128(1),
"local-home",
Os::Linux,
Arch::X64,
nz(4),
chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap(),
)
.unwrap();
store.put_host(&host).unwrap();
store
.insert_policy(&ScalePolicy::new_for_host_label(
PolicyId::from_u128(2),
target.clone(),
7,
host.id,
HostLabel::new("home").unwrap(),
PolicyMode::autoscale(
RoutingLabels::derive(
&HostLabel::new("home").unwrap(),
Os::Linux,
Arch::X64,
),
0,
nz(2),
)
.unwrap(),
CachePolicy::default(),
))
.unwrap();
drop(store);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadHost);
edit_path(&mut ui, &context, Control::HostRunnerRoot, &host_root);
activate(&mut ui, &context, Control::HostRootSave);
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
edit_path(&mut ui, &context, Control::WorkspacePath, &slots);
activate(&mut ui, &context, Control::WorkspaceSave);
}
let restarted = Context::resolve(Some(data.path()), &mut Vec::new()).unwrap();
let mut ui = SettingsUi::default();
ui.execute(&restarted, SettingsCommand::LoadHost);
assert_eq!(ui.host_root.text(), host_root);
let SettingsView::Host(form) = &ui.view else {
unreachable!()
};
assert_eq!(form.runner_root.effective_text(), Some(host_root.as_str()));
assert_eq!(
form.runner_root.source(),
cli::workspace::RootSource::Configured
);
ui.execute(&restarted, SettingsCommand::LoadPolicy(target.to_string()));
assert_eq!(ui.workspace_mode, WorkspaceKind::Persistent);
assert_eq!(ui.workspace_path.text(), slots);
let SettingsView::Policy(form) = &ui.view else {
unreachable!()
};
assert_eq!(form.workspace.effective_root(), Some(slots.as_str()));
assert_eq!(form.workspace.root_source_badge(), "repository-specific");
}
#[test]
fn organization_settings_render_ephemeral_and_say_why_it_cannot_change() {
let fixture = Workspaces::new();
let store = fixture.context.store().unwrap();
let host = cli::host::local_host(&store).unwrap().unwrap();
let organization = ScaleTarget::organization("octo-org").unwrap();
store
.insert_policy(&ScalePolicy::new_for_host_label(
PolicyId::from_u128(90),
organization.clone(),
9,
host.id,
HostLabel::new("home").unwrap(),
PolicyMode::MonitorOnly,
CachePolicy::default(),
))
.unwrap();
drop(store);
let mut ui = SettingsUi::default();
ui.execute(
&fixture.context,
SettingsCommand::LoadProfile {
target: organization.to_string(),
profile: "default".into(),
},
);
let SettingsView::Policy(form) = &ui.view else {
panic!("{:?}", ui.message)
};
assert!(form.is_organization());
assert_eq!(form.workspace.kind(), WorkspaceKind::Ephemeral);
assert_eq!(form.workspace_action_count(WorkspaceKind::Persistent), 0);
for control in [
Control::WorkspaceMode,
Control::WorkspacePath,
Control::WorkspaceSave,
Control::PolicyExecutionMode,
Control::PolicyExecutionSave,
Control::PolicyDrain,
Control::PolicyRemove,
Control::PolicyAddProfile,
] {
assert!(
!ui.controls().contains(&control),
"{control:?} must not exist for an organization"
);
}
let text = rows_paragraph(&ui, 118, false);
assert!(text.contains(ORGANIZATION_WORKSPACE_MODE), "{text}");
assert!(text.contains("cross a repository boundary"), "{text}");
assert!(text.contains("repo set-workspace OWNER/REPO"), "{text}");
for line in cli::workspace::PERSISTENT_TRUST_WARNING {
assert!(
!text.contains(line.trim()),
"an organization is never offered persistence, so it is never warned about it"
);
}
}
#[test]
fn selector_and_trust_warnings_are_mouse_copyable() {
let (_dir, context, target) = fixture(false);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
for warning in [STATIC_SELECTOR_WARNING, FORK_TRUST_WARNING] {
let row = ui
.rows(160, false)
.iter()
.position(|line| line.text == warning)
.expect("warning row");
assert_eq!(
ui.click(row as u16, 160, false),
Some(SettingsCommand::Copy(warning.to_owned()))
);
}
}
#[test]
fn clicking_another_control_ends_the_edit_and_keeps_what_was_typed() {
let fixture = Workspaces::new();
let width = 100usize;
let row_of = |ui: &SettingsUi, control: Control| {
let index = ui
.controls()
.iter()
.position(|candidate| *candidate == control)
.expect("the control is on this screen");
u16::try_from(
ui.rows(width, false)
.iter()
.position(|row| row.control == Some(index))
.expect("the control is drawn"),
)
.unwrap()
};
let mut ui = fixture.policy_screen();
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
focus_by_keyboard(&mut ui, Control::WorkspacePath);
assert!(ui.key(KeyCode::Enter).is_none());
type_path(&mut ui, &fixture.root("slots"));
let mode_row = row_of(&ui, Control::WorkspaceMode);
let _ = ui.click(mode_row, width, false);
assert_eq!(ui.workspace_mode, WorkspaceKind::Ephemeral);
assert!(
!ui.is_editing(),
"a field no frame draws may not keep the keyboard"
);
let mut ui = fixture.host_screen();
focus_by_keyboard(&mut ui, Control::HostRunnerRoot);
assert!(ui.key(KeyCode::Enter).is_none());
type_path(&mut ui, &fixture.root("clicked"));
let save_row = row_of(&ui, Control::HostRootSave);
let command = ui.click(save_row, width, false).expect("Save dispatches");
assert!(!ui.is_editing());
ui.execute(&fixture.context, command);
assert_eq!(
fixture
.stored_host()
.runner_root_override
.map(|root| root.as_str().to_owned()),
Some(fixture.root("clicked")),
"{:?}",
ui.message
);
}
#[test]
fn a_multi_line_save_message_is_drawn_one_line_per_row() {
let fixture = Workspaces::new();
let mut ui = fixture.host_screen();
edit_path(
&mut ui,
&fixture.context,
Control::HostRunnerRoot,
&fixture.root("drawn"),
);
activate(&mut ui, &fixture.context, Control::HostRootSave);
let message = ui.message.clone().expect("the success block");
assert!(message.lines().count() > 1, "{message}");
let width = 100;
let content_width = super::content_width(width);
let height = ui.rows(content_width, false).len()
+ message
.lines()
.flat_map(|line| wrap(line, content_width))
.count()
+ 2; let height = u16::try_from(height).expect("the test viewport fits in a u16");
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| render(frame, frame.area(), &ui, false))
.unwrap();
let drawn = super::super::buffer_text(terminal.backend().buffer());
for line in message.lines() {
let head = line.split_whitespace().next().unwrap_or_default();
assert!(
drawn
.lines()
.any(|row| row.trim_start_matches('│').trim_start().starts_with(head)),
"{head:?} does not begin a row of its own:\n{drawn}"
);
}
}
#[test]
fn a_constrained_terminal_keeps_the_value_the_error_and_the_save_action() {
let fixture = Workspaces::new();
let mut ui = fixture.host_screen();
edit_path(
&mut ui,
&fixture.context,
Control::HostRunnerRoot,
"build/runners",
);
let compact = rows_text(&ui, 56, true);
assert!(compact.contains("Runner root:"), "{compact}");
assert!(compact.contains("Override:"), "{compact}");
assert!(compact.contains("error:"), "{compact}");
assert!(compact.contains("Save runner root"), "{compact}");
assert!(compact.contains("Save host settings"), "{compact}");
assert!(
!compact.contains("30-second floor"),
"secondary explanation is what a compact frame drops: {compact}"
);
let mut ui = fixture.policy_screen();
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
let compact = rows_paragraph(&ui, 56, true);
assert!(compact.contains("Workspace mode:"), "{compact}");
assert!(compact.contains("Persistent root:"), "{compact}");
assert!(compact.contains("Save workspace"), "{compact}");
assert!(
compact.contains(cli::workspace::PERSISTENT_TRUST_WARNING[0].trim()),
"the headline warning survives a compact frame: {compact}"
);
}
#[test]
fn no_settings_row_names_anything_inside_a_workspace() {
let fixture = Workspaces::new();
let mut ui = fixture.policy_screen();
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
edit_path(
&mut ui,
&fixture.context,
Control::WorkspacePath,
&fixture.root("slots"),
);
activate(&mut ui, &fixture.context, Control::WorkspaceSave);
let slot = std::path::Path::new(&fixture.root("slots")).join("s1");
std::fs::create_dir_all(slot.join("_work")).unwrap();
std::fs::write(slot.join("_work").join("checkout-secret.txt"), b"x").unwrap();
std::fs::write(slot.join("build-output-42"), b"x").unwrap();
fixture.attempt(0x71, false);
let text = rows_paragraph(&fixture.policy_screen(), 118, false);
for name in ["checkout-secret.txt", "build-output-42"] {
assert!(!text.contains(name), "{name} was enumerated: {text}");
}
assert!(text.contains("Slot leases: none"), "{text}");
}
#[test]
fn no_secret_reaches_a_settings_frame() {
let fixture = Workspaces::new();
let mut ui = fixture.host_screen();
focus_by_keyboard(&mut ui, Control::HostRunnerRoot);
let _ = ui.key(KeyCode::Enter);
ui.paste("ghu_this_must_not_escape");
let command = ui.key(KeyCode::Enter).unwrap();
ui.execute(&fixture.context, command);
activate(&mut ui, &fixture.context, Control::HostRootSave);
assert_eq!(fixture.stored_host().runner_root_override, None);
let mut terminal = Terminal::new(TestBackend::new(100, 24)).unwrap();
terminal
.draw(|frame| render(frame, frame.area(), &ui, false))
.unwrap();
let drawn = super::super::buffer_text(terminal.backend().buffer());
assert!(drawn.contains("Runner root:"), "{drawn}");
assert!(!drawn.contains("Authorization"), "{drawn}");
assert!(!drawn.contains("jitconfig"), "{drawn}");
assert!(!drawn.contains("token"), "{drawn}");
}
#[test]
fn snapshot_every_required_settings_state() {
insta::assert_snapshot!(settings_matrix(), @r#"
--- host/default ---
Host settings
Current capacity: 4
Currently in use: 0
Capacity: 4 [-/+ or click]
Service start: boot [toggle]
Refresh interval: 60s [-/+ 30s]
Projected requests/hour: 360
Maximum repository targets: about 6
30-second floor; over-budget changes are refused.
Save host settings [Enter/click]
Runner root: <DEFAULT> (platform-default)
Affected attempts: 0 active, 0 awaiting cleanup
Override: (none - platform default) [Enter to edit]
Reset to platform default [Enter/click]
Save runner root [Enter/click]
Focused form actions: 4/5 capacity, 3/5 runner root
--- host/configured ---
Host settings
Current capacity: 4
Currently in use: 0
Capacity: 4 [-/+ or click]
Service start: boot [toggle]
Refresh interval: 60s [-/+ 30s]
Projected requests/hour: 360
Maximum repository targets: about 6
30-second floor; over-budget changes are refused.
Save host settings [Enter/click]
Runner root: <CONFIGURED> (configured)
Affected attempts: 0 active, 0 awaiting cleanup
Override: <CONFIGURED> [Enter to edit]
Reset to platform default [Enter/click]
Save runner root [Enter/click]
Focused form actions: 4/5 capacity, 3/5 runner root
--- host/invalid ---
Host settings
Current capacity: 4
Currently in use: 0
Capacity: 4 [-/+ or click]
Service start: boot [toggle]
Refresh interval: 60s [-/+ 30s]
Projected requests/hour: 360
Maximum repository targets: about 6
30-second floor; over-budget changes are refused.
Save host settings [Enter/click]
Runner root: <DEFAULT> (platform-default)
Affected attempts: 0 active, 0 awaiting cleanup
Override: build/runners [Enter to edit]
error: "build/runners" cannot be used as the host runner root: a configured path must be absolute;
"build/runners" is relative, and what it resolves to depends on the process working directory
(runner-manager host set-runtime-root --path <PATH>)
Reset to platform default [Enter/click]
Save runner root [Enter/click]
Focused form actions: 4/5 capacity, 3/5 runner root
--- host/active-refusal ---
Host settings
Current capacity: 4
Currently in use: 1
Capacity: 4 [-/+ or click]
Service start: boot [toggle]
Refresh interval: 60s [-/+ 30s]
Projected requests/hour: 360
Maximum repository targets: about 6
30-second floor; over-budget changes are refused.
Save host settings [Enter/click]
Runner root: <DEFAULT> (platform-default)
Affected attempts: 1 active, 0 awaiting cleanup
Override: <CONFIGURED> [Enter to edit]
refused: the host runner root cannot change while attempts still own it: 1 active and 0 awaiting
cleanup. Nothing was changed.
Reset to platform default [Enter/click]
Save runner root [Enter/click]
Focused form actions: 4/5 capacity, 3/5 runner root
--- host/compact ---
Current capacity: 4
Capacity: 4 [-/+ or click]
Save host settings [Enter/click]
Runner root: <DEFAULT> (platform-default)
Override: build/runners [Enter to edit]
error: "build/runners" cannot be used as the host runner
root: a configured path must be absolute;
"build/runners" is relative, and what it resolves to
depends on the process working directory (runner-manager
host set-runtime-root --path <PATH>)
Save runner root [Enter/click]
--- repository/ephemeral ---
Target: octo/repo
Profile: default
Mode: Autoscale
Local host: local-home
Current max_capacity: 2
runs-on: rm-home-linux-x64 [click to copy]
Static runs-on labels required; selector optional. Overlapping profiles start no runner; matrix
expressions are not resolved.
warning: fork and untrusted pull-request workflows must not run on a personal host until you
explicitly accept that trust boundary.
Scaling enabled: false [toggle]
max_capacity: 2 [-/+; setting promotes monitor-only]
Cache policy: RetainRunnerPackage [toggle]
Preview: no runner is terminated immediately.
Confirm policy [Enter/click]
Host label: rm-home-linux-x64 (fixed: it is this machine's routing identity)
Extra labels: (none - this policy answers its host label only) [Enter to edit]
Comma-separated. Saving makes the stored set equal this line.
Save labels [Enter/click]
Execution: native
Execution mode: native [toggle]
Save execution [Enter/click]
Native provider: ready
OCI provider: not installed
Hyper-V container provider: unsupported
Virtual machine provider: not installed
Provider readiness and the pinned image are checked again before JIT registration.
Drain selected profile [Enter/click]
Remove selected profile [Enter twice/click twice]
Workspace: ephemeral (platform-default)
Workspace root: <DEFAULT>
Slot leases: none
Affected attempts: 0 active, 0 awaiting cleanup
Workspace mode: ephemeral [toggle]
Save workspace [Enter/click]
Add sibling profile [Enter/click]
Focused form actions: 4/5 scaling, 2/5 labels, 2/5 workspace
--- repository/isolated ---
Target: octo/repo
Profile: default
Mode: Autoscale
Local host: local-home
Current max_capacity: 2
runs-on: rm-home-linux-x64 [click to copy]
Static runs-on labels required; selector optional. Overlapping profiles start no runner; matrix
expressions are not resolved.
warning: fork and untrusted pull-request workflows must not run on a personal host until you
explicitly accept that trust boundary.
Scaling enabled: false [toggle]
max_capacity: 2 [-/+; setting promotes monitor-only]
Cache policy: RetainRunnerPackage [toggle]
Preview: no runner is terminated immediately.
Confirm policy [Enter/click]
Host label: rm-home-linux-x64 (fixed: it is this machine's routing identity)
Extra labels: (none - this policy answers its host label only) [Enter to edit]
Comma-separated. Saving makes the stored set equal this line.
Save labels [Enter/click]
Execution: native
Execution mode: isolated [toggle]
Backend: windows-hyper-v-container [cycle]
Pinned image: <56:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd [Enter to edit]
CPU: 2200m [-/+]
Memory: 3072MiB [-/+]
Disk: 10240MiB [-/+]
Selected provider: unsupported; remedy: use Windows 11 Pro or Enterprise with Docker Desktop in
Windows-container mode, or Windows Server Standard or Datacenter with Moby or Mirantis Container
Runtime
Support: preview: native acceptance is pending for Windows 11 Pro/Enterprise with Docker Desktop and
Windows Server Standard/Datacenter with a supported server runtime, including job, restart, reboot,
and resource-exhaustion cases
Unsupported workflows: desktop, devices, container_actions, service_containers
Save execution [Enter/click]
Native provider: ready
OCI provider: not installed
Hyper-V container provider: unsupported
Virtual machine provider: not installed
Provider readiness and the pinned image are checked again before JIT registration.
Drain selected profile [Enter/click]
Remove selected profile [Enter twice/click twice]
Workspace: ephemeral (platform-default)
Workspace root: <DEFAULT>
Slot leases: none
Affected attempts: 0 active, 0 awaiting cleanup
Workspace mode: ephemeral [toggle]
Save workspace [Enter/click]
Add sibling profile [Enter/click]
Focused form actions: 4/5 scaling, 2/5 labels, 2/5 workspace
--- repository/create-isolated ---
Target: octo/repo
Profile: default
Mode: Autoscale
Local host: local-home
Current max_capacity: 2
runs-on: rm-home-linux-x64 [click to copy]
Static runs-on labels required; selector optional. Overlapping profiles start no runner; matrix
expressions are not resolved.
warning: fork and untrusted pull-request workflows must not run on a personal host until you
explicitly accept that trust boundary.
Scaling enabled: false [toggle]
max_capacity: 2 [-/+; setting promotes monitor-only]
Cache policy: RetainRunnerPackage [toggle]
Preview: no runner is terminated immediately.
Confirm policy [Enter/click]
Host label: rm-home-linux-x64 (fixed: it is this machine's routing identity)
Extra labels: (none - this policy answers its host label only) [Enter to edit]
Comma-separated. Saving makes the stored set equal this line.
Save labels [Enter/click]
Execution: native
Execution mode: isolated [toggle]
Backend: windows-hyper-v-container [cycle]
Pinned image: <56:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd [Enter to edit]
CPU: 2200m [-/+]
Memory: 3072MiB [-/+]
Disk: 10240MiB [-/+]
Selected provider: unsupported; remedy: use Windows 11 Pro or Enterprise with Docker Desktop in
Windows-container mode, or Windows Server Standard or Datacenter with Moby or Mirantis Container
Runtime
Support: preview: native acceptance is pending for Windows 11 Pro/Enterprise with Docker Desktop and
Windows Server Standard/Datacenter with a supported server runtime, including job, restart, reboot,
and resource-exhaustion cases
Unsupported workflows: desktop, devices, container_actions, service_containers
Save execution [Enter/click]
Native provider: ready
OCI provider: not installed
Hyper-V container provider: unsupported
Virtual machine provider: not installed
Provider readiness and the pinned image are checked again before JIT registration.
Drain selected profile [Enter/click]
Remove selected profile [Enter twice/click twice]
Workspace: ephemeral (platform-default)
Workspace root: <DEFAULT>
Slot leases: none
Affected attempts: 0 active, 0 awaiting cleanup
Workspace mode: ephemeral [toggle]
Save workspace [Enter/click]
Add sibling profile [Enter/click]
New profile name: gpu-build [Enter to edit]
Selector preview: rm-home-linux-x64-gpu-build
Capacity: 3 [-/+]
Execution mode: isolated [toggle]
Backend: oci [cycle]
Pinned image: <56:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee [Enter to edit]
CPU: 4000m [-/+]
Memory: 8192MiB [-/+]
Disk: 20480MiB [-/+]
Selected provider: not installed; remedy: install and configure an OCI execution provider
Create profile [Enter/click]
Cancel profile creation [Enter/click]
Focused form actions: 4/5 scaling, 2/5 labels, 2/5 workspace
--- repository/create-isolated-compact ---
Target: octo/repo
Profile: default
Static runs-on labels required; selector optional.
Overlapping profiles start no runner; matrix expressions
are not resolved.
warning: fork and untrusted pull-request workflows must
not run on a personal host until you explicitly accept
that trust boundary.
Scaling enabled: false [toggle]
max_capacity: 2 [-/+; setting promotes monitor-only]
Confirm policy [Enter/click]
Host label: rm-home-linux-x64 (fixed: it is this
machine's routing identity)
Extra labels: (none - this policy answers its host label
only) [Enter to edit]
Save labels [Enter/click]
Execution: native
Execution mode: isolated [toggle]
Backend: windows-hyper-v-container [cycle]
Pinned image: <ddddddddddddddddddddddd [Enter to edit]
CPU: 2200m [-/+]
Memory: 3072MiB [-/+]
Disk: 10240MiB [-/+]
Selected provider: unsupported; remedy: use Windows 11
Pro or Enterprise with Docker Desktop in
Windows-container mode, or Windows Server Standard or
Datacenter with Moby or Mirantis Container Runtime
Support: preview: native acceptance is pending for
Windows 11 Pro/Enterprise with Docker Desktop and
Windows Server Standard/Datacenter with a supported
server runtime, including job, restart, reboot, and
resource-exhaustion cases
Unsupported workflows: desktop, devices,
container_actions, service_containers
Save execution [Enter/click]
Drain selected profile [Enter/click]
Remove selected profile [Enter twice/click twice]
Workspace: ephemeral (platform-default)
Workspace root: <DEFAULT>
Workspace mode: ephemeral [toggle]
Save workspace [Enter/click]
Add sibling profile [Enter/click]
New profile name: gpu-build [Enter to edit]
Selector preview: rm-home-linux-x64-gpu-build
Capacity: 3 [-/+]
Execution mode: isolated [toggle]
Backend: oci [cycle]
Pinned image: <eeeeeeeeeeeeeeeeeeeeeee [Enter to edit]
CPU: 4000m [-/+]
Memory: 8192MiB [-/+]
Disk: 20480MiB [-/+]
Selected provider: not installed; remedy: install and
configure an OCI execution provider
Create profile [Enter/click]
--- repository/persistent-warning ---
Target: octo/repo
Profile: default
Mode: Autoscale
Local host: local-home
Current max_capacity: 2
runs-on: rm-home-linux-x64 [click to copy]
Static runs-on labels required; selector optional. Overlapping profiles start no runner; matrix
expressions are not resolved.
warning: fork and untrusted pull-request workflows must not run on a personal host until you
explicitly accept that trust boundary.
Scaling enabled: false [toggle]
max_capacity: 2 [-/+; setting promotes monitor-only]
Cache policy: RetainRunnerPackage [toggle]
Preview: no runner is terminated immediately.
Confirm policy [Enter/click]
Host label: rm-home-linux-x64 (fixed: it is this machine's routing identity)
Extra labels: (none - this policy answers its host label only) [Enter to edit]
Comma-separated. Saving makes the stored set equal this line.
Save labels [Enter/click]
Execution: native
Execution mode: native [toggle]
Save execution [Enter/click]
Native provider: ready
OCI provider: not installed
Hyper-V container provider: unsupported
Virtual machine provider: not installed
Provider readiness and the pinned image are checked again before JIT registration.
Drain selected profile [Enter/click]
Remove selected profile [Enter twice/click twice]
Workspace: ephemeral (platform-default)
Workspace root: <DEFAULT>
Slot leases: none
Affected attempts: 0 active, 0 awaiting cleanup
Workspace mode: persistent [toggle]
Persistent root: <SLOTS> [Enter to edit]
ready: slots s1, s2, ... would be created under <SLOTS>.
warning: a persistent workspace is a trusted-workflow optimization, not isolation.
- files under _work are an input to later jobs on the same slot;
- executable and generated content can cross branch and job boundaries;
- do not enable it for untrusted fork or pull-request workflows;
- changing or disabling persistence does not delete old directories;
- `actions/checkout` still cleans the workspace, including Git-ignored files,
unless the workflow sets `clean: false`.
Save workspace [Enter/click]
Add sibling profile [Enter/click]
Focused form actions: 4/5 scaling, 2/5 labels, 3/5 workspace
--- repository/cleanup-blocked ---
Target: octo/repo
Profile: default
Mode: Autoscale
Local host: local-home
Current max_capacity: 2
runs-on: rm-home-linux-x64 [click to copy]
Static runs-on labels required; selector optional. Overlapping profiles start no runner; matrix
expressions are not resolved.
warning: fork and untrusted pull-request workflows must not run on a personal host until you
explicitly accept that trust boundary.
Scaling enabled: false [toggle]
max_capacity: 2 [-/+; setting promotes monitor-only]
Cache policy: RetainRunnerPackage [toggle]
Preview: no runner is terminated immediately.
Confirm policy [Enter/click]
Host label: rm-home-linux-x64 (fixed: it is this machine's routing identity)
Extra labels: (none - this policy answers its host label only) [Enter to edit]
Comma-separated. Saving makes the stored set equal this line.
Save labels [Enter/click]
Execution: native
Execution mode: native [toggle]
Save execution [Enter/click]
Native provider: ready
OCI provider: not installed
Hyper-V container provider: unsupported
Virtual machine provider: not installed
Provider readiness and the pinned image are checked again before JIT registration.
Drain selected profile [Enter/click]
Remove selected profile [Enter twice/click twice]
Workspace: persistent (repository-specific)
Workspace root: <SLOTS>
Slot leases: s1 cleanup-blocked
Affected attempts: 0 active, 1 awaiting cleanup
Workspace mode: persistent [toggle]
Persistent root: <ELSEWHERE> [Enter to edit]
refused: the workspace setting for octo/repo cannot change while attempts still own it: 0 active and
1 awaiting cleanup. Nothing was changed.
warning: a persistent workspace is a trusted-workflow optimization, not isolation.
- files under _work are an input to later jobs on the same slot;
- executable and generated content can cross branch and job boundaries;
- do not enable it for untrusted fork or pull-request workflows;
- changing or disabling persistence does not delete old directories;
- `actions/checkout` still cleans the workspace, including Git-ignored files,
unless the workflow sets `clean: false`.
Left in place: every slot under <SLOTS> remains on disk, including its _work directory. Nothing is
moved or deleted.
Save workspace [Enter/click]
Add sibling profile [Enter/click]
Focused form actions: 4/5 scaling, 2/5 labels, 3/5 workspace
"#);
}
fn settings_matrix() -> String {
let mut sections: Vec<(&str, String)> = Vec::new();
let default = Workspaces::new();
sections.push((
"host/default",
stable_rows(&default.host_screen(), 100, false, &default),
));
let configured = Workspaces::new();
let mut ui = configured.host_screen();
edit_path(
&mut ui,
&configured.context,
Control::HostRunnerRoot,
&configured.root("configured"),
);
activate(&mut ui, &configured.context, Control::HostRootSave);
sections.push((
"host/configured",
stable_rows(&configured.host_screen(), 100, false, &configured),
));
let invalid = Workspaces::new();
let mut ui = invalid.host_screen();
edit_path(
&mut ui,
&invalid.context,
Control::HostRunnerRoot,
"build/runners",
);
sections.push(("host/invalid", stable_rows(&ui, 100, false, &invalid)));
let active = Workspaces::new();
active.attempt(0x81, false);
let mut ui = active.host_screen();
edit_path(
&mut ui,
&active.context,
Control::HostRunnerRoot,
&active.root("configured"),
);
sections.push(("host/active-refusal", stable_rows(&ui, 100, false, &active)));
let mut ui = invalid.host_screen();
edit_path(
&mut ui,
&invalid.context,
Control::HostRunnerRoot,
"build/runners",
);
sections.push(("host/compact", stable_rows(&ui, 56, true, &invalid)));
let repository = Workspaces::new();
sections.push((
"repository/ephemeral",
stable_rows(&repository.policy_screen(), 100, false, &repository),
));
let mut isolated = repository.policy_screen();
isolated.execution_mode = ExecutionMode::Isolated;
isolated.execution_backend = BackendMode::WindowsHyperVContainer;
isolated.execution_image.reset_to(&format!(
"registry.example/runner@sha256:{}",
"d".repeat(64)
));
isolated.execution_cpu = 2200;
isolated.execution_memory = 3072;
isolated.execution_disk = 10240;
sections.push((
"repository/isolated",
stable_rows(&isolated, 100, false, &repository),
));
isolated.profile_create_open = true;
isolated.create_name.reset_to("gpu-build");
isolated.create_capacity = 3;
isolated.create_execution_mode = ExecutionMode::Isolated;
isolated.create_execution_backend = BackendMode::Oci;
isolated.create_execution_image.reset_to(&format!(
"registry.example/gpu-runner@sha256:{}",
"e".repeat(64)
));
isolated.create_execution_cpu = 4000;
isolated.create_execution_memory = 8192;
isolated.create_execution_disk = 20480;
sections.push((
"repository/create-isolated",
stable_rows(&isolated, 100, false, &repository),
));
sections.push((
"repository/create-isolated-compact",
stable_rows(&isolated, 56, true, &repository),
));
let mut ui = repository.policy_screen();
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
edit_path(
&mut ui,
&repository.context,
Control::WorkspacePath,
&repository.root("slots"),
);
sections.push((
"repository/persistent-warning",
stable_rows(&ui, 100, false, &repository),
));
let blocked = Workspaces::new();
let mut ui = blocked.policy_screen();
focus_by_keyboard(&mut ui, Control::WorkspaceMode);
let _ = ui.key(KeyCode::Right);
edit_path(
&mut ui,
&blocked.context,
Control::WorkspacePath,
&blocked.root("slots"),
);
activate(&mut ui, &blocked.context, Control::WorkspaceSave);
blocked.attempt_in(0x91, true, AttemptWorkspace::persistent_slot(nz(1)));
let mut ui = blocked.policy_screen();
edit_path(
&mut ui,
&blocked.context,
Control::WorkspacePath,
&blocked.root("elsewhere"),
);
sections.push((
"repository/cleanup-blocked",
stable_rows(&ui, 100, false, &blocked),
));
sections
.into_iter()
.map(|(name, body)| format!("--- {name} ---\n{body}"))
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn the_label_field_stores_exactly_what_was_typed() {
let (_dir, context, target) = fixture(false);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
assert_eq!(field_of(&ui, Control::PolicyLabels).text(), "");
let drawn = rows_paragraph(&ui, 100, false);
assert!(
drawn.contains("Host label: rm-home-linux-x64"),
"the immovable label is shown: {drawn}"
);
edit_labels(&mut ui, &context, "self-hosted, macos");
assert_eq!(
stored_labels(&context, &target),
(
"rm-home-linux-x64".to_owned(),
vec!["macos".to_owned(), "self-hosted".to_owned()]
),
"both labels are stored, sorted, beside an untouched host label"
);
edit_labels(&mut ui, &context, "macos");
assert_eq!(
stored_labels(&context, &target),
("rm-home-linux-x64".to_owned(), vec!["macos".to_owned()]),
"a label left out of the line is removed"
);
let drawn = rows_paragraph(&ui, 100, false);
assert!(
drawn.contains("race this product cannot arbitrate"),
"the shared warning is shown beside the field: {drawn}"
);
}
#[test]
fn typing_the_host_label_into_the_field_changes_nothing() {
let (_dir, context, target) = fixture(false);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
edit_labels(&mut ui, &context, "rm-home-linux-x64, macos");
assert_eq!(
stored_labels(&context, &target),
("rm-home-linux-x64".to_owned(), vec!["macos".to_owned()]),
"the host label is not duplicated into the optional set"
);
assert!(
!ui.message.as_deref().unwrap_or_default().contains("error"),
"and it is not an error to have typed it: {:?}",
ui.message
);
}
#[test]
fn a_refused_label_keeps_the_draft_and_changes_nothing() {
let (_dir, context, target) = fixture(false);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
edit_labels(&mut ui, &context, "macos");
let too_long = "x".repeat(Label::MAX_LEN + 1);
let typed = format!("macos, {too_long}");
edit_labels(&mut ui, &context, &typed);
assert!(
ui.message
.as_deref()
.unwrap_or_default()
.starts_with("error"),
"the refusal is reported: {:?}",
ui.message
);
assert_eq!(
field_of(&ui, Control::PolicyLabels).text(),
typed,
"the refused draft is still there to be corrected"
);
assert_eq!(
stored_labels(&context, &target),
("rm-home-linux-x64".to_owned(), vec!["macos".to_owned()]),
"and nothing was stored -- not even the half of the line that parsed"
);
}
#[test]
fn opening_the_screen_and_saving_changes_nothing() {
let (_dir, context, target) = fixture(false);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
edit_labels(&mut ui, &context, "my label, self-hosted");
let before = stored_labels(&context, &target);
assert_eq!(before.1, ["my label", "self-hosted"]);
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
assert_eq!(
field_of(&ui, Control::PolicyLabels).text(),
"my label, self-hosted"
);
activate(&mut ui, &context, Control::PolicyLabelsSave);
assert_eq!(
stored_labels(&context, &target),
before,
"a save with no edit must not rewrite the set"
);
assert_eq!(
ui.message.as_deref(),
Some("No label changed; octo/repo already had them that way.")
);
}
#[test]
fn monitor_only_has_no_label_control() {
let (_dir, context, target) = fixture(true);
let mut ui = SettingsUi::default();
ui.execute(&context, SettingsCommand::LoadPolicy(target.to_string()));
let controls = ui.controls();
assert!(
!controls.contains(&Control::PolicyLabels)
&& !controls.contains(&Control::PolicyLabelsSave),
"no label control on a monitor-only policy: {controls:?}"
);
let drawn = rows_paragraph(&ui, 100, false);
assert!(
drawn.contains("none until this policy is promoted"),
"and the screen says why: {drawn}"
);
}
}