use std::path::Path;
use std::time::{Duration, Instant};
use shep_client::RequestError;
use shep_core::protocol::{Response, RpcError, RpcErrorCode};
use crate::commands::settings::load_settings;
use crate::commands::shep_toml::ShepToml;
use crate::lookout::app::{ActionVerb, App, Effect, KeyPress, Msg, RevealedValue, RowKey, Sent};
use crate::lookout::frames::fixtures::{
close_dialog_config_view, dogs_listing, edit_pane_config_view, flock_without_api, jobs_entry,
kelpie_config_view, kelpie_entry, move_settings_cursor_to, restarted_api,
settings_snapshot_for_gallery, settings_snapshot_with_dog_drift, sheep_dogs_config_view,
};
use crate::lookout::frames::scene::Scene;
use crate::lookout::pane::ListedDog;
use crate::lookout::secrets::{SecretRow, SecretsModel, Source};
use crate::lookout::view::fixtures::{
entries_dog_schema, entries_dog_section, open_adopted_dog_row, select_field, select_node,
select_setting,
};
use crate::secret_readers::Reader;
use crate::style::{StyleLevel, StyleSource};
pub(super) fn apply_actions(app: &mut App, which: Scene, t0: Instant) {
match which {
Scene::ActionRefusedOffline => {
app.update(Msg::Retrying { attempt: 3 });
app.update(Msg::Key(KeyPress::Action(ActionVerb::Restart)));
}
Scene::Confirm | Scene::Acting | Scene::ActionRefused | Scene::ActionAccepted => {
app.update(Msg::Key(KeyPress::Action(ActionVerb::Restart)));
if which != Scene::Confirm {
app.update(Msg::Key(KeyPress::Confirm));
}
if which == Scene::ActionAccepted {
app.update(Msg::Replied {
sent: Sent::Action {
verb: ActionVerb::Restart,
target: RowKey::Sheep(2),
name: "api".to_string(),
},
result: Ok(Response::Restarted {
accepted: vec![restarted_api()],
refused: Vec::new(),
}),
});
}
if which == Scene::ActionRefused {
app.update(Msg::Snapshot {
rows: flock_without_api(),
at: t0,
});
app.update(Msg::Replied {
sent: Sent::Action {
verb: ActionVerb::Restart,
target: RowKey::Sheep(2),
name: "api".to_string(),
},
result: Err(RequestError::Rpc(RpcError {
code: RpcErrorCode::NotFound,
message: "selector matched no registered sheep".to_string(),
daemon_version: None,
})),
});
}
}
_ => {}
}
}
pub(super) fn apply_post_tick_scene(app: &mut App, which: Scene) {
match which {
Scene::Secrets => {
app.update(Msg::Key(KeyPress::Secrets));
app.update(Msg::Secrets {
environment: "production".to_string(),
result: Ok(Box::new(SecretsModel {
environments: vec![
"all".to_string(),
"ci".to_string(),
"production".to_string(),
],
rows: vec![
SecretRow {
key: "DB_PASSWORD".to_string(),
source: Source::Operator,
in_force: Some("production".to_string()),
set_in: vec!["production".to_string()],
byte_len: Some("hunter2-not-really".len()),
readers: vec![Reader {
name: "catcher".to_string(),
environment: "production".to_string(),
online: true,
}],
},
SecretRow {
key: "ELSEWHERE_ONLY".to_string(),
source: Source::Operator,
in_force: None,
set_in: vec!["ci".to_string()],
byte_len: None,
readers: Vec::new(),
},
SecretRow {
key: "vercel/API_TOKEN".to_string(),
source: Source::Namespace("vercel".to_string()),
in_force: Some("production".to_string()),
set_in: vec!["production".to_string()],
byte_len: Some(6),
readers: Vec::new(),
},
],
roll_age: Some(Duration::from_secs(184)),
allow_read: true,
..SecretsModel::default()
})),
});
app.update(Msg::Key(KeyPress::Reveal));
app.update(Msg::Revealed {
key: "DB_PASSWORD".to_string(),
environment: "production".to_string(),
value: Some(RevealedValue("hunter2-not-really".to_string())),
});
}
Scene::SettingsFresh => {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("shep.toml");
ShepToml::edit(&path, ShepToml::write_starter_interpreters).unwrap();
let snapshot = load_settings(
&path,
std::path::Path::new("/home/ada/.shep/run/shep.sock"),
(StyleLevel::Full, StyleSource::Default),
)
.unwrap();
app.update(Msg::Key(KeyPress::Settings));
app.update(Msg::Settings {
result: Ok(snapshot),
});
}
Scene::SettingsSet => {
app.update(Msg::Key(KeyPress::Settings));
app.update(Msg::Settings {
result: Ok(settings_snapshot_for_gallery()),
});
}
Scene::SettingsConfirm => {
app.update(Msg::Key(KeyPress::Settings));
app.update(Msg::Settings {
result: Ok(settings_snapshot_for_gallery()),
});
move_settings_cursor_to(app, "daemon.log_level");
app.update(Msg::Key(KeyPress::Cycle));
}
Scene::SettingsTyping => {
app.update(Msg::Key(KeyPress::Settings));
app.update(Msg::Settings {
result: Ok(settings_snapshot_for_gallery()),
});
move_settings_cursor_to(app, "daemon.socket");
app.update(Msg::Key(KeyPress::Confirm));
for _ in 0..8 {
app.update(Msg::Key(KeyPress::TextBackspace));
}
}
Scene::SettingsDogs | Scene::SettingsNarrow => {
app.update(Msg::Key(KeyPress::Settings));
app.update(Msg::Settings {
result: Ok(settings_snapshot_with_dog_drift()),
});
}
Scene::SettingsDogEnv | Scene::SettingsDogEnvNarrow => {
app.update(Msg::Key(KeyPress::Settings));
app.update(Msg::Settings {
result: Ok(settings_snapshot_with_dog_drift()),
});
open_adopted_dog_row(app, "otel", Path::new("/usr/local/bin/shep-otel"));
select_setting(app, "dog.otel.env+");
}
Scene::SettingsShort => {
app.update(Msg::Key(KeyPress::Settings));
app.update(Msg::Settings {
result: Ok(settings_snapshot_with_dog_drift()),
});
app.update(Msg::Key(KeyPress::SelectLast));
}
Scene::EditPane
| Scene::EditPaneEdited
| Scene::EditPaneSqueezed
| Scene::EditPaneNarrow => {
app.update(Msg::Key(KeyPress::Edit));
app.update(Msg::Replied {
sent: Sent::SheepConfig {
name: "api".to_string(),
},
result: Ok(Response::SheepConfig(Box::new(edit_pane_config_view()))),
});
if which == Scene::EditPaneEdited {
for (key, typed) in [("cwd", "/srv/api"), ("max_memory", "256")] {
select_field(app, key);
app.update(Msg::Key(KeyPress::Confirm));
for character in typed.chars() {
app.update(Msg::Key(KeyPress::TextChar(character)));
}
app.update(Msg::Key(KeyPress::TextApply));
}
}
}
Scene::SheepDogsRow
| Scene::SheepDogsList
| Scene::SheepDogsRemove
| Scene::SheepDogTable => {
app.update(Msg::Key(KeyPress::Edit));
app.update(Msg::Replied {
sent: Sent::SheepConfig {
name: "api".to_string(),
},
result: Ok(Response::SheepConfig(Box::new(sheep_dogs_config_view()))),
});
let Effect::ListSheepDogs { ask, .. } = app.update(Msg::Key(KeyPress::Group(9))) else {
panic!("the dogs tab lists its dogs");
};
app.update(Msg::SheepDogNames {
sheep: "api".to_string(),
ask,
dogs: dogs_listing(),
});
if which != Scene::SheepDogsRow {
app.update(Msg::Key(KeyPress::SelectDown));
let Effect::LoadSheepDog { .. } = app.update(Msg::Key(KeyPress::Confirm)) else {
panic!("opening jobs probes it");
};
app.update(Msg::SheepDog {
sheep: "api".to_string(),
entry: jobs_entry(),
});
match which {
Scene::SheepDogsRemove => {
app.update(Msg::Key(KeyPress::Remove));
}
Scene::SheepDogTable => {
select_field(app, "models.worker.token");
}
_ => {}
}
}
}
Scene::SheepDogUnion => {
app.update(Msg::Key(KeyPress::Edit));
app.update(Msg::Replied {
sent: Sent::SheepConfig {
name: "api".to_string(),
},
result: Ok(Response::SheepConfig(Box::new(kelpie_config_view()))),
});
let Effect::ListSheepDogs { ask, .. } = app.update(Msg::Key(KeyPress::Group(9))) else {
panic!("the dogs tab lists its dogs");
};
app.update(Msg::SheepDogNames {
sheep: "api".to_string(),
ask,
dogs: vec![ListedDog {
entry: kelpie_entry(),
asked: true,
}],
});
app.update(Msg::Key(KeyPress::Confirm));
select_field(app, "skills.implement");
app.update(Msg::Key(KeyPress::Confirm));
app.update(Msg::Key(KeyPress::SelectDown));
app.update(Msg::Key(KeyPress::SelectDown));
}
Scene::DogEntries
| Scene::DogEntriesNarrow
| Scene::DogEntryTyping
| Scene::DogEntriesNew => {
app.update(Msg::DogPane {
name: "paddock".to_string(),
adopted_path: None,
env_keys: Vec::new(),
result: Ok(entries_dog_schema()),
});
app.update(Msg::Replied {
sent: Sent::DogSection {
name: "paddock".to_string(),
},
result: Ok(Response::DogSection {
toml: entries_dog_section().into(),
}),
});
let opened = match which {
Scene::DogEntryTyping => vec!["clients", "clients[1]"],
Scene::DogEntriesNew => vec!["models"],
_ => vec!["clients", "clients[0]"],
};
for key in opened {
select_node(app, key);
app.update(Msg::Key(KeyPress::TabNext));
}
if which == Scene::DogEntriesNew {
select_node(app, "models[+]");
app.update(Msg::Key(KeyPress::Confirm));
for character in "gpt-5".chars() {
app.update(Msg::Key(KeyPress::TextChar(character)));
}
} else if which == Scene::DogEntryTyping {
select_node(app, "clients[1].name");
app.update(Msg::Key(KeyPress::Confirm));
for character in "beta".chars() {
app.update(Msg::Key(KeyPress::TextChar(character)));
}
} else {
app.update(Msg::Key(KeyPress::SelectDown));
app.update(Msg::Key(KeyPress::SelectDown));
}
}
Scene::CloseDialog
| Scene::CloseDialogFloor
| Scene::CloseDialogNarrow
| Scene::CloseDialogParked => {
app.update(Msg::Key(KeyPress::Edit));
app.update(Msg::Replied {
sent: Sent::SheepConfig {
name: "api".to_string(),
},
result: Ok(Response::SheepConfig(Box::new(close_dialog_config_view()))),
});
if which != Scene::CloseDialogParked {
for (key, typed) in [("cwd", "/srv/api"), ("err_file", "/var/log/api/err.log")] {
select_field(app, key);
app.update(Msg::Key(KeyPress::Confirm));
for character in typed.chars() {
app.update(Msg::Key(KeyPress::TextChar(character)));
}
app.update(Msg::Key(KeyPress::TextApply));
}
}
app.update(Msg::Key(KeyPress::Escape));
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use crate::lookout::frames::build::probe::{
dog_row_for, marked_row_name_starts_with, row_for, selected_line, tab_dog_row_for,
};
use crate::lookout::frames::render::render_text;
use crate::lookout::frames::scene;
use super::*;
#[test]
#[cfg(unix)] fn every_action_scene_shows_its_own_stage_of_the_round_trip() {
let confirm = render_text(&scene(Scene::Confirm).1);
assert!(confirm.contains("restart api (id 2)? enter confirms, any other key cancels"));
assert!(confirm.contains("control enabled"), "the gate is open");
assert!(
row_for(&confirm, "api").is_some_and(|row| row.contains("online")),
"nothing was sent, so api is still online: {confirm:?}"
);
let acting_buffer = scene(Scene::Acting).1;
let acting = render_text(&acting_buffer);
assert!(acting.contains("restart api (id 2): sent, waiting for the shepherd"));
assert!(
selected_line(&acting, &acting_buffer).is_some_and(|line| line.contains("api")),
"the table is untouched: the selection is still on api"
);
assert!(
row_for(&acting, "api").is_some_and(|row| row.contains("online")),
"and the row still says what the shepherd last said"
);
let accepted = render_text(&scene(Scene::ActionAccepted).1);
assert!(accepted.contains("restart api (id 2): the shepherd restarted it"));
assert!(
row_for(&accepted, "api").is_some_and(|row| row.contains("48299")),
"the reply's own row reached the table without waiting for a poll"
);
let action_refused_buffer = scene(Scene::ActionRefused).1;
let refused = render_text(&action_refused_buffer);
assert!(refused.contains("restart api (id 2): selector matched no registered sheep"));
assert!(
!refused.contains("NotFound"),
"no Rust identifiers on the bar"
);
assert!(refused.contains("5 in the flock"), "one row shorter");
assert!(
row_for(&refused, "api").is_none(),
"api is the row that went"
);
assert!(
marked_row_name_starts_with(&refused, &action_refused_buffer, "billing"),
"and the cursor has moved to the row below: {refused:?}"
);
let offline = render_text(&scene(Scene::ActionRefusedOffline).1);
assert_eq!(
offline.matches("reconnecting (attempt 3)").count(),
2,
"the banner and the refusal under it agree, rather than one \
saying reconnecting and the other saying gone: {offline:?}"
);
assert!(
!offline.contains("nothing left to ask"),
"the ladder has not run out yet, so the refusal must not claim it has: {offline:?}"
);
let lambs_bar = render_text(&scene(Scene::Lambs).1);
for key in ["x stop", "R restart", "L reload"] {
assert!(lambs_bar.contains(key), "the control hint names {key}");
}
}
#[test]
#[cfg(unix)] fn every_settings_scene_shows_the_layer_it_is_named_for() {
let fresh = render_text(&scene(Scene::SettingsFresh).1);
assert_eq!(
fresh.matches("the default").count(),
10,
"every key outside [interpreters] reads the default: {fresh:?}"
);
assert!(
fresh.matches("shep.toml ").count() > 0,
"the starter interpreters are declared: {fresh:?}"
);
let set = render_text(&scene(Scene::SettingsSet).1);
assert!(set.contains("shep.toml"), "some scalars are declared");
assert!(set.contains("the default"), "and some are not: {set:?}");
let confirm = render_text(&scene(Scene::SettingsConfirm).1);
assert!(confirm.contains("shep daemon reload"), "got: {confirm:?}");
assert!(confirm.contains("SHEP_LOG_LEVEL"), "got: {confirm:?}");
assert!(confirm.contains("--log-level"), "got: {confirm:?}");
let typing = render_text(&scene(Scene::SettingsTyping).1);
assert!(
typing.contains("editing daemon.socket"),
"names the field being typed: {typing:?}"
);
assert!(
!typing.contains("filter "),
"must not read as the dashboard's own filter box: {typing:?}"
);
let dogs = render_text(&scene(Scene::SettingsDogs).1);
assert!(
dog_row_for(&dogs, "otel")
.is_some_and(|row| row.contains("no") && row.contains("online")),
"otel: disabled in the file, running: {dogs:?}"
);
assert!(
dog_row_for(&dogs, "ledger")
.is_some_and(|row| row.contains("yes") && row.contains("not running")),
"ledger: enabled, absent from the flock: {dogs:?}"
);
assert!(
dog_row_for(&dogs, "bark")
.is_some_and(|row| row.contains("yes") && row.contains("silent")),
"bark: enabled, running, never handshook: {dogs:?}"
);
let narrow = render_text(&scene(Scene::SettingsNarrow).1);
assert!(
narrow.contains("shep.toml"),
"the scalar rows keep SOURCE: {narrow:?}"
);
assert!(
!narrow.contains("needs shep daemon reload"),
"and lose the apply cost: {narrow:?}"
);
assert!(
dog_row_for(&narrow, "otel").is_some_and(|row| row.contains("online")),
"the dog header rows keep RUNNING: {narrow:?}"
);
assert!(!narrow.contains("built-in"), "and loses SOURCE: {narrow:?}");
let short = render_text(&scene(Scene::SettingsShort).1);
assert!(
short.contains("... 12 above"),
"the marker names how many rows are off the top: {short:?}"
);
assert!(
short
.lines()
.any(|line| line.starts_with("> bark")
|| line.starts_with('>') && line.contains("bark")),
"the cursor's own row is drawn: {short:?}"
);
assert!(
!short.contains("log_level"),
"and the rows above it are the ones that went: {short:?}"
);
assert!(
short.contains("interpreters") && short.contains("[dogs]"),
"what survives is the tail of the tree and the dog rows, caption and all: {short:?}"
);
}
#[test]
#[cfg(unix)] fn every_edit_pane_scene_draws_the_panel_it_is_named_for() {
let edit_pane = render_text(&scene(Scene::EditPane).1);
let title = edit_pane
.lines()
.find(|line| line.contains("(sheep config)"))
.expect("the title band");
assert!(
!title.contains("edit"),
"fresh, so the title names no edit count: {title:?}"
);
let header = edit_pane
.lines()
.find(|line| line.contains("FIELD") && line.contains("VALUE"))
.expect("the field list header");
assert!(
header.contains("LANDS"),
"wide enough for the cost column: {header:?}"
);
assert!(
edit_pane.contains("FOCUSED"),
"the explanation panel names the focused field: {edit_pane:?}"
);
let edit_pane_edited = render_text(&scene(Scene::EditPaneEdited).1);
let edited_title = edit_pane_edited
.lines()
.find(|line| line.contains("(sheep config)"))
.expect("the title band");
assert!(
edited_title.contains("2 edits"),
"two edits filed: {edited_title:?}"
);
let pending_section = edit_pane_edited
.lines()
.skip_while(|line| !line.trim_start().starts_with("pending edits"))
.take(3)
.collect::<Vec<_>>()
.join("\n");
assert!(
pending_section.contains("cwd") && pending_section.contains("max_memory"),
"both filed edits are named under the pending edits section: {pending_section:?}"
);
let squeezed = render_text(&scene(Scene::EditPaneSqueezed).1);
let squeezed_header = squeezed
.lines()
.find(|line| line.contains("FIELD") && line.contains("VALUE"))
.expect("the field list header");
assert!(
!squeezed_header.contains("LANDS"),
"LANDS gives way to the panel at 120 columns: {squeezed_header:?}"
);
assert!(
squeezed.contains("FOCUSED"),
"the panel still draws at 120 columns: {squeezed:?}"
);
let narrow_edit = render_text(&scene(Scene::EditPaneNarrow).1);
let narrow_edit_header = narrow_edit
.lines()
.find(|line| line.contains("FIELD") && line.contains("VALUE"))
.expect("the field list header");
assert!(
narrow_edit_header.contains("LANDS"),
"nothing else carries cost at 88 columns, so LANDS is back: {narrow_edit_header:?}"
);
assert!(
!narrow_edit.contains("FOCUSED"),
"the panel does not draw at 88 columns: {narrow_edit:?}"
);
}
#[test]
#[cfg(unix)] fn every_sheep_dogs_scene_shows_its_own_state_and_no_secret() {
const TOKEN: &str = "sk-live-51Hx9Qa";
const LEGACY_CREDENTIAL: &str = "hunter2";
let row_scene = render_text(&scene(Scene::SheepDogsRow).1);
assert!(
tab_dog_row_for(&row_scene, "deploy")
.is_some_and(|row| row.contains('-') && row.contains("(no table)")),
"deploy is unset: {row_scene:?}"
);
assert!(
tab_dog_row_for(&row_scene, "jobs")
.is_some_and(|row| row.contains("set") && row.contains("concurrency")),
"jobs is set, by key name: {row_scene:?}"
);
assert!(
tab_dog_row_for(&row_scene, "legacy")
.is_some_and(|row| row.contains("read-only") && row.contains("url")),
"legacy is read-only, by key name: {row_scene:?}"
);
for dog in ["deploy", "jobs", "legacy"] {
assert!(
tab_dog_row_for(&row_scene, dog).is_some_and(|row| row.contains('\u{25b8}')),
"{dog} is collapsed: {row_scene:?}"
);
}
assert!(!row_scene.contains("\u{25be} hours"), "no table is drawn");
assert!(!row_scene.contains(TOKEN), "{row_scene:?}");
assert!(!row_scene.contains(LEGACY_CREDENTIAL), "{row_scene:?}");
let list = render_text(&scene(Scene::SheepDogsList).1);
assert!(
tab_dog_row_for(&list, "jobs").is_some_and(|row| row.contains('\u{25be}')),
"jobs is open: {list:?}"
);
for dog in ["deploy", "legacy"] {
assert!(
tab_dog_row_for(&list, dog).is_some_and(|row| row.contains('\u{25b8}')),
"{dog} is collapsed: {list:?}"
);
}
assert!(list.contains("\u{25be} hours"), "jobs's table is inline");
assert!(!list.contains(TOKEN), "{list:?}");
assert!(!list.contains(LEGACY_CREDENTIAL), "{list:?}");
let remove = render_text(&scene(Scene::SheepDogsRemove).1);
assert!(
remove.contains("remove api's jobs table? enter confirms"),
"{remove:?}"
);
assert!(!remove.contains(TOKEN), "{remove:?}");
assert!(!remove.contains(LEGACY_CREDENTIAL), "{remove:?}");
let table = render_text(&scene(Scene::SheepDogTable).1);
for drawn in [
"\u{25be} hours",
"end",
"start",
"merge",
"\u{25be} models",
"\u{25be} worker",
"model",
"token",
] {
assert!(table.contains(drawn), "{drawn} is its own row: {table:?}");
}
let token_row = table
.lines()
.find(|line| line.starts_with('>'))
.expect("the cursor is on the secret's own row");
assert!(
token_row.contains("token") && token_row.contains("<set>"),
"{token_row:?}"
);
assert!(!table.contains(TOKEN), "{table:?}");
assert!(!table.contains(LEGACY_CREDENTIAL), "{table:?}");
}
#[test]
#[cfg(unix)] fn the_sheep_dog_union_scene_opens_one_union_onto_its_arm() {
let text = render_text(&scene(Scene::SheepDogUnion).1);
for drawn in [
"\u{25be} implement",
"kind = path",
"\u{25b8} review",
"kind = plugin",
"\u{25b8} retro",
"(unset)",
"skills/mine",
"A skill kept in the repo.",
] {
assert!(text.contains(drawn), "{drawn} is drawn: {text:?}");
}
let cursor_row = text
.lines()
.find(|line| line.starts_with('>'))
.expect("the cursor is on a row");
assert!(
cursor_row.contains("path") && cursor_row.contains("skills/mine"),
"{cursor_row:?}"
);
assert!(!text.contains('\u{1f}'), "no edit key reaches the screen");
}
#[test]
#[cfg(unix)] fn the_dog_entries_scenes_count_name_and_mask() {
for which in [Scene::DogEntries, Scene::DogEntriesNarrow] {
let text = render_text(&scene(which).1);
for drawn in [
"3 entries",
"2 entries",
"\u{25be} clients",
"\u{25b8} models",
"alpha",
"b-2",
"#2",
] {
assert!(text.contains(drawn), "{which:?} lacks {drawn}: {text:?}");
}
let key_row = text
.lines()
.find(|line| line.starts_with('>'))
.expect("the cursor is on the credential's own row");
assert!(
key_row.contains("key") && key_row.contains("<set>"),
"{which:?}: {key_row:?}"
);
for credential in ["hunter2", "hunter3", "hunter4"] {
assert!(!text.contains(credential), "{which:?} drew {credential}");
}
}
}
#[test]
#[cfg(unix)] fn the_dog_entry_typing_scene_draws_the_text_being_typed() {
let text = render_text(&scene(Scene::DogEntryTyping).1);
let name_row = text
.lines()
.find(|line| line.starts_with('>'))
.expect("the cursor is on the name row");
assert!(
name_row.contains("name") && name_row.contains("beta\u{258f}"),
"{name_row:?}"
);
assert!(text.contains("FOCUSED clients[1].name"), "{text}");
for credential in ["hunter2", "hunter3", "hunter4"] {
assert!(!text.contains(credential), "drew {credential}");
}
}
#[test]
#[cfg(unix)] fn the_dog_entries_new_scene_draws_the_key_editor_on_the_plus_new_row() {
let text = render_text(&scene(Scene::DogEntriesNew).1);
let new_row = text
.lines()
.find(|line| line.starts_with('>'))
.expect("the cursor is on the + new row");
assert!(new_row.contains("+ new"), "{new_row:?}");
assert!(text.contains("new models key"), "{text}");
assert!(text.contains("gpt-5\u{258f}"), "{text}");
assert!(!text.contains("[1 edit"), "{text}");
for credential in ["hunter2", "hunter3", "hunter4"] {
assert!(!text.contains(credential), "drew {credential}");
}
}
#[test]
#[cfg(unix)] fn every_close_dialog_scene_draws_the_heading_it_is_named_for() {
let close_dialog = render_text(&scene(Scene::CloseDialog).1);
let close_dialog_lines: Vec<&str> = close_dialog.lines().collect();
let border_row = close_dialog_lines
.iter()
.position(|line| line.contains('▛')) .expect("the box border draws at 160 columns");
let heading = close_dialog_lines[border_row + 1];
assert!(
heading.contains("EDITS NEED A RESPAWN") && heading.contains("FIELD ALREADY"),
"the heading names both the unsent and the parked half: {heading:?}"
);
let close_dialog_floor = render_text(&scene(Scene::CloseDialogFloor).1);
let floor_lines: Vec<&str> = close_dialog_floor.lines().collect();
let floor_border_row = floor_lines
.iter()
.position(|line| line.contains('▛')) .expect("the box border draws at the floor");
assert!(
floor_lines[floor_border_row].contains('▜'), "the border's top row closes at the floor: {:?}",
floor_lines[floor_border_row]
);
let close_dialog_narrow = render_text(&scene(Scene::CloseDialogNarrow).1);
for glyph in ['▛', '▜', '▙', '▟', '▐', '▀', '▄', '▌'] {
assert!(
!close_dialog_narrow.contains(glyph),
"no border glyph {glyph:?} draws one column under the floor: {close_dialog_narrow:?}"
);
}
let close_dialog_parked = render_text(&scene(Scene::CloseDialogParked).1);
let parked_lines: Vec<&str> = close_dialog_parked.lines().collect();
let parked_border_row = parked_lines
.iter()
.position(|line| line.contains('▛'))
.expect("the box border draws at 160 columns");
let parked_heading = parked_lines[parked_border_row + 1];
assert!(
parked_heading.contains("FIELD ALREADY") && !parked_heading.contains("EDIT"),
"the heading names the parked half alone, with no unsent count: {parked_heading:?}"
);
}
#[test]
fn the_secrets_scene_shows_a_revealed_row_not_a_mask() {
let text = render_text(&scene(Scene::Secrets).1);
let revealed = text
.lines()
.find(|line| line.contains("DB_PASSWORD"))
.expect("the secrets scene draws a DB_PASSWORD row");
assert!(
revealed.contains("hunter2-not-really"),
"DB_PASSWORD's row should show the revealed plaintext, not a mask: {revealed:?}"
);
assert!(
!revealed.contains("bytes"),
"the VALUE cell should show plaintext, not a masked byte count: {revealed:?}"
);
assert!(
revealed.contains("visible"),
"a revealed row should show a countdown, not the unrevealed dash: {revealed:?}"
);
}
#[test]
fn the_secrets_scene_does_not_deny_the_roll_its_readers_came_from() {
let text = render_text(&scene(Scene::Secrets).1);
let read_by = text
.lines()
.find(|line| line.contains("DB_PASSWORD"))
.expect("the secrets scene draws a DB_PASSWORD row");
assert!(
read_by.contains("1 (1 online)"),
"the scene's own row names a reader: {read_by:?}"
);
assert!(
!text.contains("no muster roll yet"),
"and so cannot also say the roll it came from was never written"
);
assert!(
text.contains("READ BY as of the roll"),
"it says how old the roll is instead"
);
}
}