use super::super::*;
use crate::dog_settings::touched_refusal;
use crate::lookout::edits::EditKey;
use crate::lookout::pane::toml_table;
impl App {
pub(in crate::lookout::app) fn take_pane_writes(&mut self) -> PaneWrites {
let mut writes = self.take_seated_writes();
let edited = self
.config_pane()
.map(|pane| pane.dogs_tab().edited())
.unwrap_or_default();
for dog in edited {
writes.requests.extend(
self.in_dog_table(dog, Self::take_seated_writes)
.unwrap_or_default()
.requests,
);
}
writes
}
fn take_seated_writes(&mut self) -> PaneWrites {
let Some(authority) = WriteAuthority::granted(self) else {
return PaneWrites::default();
};
let Some(pane) = self.body.config_pane_mut() else {
return PaneWrites::default();
};
let edits = pane.close();
if edits.is_empty() {
return PaneWrites::default();
}
let mut ticket = self.next_write_ticket;
let mut writes = PaneWrites::default();
match pane.target().clone() {
PaneTarget::Dog { name, .. } => {
let env: Vec<(String, Option<EnvValue>)> = edits
.iter()
.filter_map(|(key, entry)| match (key, entry.edit()) {
(EditKey::Env(key), PaneEdit::SetEnv { value, .. }) => {
Some((key.clone(), value.clone()))
}
_ => None,
})
.collect();
if !env.is_empty() {
writes.env = Some(DogEnvWrite {
name: name.clone(),
edits: env,
ticket,
authority,
});
ticket += 1;
}
let dropped = if writes.env.is_some() {
", its env edit with it"
} else {
""
};
let schema = pane.schema().cloned();
match pane.edited_section_with(&edits) {
SectionWrite::Written(toml) => {
if let Some(refused) = schema.as_ref().and_then(|schema| {
touched_refusal(
schema,
&toml_table(toml.as_str()),
&pane.touched_paths(&edits),
)
}) {
self.notice =
Some(Notice::grave(format!("{name}: {}{dropped}", refused.text)));
return PaneWrites::default();
}
writes.requests.push(Sent::SetDogSection {
name,
ticket,
toml,
authority,
});
ticket += 1;
}
SectionWrite::Nothing => {}
SectionWrite::Refused => {
self.notice = Some(Notice::grave(format!(
"{name}: its section in dogs.toml does not parse, or is not shaped for \
the edit{dropped}"
)));
return PaneWrites::default();
}
}
}
PaneTarget::Sheep { name } => {
let filed = edits.into_writes();
let mut requests = Vec::with_capacity(filed.len());
for edit in filed {
let this = ticket;
ticket += 1;
requests.push(match edit {
PaneEdit::SetEnv { key, value } => Sent::SetEnv {
name: name.clone(),
ticket: this,
key,
value,
authority,
},
PaneEdit::Set { key, value } => Sent::ApplyField {
name: name.clone(),
ticket: this,
key,
value,
authority,
},
});
}
writes.requests = requests;
}
PaneTarget::SheepDog { sheep, dog, .. } => {
let schema = pane.schema().cloned();
let table = pane.edited_table_with(&edits);
let touched = pane.touched_paths(&edits);
if let Some(refused) = schema
.as_ref()
.and_then(|schema| touched_refusal(schema, &table, &touched))
{
self.notice = Some(Notice::grave(format!("{sheep}.{dog}: {}", refused.text)));
return PaneWrites::default();
}
writes.requests.push(Sent::SetSheepDogTable {
name: sheep,
dog,
ticket,
table: Some(table.into()),
authority,
});
ticket += 1;
}
}
self.next_write_ticket = ticket;
writes
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lookout::app::testing::*;
#[test]
fn one_edit_reaches_the_wire_as_a_single_field_override() {
for key in [
"autorestart", "watch", "merge_logs", ] {
let mut app = fixtures::app_in_sheep_pane_with_control();
pane_to(&mut app, key);
let _ = app.update(Msg::Key(KeyPress::Cycle));
assert_eq!(
app.config_pane().unwrap().edits().len(),
1,
"{key}: space files an edit and sends nothing"
);
let request = one_wire(close_writing(&mut app));
let Request::SetSheepField {
name,
key: sent,
value,
} = request
else {
panic!("{key}: expected SetSheepField, got {request:?}");
};
assert_eq!(name, "web", "{key}");
assert_eq!(sent, key, "{key}");
assert!(value.is_boolean(), "{key}: {value}");
}
}
#[test]
fn escape_sends_every_filed_edit_at_once() {
let mut app = fixtures::app_in_sheep_pane_with_two_edits();
let effect = close_writing(&mut app);
let Effect::SendAll(sent) = effect else {
panic!("wanted a batch, got {effect:?}");
};
assert_eq!(sent.len(), 2);
assert!(
app.config_pane().is_none(),
"the pane closes on the same key"
);
}
#[test]
fn escape_with_nothing_filed_closes_and_sends_nothing() {
let mut app = fixtures::app_in_sheep_pane();
let effect = app.update(Msg::Key(KeyPress::Escape));
assert!(matches!(effect, Effect::None), "{effect:?}");
assert!(app.config_pane().is_none());
}
#[test]
fn a_refused_write_notices_after_the_pane_has_closed() {
let mut app = fixtures::app_in_sheep_pane_with_two_edits();
let Effect::SendAll(mut sent) = close_writing(&mut app) else {
panic!("wanted a batch");
};
let first = sent.remove(0);
assert!(app.config_pane().is_none());
app.update(Msg::Replied {
sent: first,
result: Err(fixtures::a_refusal()),
});
let notice = app
.notice()
.map(ToString::to_string)
.expect("a refusal says so");
assert!(
notice.contains("cwd"),
"the notice names the field: {notice}"
);
}
#[test]
fn a_config_re_read_replaces_the_values_and_keeps_the_edits() {
let mut app = fixtures::app_in_sheep_pane_with_two_edits();
let Effect::Send(sent) = app.update(Msg::Key(KeyPress::Refresh)) else {
panic!("refresh asks the shepherd for the config again");
};
app.update(Msg::Replied {
sent,
result: Ok(Response::SheepConfig(Box::new(
fixtures::sheep_config_view(),
))),
});
assert_eq!(app.config_pane().unwrap().edits().len(), 2);
}
#[test]
fn a_batch_names_each_key_in_its_own_request() {
let mut app = fixtures::app_in_sheep_pane_with_control();
pane_to(&mut app, "autorestart");
let _ = app.update(Msg::Key(KeyPress::Cycle));
pane_to(&mut app, "watch");
let _ = app.update(Msg::Key(KeyPress::Cycle));
let requests = wire_all(close_writing(&mut app));
let named: Vec<String> = requests
.iter()
.map(|request| match request {
Request::SetSheepField { key, .. } => key.clone(),
other => panic!("{other:?}"),
})
.collect();
assert_eq!(named, vec!["autorestart".to_owned(), "watch".to_owned()]);
}
#[test]
fn a_landed_write_re_reads_the_config_and_reports_what_the_shepherd_said() {
for (pending, wanted) in [(false, "set to false"), (true, "shep reload")] {
let mut app = fixtures::app_in_sheep_pane_with_control();
pane_to(&mut app, "autorestart");
let _ = app.update(Msg::Key(KeyPress::Cycle));
let mut batch = wire_batch(close_writing(&mut app));
let effect = app.update(Msg::Replied {
sent: batch.remove(0),
result: Ok(Response::SheepFieldSet {
name: "web".to_owned(),
key: "autorestart".to_owned(),
pending,
warning: None,
}),
});
assert_eq!(
effect,
Effect::Send(Sent::SheepConfig {
name: "web".to_owned()
}),
"a landed write re-reads what the shepherd now holds"
);
let notice = app.notice().expect("the outcome is reported");
assert!(!notice.is_grave(), "{notice:?}");
assert!(notice.to_string().contains(wanted), "{notice:?}");
}
}
#[test]
fn a_path_warning_rides_the_same_notice_as_the_write() {
let mut app = fixtures::app_in_sheep_pane_with_control();
pane_to(&mut app, "cwd");
fixtures::type_into_the_open_editor(&mut app, "/does/not/exist");
app.update(Msg::Key(KeyPress::Escape));
let mut batch = wire_batch(app.update(Msg::Key(KeyPress::Continue)));
let effect = app.update(Msg::Replied {
sent: batch.remove(0),
result: Ok(Response::SheepFieldSet {
name: "web".to_owned(),
key: "cwd".to_owned(),
pending: true,
warning: Some("/does/not/exist does not exist yet".to_owned()),
}),
});
assert!(
matches!(effect, Effect::Send(Sent::SheepConfig { .. })),
"{effect:?}"
);
let notice = app.notice().expect("the outcome is reported");
assert!(!notice.is_grave(), "{notice:?}");
let text = notice.to_string();
assert!(text.contains("shep reload"), "{text}");
assert!(text.contains("does not exist yet"), "{text}");
}
#[test]
fn a_refused_write_is_reported_and_does_not_re_read() {
let mut app = fixtures::app_in_sheep_pane_with_control();
pane_to(&mut app, "autorestart");
let _ = app.update(Msg::Key(KeyPress::Cycle));
let mut batch = wire_batch(close_writing(&mut app));
let effect = app.update(Msg::Replied {
sent: batch.remove(0),
result: Err(fixtures::a_refusal()),
});
assert_eq!(effect, Effect::None, "a refusal does not re-read");
let notice = app.notice().expect("the refusal is reported");
assert!(notice.is_grave());
assert!(
notice.to_string().contains("the store is locked"),
"{notice:?}"
);
}
#[test]
fn a_write_effects_debug_names_no_value() {
let mut app = fixtures::app_in_sheep_pane_with_control();
pane_to(&mut app, "cwd");
let _ = app.update(Msg::Key(KeyPress::Confirm));
for _ in 0..40 {
let _ = app.update(Msg::Key(KeyPress::TextBackspace));
}
for typed in "/home/ada/secret-project".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let field = close_writing(&mut app);
assert_eq!(
format!("{field:?}"),
"SendAll([ApplyField { name: \"web\", ticket: 0, key: \"cwd\", \
value: FieldValue(<string>), authority: WriteAuthority(()) }])"
);
let mut app = fixtures::app_in_sheep_pane_with_control();
let _ = app.update(Msg::Key(KeyPress::SelectLast));
let _ = app.update(Msg::Key(KeyPress::SelectUp));
let _ = app.update(Msg::Key(KeyPress::SelectUp));
assert_eq!(app.config_pane().unwrap().cursor(), Some(PaneRow::Env(0)));
let _ = app.update(Msg::Key(KeyPress::Confirm));
for typed in "hunter2".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let env = close_writing(&mut app);
assert_eq!(
format!("{env:?}"),
"SendAll([SetEnv { name: \"web\", ticket: 0, key: \"DB_HOST\", \
value: Some(EnvValue(<7 bytes>)), authority: WriteAuthority(()) }])"
);
}
#[test]
fn closing_a_dogs_env_edit_writes_the_file_before_anything_else() {
let mut app = fixtures::app_in_adopted_dog_pane_with_env();
let _ = app.update(Msg::Key(KeyPress::SelectLast));
let _ = app.update(Msg::Key(KeyPress::SelectUp));
assert_eq!(
app.config_pane().unwrap().cursor(),
Some(PaneRow::Env(1)),
"RUST_LOG, the fixture's second env key"
);
let _ = app.update(Msg::Key(KeyPress::Confirm));
for typed in "warn".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let sent = read_dog_close(&mut app);
let effect = app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: "ratio = 1.5\n".to_owned().into(),
}),
});
let Effect::WriteDogEnv { write, then } = effect else {
panic!("a dog's env edit writes shep.toml: {effect:?}");
};
assert_eq!(write.name, "kelpie");
assert_eq!(write.edits.len(), 1);
assert_eq!(write.edits[0].0, "RUST_LOG");
assert!(
then.is_empty(),
"the section says what it said, so the close has no daemon half: {then:?}"
);
}
#[test]
fn a_dogs_env_and_section_edits_close_as_the_file_half_then_the_request() {
let mut app = fixtures::app_in_adopted_dog_pane_with_env();
pane_to(&mut app, "ratio");
let _ = app.update(Msg::Key(KeyPress::Confirm));
for _ in 0..64 {
let _ = app.update(Msg::Key(KeyPress::TextBackspace));
}
for typed in "2.5".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let _ = app.update(Msg::Key(KeyPress::SelectLast));
let _ = app.update(Msg::Key(KeyPress::SelectUp));
let _ = app.update(Msg::Key(KeyPress::Confirm));
for typed in "warn".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let sent = read_dog_close(&mut app);
let effect = app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: "ratio = 1.5\n".to_owned().into(),
}),
});
let Effect::WriteDogEnv { write, then } = effect else {
panic!("the file half goes first: {effect:?}");
};
assert_eq!(write.edits.len(), 1);
assert_eq!(then.len(), 1);
assert!(matches!(then[0], Sent::SetDogSection { .. }), "{then:?}");
let effect = app.update(Msg::DogEnvWritten {
name: write.name,
ticket: write.ticket,
then,
result: Ok(()),
});
let Effect::SendAll(mut sent) = effect else {
panic!("the request the close built goes once the file lands: {effect:?}");
};
assert!(
matches!(sent.remove(0), Sent::SetDogSection { .. }),
"and it is the section write"
);
let notice = app.notice().expect("the write reports itself").to_string();
assert!(notice.contains("kelpie"), "{notice}");
assert!(
notice.contains("keeps the one it started with"),
"a running dog is not silently re-handed its environment: {notice}"
);
assert!(notice.contains("shep disable kelpie"), "{notice}");
}
#[test]
fn a_refused_dog_env_write_sends_no_section() {
let mut app = fixtures::app_in_adopted_dog_pane_with_env();
pane_to(&mut app, "ratio");
let _ = app.update(Msg::Key(KeyPress::Confirm));
for _ in 0..64 {
let _ = app.update(Msg::Key(KeyPress::TextBackspace));
}
for typed in "2.5".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let _ = app.update(Msg::Key(KeyPress::SelectLast));
let _ = app.update(Msg::Key(KeyPress::SelectUp));
let _ = app.update(Msg::Key(KeyPress::Confirm));
for typed in "warn".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let sent = read_dog_close(&mut app);
let Effect::WriteDogEnv { write, then } = app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: "ratio = 1.5\n".to_owned().into(),
}),
}) else {
panic!("wanted the file half");
};
let effect = app.update(Msg::DogEnvWritten {
name: write.name,
ticket: write.ticket,
then,
result: Err("permission denied".to_owned()),
});
assert_eq!(effect, Effect::None, "nothing goes to the shepherd");
assert!(app.notice().expect("the refusal says so").is_grave());
}
#[test]
fn a_refused_dog_section_names_the_env_edit_it_drops_with_it() {
let mut app = fixtures::app_in_adopted_dog_pane_with_an_unwriteable_section();
let _ = app.update(Msg::Key(KeyPress::SelectLast));
let _ = app.update(Msg::Key(KeyPress::SelectUp));
assert_eq!(
app.config_pane().unwrap().cursor(),
Some(PaneRow::Env(0)),
"PADDOCK_KEY, the fixture's only env key"
);
let _ = app.update(Msg::Key(KeyPress::Confirm));
for typed in "sk_live_1".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
pane_to(&mut app, "models.model");
let _ = app.update(Msg::Key(KeyPress::Confirm));
for typed in "large".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let sent = read_dog_close(&mut app);
let effect = app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: "models = \"old\"\n".to_owned().into(),
}),
});
assert_eq!(
effect,
Effect::None,
"the close writes neither half: {effect:?}"
);
let notice = app.notice().expect("the refusal says so").to_string();
assert!(notice.contains("env edit"), "{notice}");
}
#[test]
fn a_dog_env_write_effect_debug_names_no_value() {
let mut app = fixtures::app_in_adopted_dog_pane_with_env();
let _ = app.update(Msg::Key(KeyPress::SelectLast));
let _ = app.update(Msg::Key(KeyPress::SelectUp));
let _ = app.update(Msg::Key(KeyPress::Confirm));
for typed in "sk_live_1".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let sent = read_dog_close(&mut app);
let effect = app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: "ratio = 1.5\n".to_owned().into(),
}),
});
assert_eq!(
format!("{effect:?}"),
"WriteDogEnv { write: DogEnvWrite { name: \"kelpie\", edits: [(\"RUST_LOG\", Some(EnvValue(<9 bytes>)))], ticket: 0, authority: WriteAuthority(()) }, then: [] }"
);
}
#[test]
fn no_two_writes_share_a_ticket() {
let mut app = fixtures::app_in_sheep_pane_with_control();
pane_to(&mut app, "autorestart");
let _ = app.update(Msg::Key(KeyPress::Cycle));
pane_to(&mut app, "watch");
let _ = app.update(Msg::Key(KeyPress::Cycle));
let first = wire_batch(close_writing(&mut app));
assert_ne!(first[0], first[1], "two entries are two tickets");
let _ = app.update(Msg::Key(KeyPress::Escape));
let _ = app.update(Msg::Key(KeyPress::Edit));
let _ = app.update(Msg::Replied {
sent: Sent::SheepConfig {
name: "web".to_owned(),
},
result: Ok(Response::SheepConfig(Box::new(
fixtures::sheep_config_view(),
))),
});
pane_to(&mut app, "autorestart");
let _ = app.update(Msg::Key(KeyPress::Cycle));
let second = wire_batch(close_writing(&mut app));
assert_ne!(
first[0], second[0],
"a second close does not reuse the first's tickets"
);
}
#[track_caller]
fn read_dog_close(app: &mut App) -> Sent {
let Effect::Send(sent) = app.update(Msg::Key(KeyPress::Escape)) else {
panic!("a dog close reads its section first");
};
sent
}
#[test]
fn a_dotted_dog_row_changes_one_line_of_its_section() {
let mut app = fixtures::app_in_dog_pane_with_a_nested_table();
pane_to(&mut app, "models.worker.model");
let _ = app.update(Msg::Key(KeyPress::Confirm));
for _ in 0..64 {
let _ = app.update(Msg::Key(KeyPress::TextBackspace));
}
for typed in "large".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let Effect::Send(sent) = app.update(Msg::Key(KeyPress::Escape)) else {
panic!("a dog close reads its section first");
};
let effect = app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: fixtures::nested_dog_section().into(),
}),
});
let mut batch = wire_batch(effect);
let Sent::SetDogSection { name, toml, .. } = batch.remove(0) else {
panic!("closing the pane sends the section: {batch:?}");
};
assert_eq!(name, "jobs");
let before = fixtures::nested_dog_section();
let sent = toml.as_str();
let changed: Vec<(usize, &str, &str)> = sent
.lines()
.zip(before.lines())
.enumerate()
.filter(|(_, pair)| pair.0 != pair.1)
.map(|(line, pair)| (line, pair.0, pair.1))
.collect();
assert_eq!(
changed,
vec![(6, "model = \"large\"", "model = \"gpt\"")],
"{sent}",
);
}
#[test]
fn a_dog_unset_the_section_already_carries_closes_without_a_notice() {
let mut app = fixtures::app_in_dog_pane_with_a_nested_table();
pane_to(&mut app, "models.worker.model");
let _ = app.update(Msg::Key(KeyPress::Remove));
assert_eq!(
app.config_pane().expect("the pane is open").edits().len(),
1,
"d on a dog row files the removal"
);
let removed = fixtures::nested_dog_section().replace("model = \"gpt\"\n", "");
let Effect::Send(sent) = app.update(Msg::Key(KeyPress::Escape)) else {
panic!("a dog close reads its section first");
};
let effect = app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: removed.clone().into(),
}),
});
assert!(matches!(effect, Effect::None), "{effect:?}");
assert!(
app.config_pane().is_some(),
"a moved section holds the pane"
);
let Effect::Send(sent) = app.update(Msg::Key(KeyPress::Escape)) else {
panic!("the second close reads again");
};
let effect = app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: removed.into(),
}),
});
assert!(matches!(effect, Effect::None), "{effect:?}");
assert!(
app.config_pane().is_none(),
"the close finishes: nothing to send is not a refusal"
);
assert_eq!(
app.notice().map(ToString::to_string),
None,
"the file parses and already says what the operator asked for"
);
}
#[test]
fn an_element_added_to_a_dogs_list_changes_one_line_of_its_section() {
let mut app = fixtures::app_in_dog_pane_with_lists();
pane_to(&mut app, "hosts");
let _ = app.update(Msg::Key(KeyPress::Confirm));
let _ = app.update(Msg::Key(KeyPress::SelectLast));
let _ = app.update(Msg::Key(KeyPress::Edit));
for typed in "c.example".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(typed)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
assert_eq!(
filed_value(&app, "hosts"),
serde_json::json!(["a.example", "b.example", "c.example"])
);
let _ = app.update(Msg::Key(KeyPress::Escape));
let Effect::Send(sent) = app.update(Msg::Key(KeyPress::Escape)) else {
panic!("a dog close reads its section first");
};
let effect = app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: fixtures::list_dog_section().into(),
}),
});
let mut batch = wire_batch(effect);
let Sent::SetDogSection { name, toml, .. } = batch.remove(0) else {
panic!("closing the pane sends the section: {batch:?}");
};
assert_eq!(name, "watch");
let before = fixtures::list_dog_section();
let sent = toml.as_str();
let changed: Vec<(usize, &str, &str)> = sent
.lines()
.zip(before.lines())
.enumerate()
.filter(|(_, pair)| pair.0 != pair.1)
.map(|(line, pair)| (line, pair.0, pair.1))
.collect();
assert_eq!(
changed,
vec![(
1,
"hosts = [\"a.example\", \"b.example\", \"c.example\"]",
"hosts = [\"a.example\", \"b.example\"]"
)],
"{sent}",
);
assert_eq!(sent.lines().count(), before.lines().count(), "{sent}");
}
#[test]
fn an_element_a_dogs_item_schema_refuses_files_nothing() {
let mut app = fixtures::app_in_dog_pane_with_lists();
pane_to(&mut app, "hosts");
let _ = app.update(Msg::Key(KeyPress::Confirm));
let _ = app.update(Msg::Key(KeyPress::SelectLast));
let _ = app.update(Msg::Key(KeyPress::Edit));
let _ = app.update(Msg::Key(KeyPress::TextChar('x')));
assert_eq!(app.update(Msg::Key(KeyPress::TextApply)), Effect::None);
assert_eq!(app.mode(), InputMode::Text, "the editor stays open");
let notice = app.notice().expect("a refusal answers");
assert!(notice.is_grave(), "{notice}");
assert!(notice.to_string().starts_with("hosts is \"x\""), "{notice}");
assert!(
app.config_pane()
.expect("the pane is open")
.edits()
.is_empty(),
"nothing was filed"
);
}
#[test]
fn enter_on_a_dogs_secret_list_or_map_opens_no_sub_screen() {
for key in ["keys", "labels"] {
let mut app = fixtures::app_in_dog_pane_with_lists();
pane_to(&mut app, key);
let _ = app.update(Msg::Key(KeyPress::Confirm));
let pane = app.config_pane().expect("the pane is open");
assert!(pane.list().is_none(), "{key}");
let notice = app.notice().expect("a locked row answers").to_string();
assert!(notice.contains("no editor in this pane"), "{key}: {notice}");
}
}
}