use super::super::*;
impl App {
pub(super) fn confirm_setting(&mut self) -> Effect {
let Some(settings) = self.settings() else {
return Effect::None;
};
let opens_editor = settings.pending.is_none()
&& matches!(
settings.cursor(),
Some(SettingsRow::Scalar(
SettingField::Socket | SettingField::MaxCronSleep
))
);
if !opens_editor && !settings.is_armed() {
return Effect::None;
}
let Some(authority) = self.authorize_write() else {
return Effect::None;
};
let Some(settings) = self.settings_mut() else {
return Effect::None;
};
if opens_editor {
let Some(SettingsRow::Scalar(field)) = settings.cursor() else {
return Effect::None;
};
let buffer = settings.text_seed(field).to_string();
settings.pending = Some(Pending::Typing { field, buffer });
self.mode = InputMode::Text;
return Effect::None;
}
let ticket = self.take_write_ticket();
let Some(settings) = self.settings_mut() else {
return Effect::None;
};
match settings.pending.take() {
Some(Pending::Armed { edit, text, .. }) => {
settings.pending = Some(Pending::Sent { text, ticket });
Effect::WriteSetting {
edit,
ticket,
authority,
}
}
Some(Pending::DogArmed { edit, text, .. }) => {
settings.pending = Some(Pending::Sent { text, ticket });
Effect::WriteDog {
edit,
ticket,
authority,
}
}
other => {
settings.pending = other;
Effect::None
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::super::*;
use super::*;
use crate::commands::settings::ScalarView;
use crate::lookout::view::fixtures;
#[test]
fn the_daemon_confirm_names_both_layers_lookout_cannot_see() {
let mut app = fixtures::app_in_settings_with_control();
let _ = app.update(Msg::Key(KeyPress::Cycle));
let text = app.settings().unwrap().pending().unwrap().text.to_string();
assert!(text.contains("shep daemon reload"), "got: {text}");
assert!(text.contains("SHEP_LOG_LEVEL"), "got: {text}");
assert!(text.contains("--log-level"), "got: {text}");
}
#[test]
fn the_whistle_confirm_names_a_whistle_restart_and_not_a_reload() {
let mut app = fixtures::app_in_settings_on(SettingField::AllowControl);
let _ = app.update(Msg::Key(KeyPress::Cycle));
let text = app.settings().unwrap().pending().unwrap().text.to_string();
assert!(text.contains("shep whistle restarted"), "got: {text}");
assert!(
!text.contains("daemon reload"),
"a whistle key needs no reload: {text}"
);
}
#[test]
fn the_style_confirm_promises_nothing_beyond_the_next_command() {
let mut app = fixtures::app_in_settings_on(SettingField::StyleLevel);
let _ = app.update(Msg::Key(KeyPress::Cycle));
let text = app.settings().unwrap().pending().unwrap().text.to_string();
assert!(text.contains("the next command reads it"), "got: {text}");
}
#[test]
fn a_shadowed_style_confirm_names_the_layer_that_keeps_winning() {
for (source, layer) in [
(StyleSource::Env, "$SHEP_STYLE"),
(StyleSource::Flag, "--style"),
] {
let mut app = fixtures::app_in_settings_with_shadowed_style(source);
let _ = app.update(Msg::Key(KeyPress::Cycle));
let text = app.settings().unwrap().pending().unwrap().text.to_string();
assert!(text.contains(layer), "{source} must name itself: {text}");
assert!(
text.contains("keeps winning"),
"{source} must say what it does: {text}"
);
assert!(
!text.contains("the next command reads it"),
"{source}: the next command reads {source}, not the file: {text}"
);
}
}
#[test]
fn enter_sends_the_armed_edit() {
let mut app = fixtures::app_in_settings_with_control();
let _ = app.update(Msg::Key(KeyPress::Cycle));
let effect = app.update(Msg::Key(KeyPress::Confirm));
assert!(matches!(
effect,
Effect::WriteSetting {
edit: SettingEdit::Set {
field: SettingField::LogLevel,
..
},
..
}
));
assert!(app.settings().unwrap().pending().unwrap().sent);
}
#[test]
fn a_written_edit_updates_the_row_and_its_source() {
let mut app = fixtures::app_in_settings_with_control();
let _ = app.update(Msg::Key(KeyPress::Cycle));
let Effect::WriteSetting { edit, ticket, .. } = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send");
};
let SettingEdit::Set {
value: candidate, ..
} = edit.clone()
else {
panic!("cycling only ever arms Set");
};
let effect = app.update(Msg::SettingWritten {
edit,
ticket,
result: Ok(()),
});
assert_eq!(
effect,
Effect::LoadSettings,
"a landed write re-reads rather than hand-folding the row"
);
assert!(app.settings().unwrap().pending().is_none());
let mut updated = fixtures::settings_snapshot();
updated.log_level = ScalarView {
value: candidate,
source: StyleSource::Config,
};
let _ = app.update(Msg::Settings {
result: Ok(updated.clone()),
});
assert_eq!(app.settings().unwrap().snapshot(), &updated);
}
#[test]
fn an_unset_write_returns_the_row_to_the_default() {
let mut app = fixtures::app_in_settings_on(SettingField::MaxCronSleep);
let _ = app.update(Msg::Key(KeyPress::Confirm));
for _ in 0..8 {
let _ = app.update(Msg::Key(KeyPress::TextBackspace));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let Effect::WriteSetting { edit, ticket, .. } = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send");
};
assert!(matches!(
edit,
SettingEdit::Unset {
field: SettingField::MaxCronSleep
}
));
let effect = app.update(Msg::SettingWritten {
edit,
ticket,
result: Ok(()),
});
assert_eq!(effect, Effect::LoadSettings);
let mut updated = fixtures::settings_snapshot();
updated.max_cron_sleep = ScalarView {
value: "30s".to_string(),
source: StyleSource::Default,
};
let _ = app.update(Msg::Settings {
result: Ok(updated.clone()),
});
assert_eq!(app.settings().unwrap().snapshot(), &updated);
}
#[test]
fn a_refused_write_closes_the_overlay_before_reopening_the_editor() {
let mut app = fixtures::app_in_settings_on(SettingField::MaxCronSleep);
let _ = app.update(Msg::Key(KeyPress::Confirm));
let _ = app.update(Msg::Key(KeyPress::TextApply));
let Effect::WriteSetting { edit, ticket, .. } = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send");
};
let _ = app.update(Msg::Key(KeyPress::Help));
assert!(
app.keymap_open(),
"a write in flight is not armed, so `h` must open the overlay"
);
let _ = app.update(Msg::SettingWritten {
edit,
ticket,
result: Err("refused".to_string()),
});
assert!(
!app.keymap_open(),
"the overlay is still up while text mode owns the keyboard"
);
assert_eq!(
app.mode,
InputMode::Text,
"the editor must reopen, so the operator gets their typed text back"
);
assert!(
matches!(
app.settings().unwrap().pending,
Some(Pending::Typing { .. })
),
"the refused write's buffer is what the editor reopens with"
);
}
#[test]
fn the_cursor_survives_a_landed_writes_reload() {
let mut app = fixtures::app_in_settings_on(SettingField::MaxCronSleep);
let before = app.settings().unwrap().cursor();
let _ = app.update(Msg::Key(KeyPress::Confirm));
let _ = app.update(Msg::Key(KeyPress::TextApply));
let Effect::WriteSetting { edit, ticket, .. } = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send");
};
let _ = app.update(Msg::SettingWritten {
edit,
ticket,
result: Ok(()),
});
let _ = app.update(Msg::Settings {
result: Ok(fixtures::settings_snapshot()),
});
assert_eq!(app.settings().unwrap().cursor(), before);
}
#[test]
fn the_cursor_survives_a_refresh() {
let mut app = fixtures::app_in_settings_on(SettingField::MaxCronSleep);
let before = app.settings().unwrap().cursor();
assert_eq!(
app.update(Msg::Key(KeyPress::Refresh)),
Effect::LoadSettings
);
let _ = app.update(Msg::Settings {
result: Ok(fixtures::settings_snapshot()),
});
assert_eq!(app.settings().unwrap().cursor(), before);
}
#[test]
fn a_refused_write_says_why_and_leaves_the_row_alone() {
let mut app = fixtures::app_in_settings_with_control();
let before = app.settings().unwrap().snapshot().log_level.clone();
let _ = app.update(Msg::Key(KeyPress::Cycle));
let Effect::WriteSetting { edit, ticket, .. } = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send");
};
let _ = app.update(Msg::SettingWritten {
edit,
ticket,
result: Err("max_cron_sleep is 500ms, below the 1s floor".into()),
});
assert_eq!(app.settings().unwrap().snapshot().log_level, before);
let notice = app.notice().unwrap();
assert!(notice.is_grave());
assert!(notice.to_string().contains("below the 1s floor"));
}
#[test]
fn a_superseded_reply_does_not_resolve_the_edit_that_replaced_it() {
let mut app = fixtures::app_in_settings_with_control();
let _ = app.update(Msg::Key(KeyPress::Cycle));
let Effect::WriteSetting {
edit: first,
ticket: first_ticket,
..
} = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send the first edit");
};
let _ = app.update(Msg::Key(KeyPress::SelectDown));
let _ = app.update(Msg::Key(KeyPress::Cycle));
let Effect::WriteSetting {
edit: second,
ticket: second_ticket,
..
} = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send the second edit too");
};
assert_ne!(first_ticket, second_ticket, "two writes are two tickets");
let effect = app.update(Msg::SettingWritten {
edit: first,
ticket: first_ticket,
result: Ok(()),
});
assert_eq!(
effect,
Effect::None,
"a re-read here rebuilds the screen and throws the live edit away"
);
assert!(
app.settings().unwrap().pending().is_some_and(|p| p.sent),
"the second edit is still in flight and still says so"
);
let effect = app.update(Msg::SettingWritten {
edit: second,
ticket: second_ticket,
result: Ok(()),
});
assert_eq!(effect, Effect::LoadSettings, "its own reply re-reads");
assert!(app.settings().unwrap().pending().is_none());
}
#[test]
fn a_superseded_refusal_does_not_reopen_the_editor_over_a_live_edit() {
let mut app = fixtures::app_in_settings_on(SettingField::Socket);
let _ = app.update(Msg::Key(KeyPress::Confirm));
let _ = app.update(Msg::Key(KeyPress::TextApply));
let Effect::WriteSetting {
edit: socket,
ticket: socket_ticket,
..
} = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send the socket edit");
};
let _ = app.update(Msg::Key(KeyPress::SelectFirst));
let _ = app.update(Msg::Key(KeyPress::Cycle));
let Effect::WriteSetting {
edit: level,
ticket: level_ticket,
..
} = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send the log level edit too");
};
let _ = app.update(Msg::SettingWritten {
edit: socket,
ticket: socket_ticket,
result: Err("the socket path is too long".into()),
});
assert_eq!(
app.mode(),
InputMode::Normal,
"no editor was opened, so no text mode is owed one"
);
assert!(app.settings().unwrap().typing().is_none());
assert!(
app.settings().unwrap().pending().is_some_and(|p| p.sent),
"the log level edit is still in flight"
);
assert!(
app.notice().unwrap().to_string().contains("too long"),
"the refusal is still reported"
);
let _ = app.update(Msg::SettingWritten {
edit: level,
ticket: level_ticket,
result: Ok(()),
});
assert_eq!(app.mode(), InputMode::Normal);
assert!(app.settings().unwrap().pending().is_none());
}
#[test]
fn a_superseded_dog_reply_does_not_resolve_the_edit_that_replaced_it() {
let mut app = fixtures::app_in_settings_on_dog("metrics");
let _ = app.update(Msg::Key(KeyPress::Cycle));
let Effect::WriteDog { edit, ticket, .. } = app.update(Msg::Key(KeyPress::Confirm)) else {
panic!("Enter must send the file half");
};
let Effect::Send(dog) = app.update(Msg::DogWritten {
edit,
ticket,
result: Ok(DogSource::BuiltIn),
}) else {
panic!("a landed file half must raise the daemon half");
};
let _ = app.update(Msg::Key(KeyPress::SelectFirst));
let _ = app.update(Msg::Key(KeyPress::Cycle));
let Effect::WriteSetting { ticket: scalar, .. } = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send the scalar edit");
};
assert_ne!(ticket, scalar, "the toggle and the scalar are two writes");
let info = ProcessInfo::builder(50, "metrics", ProcStatus::Online)
.pid(Some(50_000))
.dog(Some(DogSource::BuiltIn))
.build();
let effect = app.update(Msg::Replied {
sent: dog,
result: Ok(Response::DogStarted(info)),
});
assert_eq!(effect, Effect::None, "the live edit survives a re-read");
assert!(
app.settings().unwrap().pending().is_some_and(|p| p.sent),
"the scalar edit is still in flight and still says so"
);
assert_eq!(
app.notice().map(ToString::to_string).as_deref(),
Some("enable metrics: the shepherd started it"),
"the toggle still reports what the shepherd did"
);
}
#[test]
fn a_reply_for_a_write_the_screen_walked_away_from_touches_nothing() {
let mut app = fixtures::app_in_settings_on(SettingField::Socket);
let _ = app.update(Msg::Key(KeyPress::Confirm));
let _ = app.update(Msg::Key(KeyPress::TextApply));
let Effect::WriteSetting {
edit: socket,
ticket: socket_ticket,
..
} = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send the socket edit");
};
let _ = app.update(Msg::Key(KeyPress::Escape));
assert!(app.settings().is_none(), "Escape leaves the screen");
let _ = app.update(Msg::Key(KeyPress::Settings));
let _ = app.update(Msg::Settings {
result: Ok(fixtures::settings_snapshot()),
});
let _ = app.update(Msg::Key(KeyPress::Cycle));
let Effect::WriteSetting { ticket: level, .. } = app.update(Msg::Key(KeyPress::Confirm))
else {
panic!("Enter must send the reopened screen's edit");
};
assert_ne!(socket_ticket, level, "a reopened screen mints its own");
let _ = app.update(Msg::SettingWritten {
edit: socket,
ticket: socket_ticket,
result: Err("the socket path is too long".into()),
});
assert_eq!(app.mode(), InputMode::Normal);
assert!(app.settings().unwrap().typing().is_none());
assert!(
app.settings().unwrap().pending().is_some_and(|p| p.sent),
"the reopened screen's own edit is untouched"
);
}
}