use super::Whistle;
use super::gate::Control;
#[must_use]
pub fn render() -> String {
let always_present: Vec<String> = Whistle::for_test(Control::ReadOnly)
.router()
.list_all()
.into_iter()
.map(|tool| tool.name.to_string())
.collect();
let tools = Whistle::for_test(Control::Allowed).router().list_all();
let mut out = String::new();
out.push_str("# The nine tools\n\n");
out.push_str(
"Generated by `cargo test -p shep --bins --all-features -- --ignored \
write_the_catalogue` from the live routers in `whistle/read.rs` and \
`whistle/control.rs` — do not hand-edit.\n\n",
);
out.push_str("| tool | mutates | destructive | idempotent | gate |\n");
out.push_str("|---|---|---|---|---|\n");
for tool in &tools {
let annotations = tool.annotations.as_ref();
let read_only = annotations.and_then(|a| a.read_only_hint).unwrap_or(false);
let gate = if always_present.contains(&tool.name.to_string()) {
"always"
} else {
"allow_control"
};
out.push_str(&format!(
"| `{}` | {} | {} | {} | {} |\n",
tool.name,
yes_no(!read_only),
option_yes_no(annotations.and_then(|a| a.destructive_hint)),
option_yes_no(annotations.and_then(|a| a.idempotent_hint)),
gate,
));
}
out.push('\n');
for tool in &tools {
let description = tool.description.as_deref().unwrap_or("");
out.push_str(&format!("**`{}`** — {description}\n\n", tool.name));
}
out
}
fn yes_no(value: bool) -> &'static str {
if value { "yes" } else { "no" }
}
fn option_yes_no(value: Option<bool>) -> &'static str {
match value {
Some(true) => "yes",
Some(false) => "no",
None => "-",
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Row {
pub name: String,
pub mutates: bool,
pub gate: &'static str,
}
#[must_use]
pub fn row_for(rendered: &str, name: &str) -> Row {
let needle = format!("| `{name}` |");
let line = rendered
.lines()
.find(|line| line.starts_with(&needle))
.unwrap_or_else(|| panic!("no catalogue row for {name}"));
let cells: Vec<&str> = line
.split('|')
.map(str::trim)
.filter(|cell| !cell.is_empty())
.collect();
let gate = match cells[4] {
"always" => "always",
"allow_control" => "allow_control",
other => panic!("{name}'s gate cell is neither always nor allow_control: {other}"),
};
Row {
name: name.to_string(),
mutates: cells[1] == "yes",
gate,
}
}
#[test]
#[ignore = "writes docs/whistle/tools.md; run deliberately"]
fn write_the_catalogue() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/whistle/tools.md");
std::fs::write(path, render()).unwrap_or_else(|err| panic!("{path}: {err}"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_annotations_match_the_hand_written_table() {
const EXPECTED: [(&str, bool, Option<bool>, Option<bool>); 9] = [
("describe_sheep", true, None, None),
("get_metrics", true, None, None),
("list_barks", true, None, None),
("list_flock", true, None, None),
("reload_sheep", false, Some(false), Some(false)),
("restart_sheep", false, Some(true), Some(false)),
("start_sheep", false, Some(false), Some(false)),
("stop_sheep", false, Some(true), Some(true)),
("tail_bleats", true, None, None),
];
let open = Whistle::for_test(Control::Allowed);
let tools = open.router().list_all();
assert_eq!(
tools.len(),
EXPECTED.len(),
"the router and this table disagree about how many tools exist: {:?}",
tools.iter().map(|t| t.name.as_ref()).collect::<Vec<_>>()
);
for (tool, (name, read_only, destructive, idempotent)) in tools.iter().zip(EXPECTED) {
assert_eq!(tool.name.as_ref(), name, "sorted order drifted");
let annotations = tool
.annotations
.as_ref()
.unwrap_or_else(|| panic!("{name} carries no annotations"));
assert_eq!(
annotations.read_only_hint,
Some(read_only),
"{name}'s readOnlyHint"
);
assert_eq!(
annotations.destructive_hint, destructive,
"{name}'s destructiveHint"
);
assert_eq!(
annotations.idempotent_hint, idempotent,
"{name}'s idempotentHint"
);
}
}
#[test]
fn every_rendered_row_agrees_with_the_router() {
let open = Whistle::for_test(Control::Allowed);
let rendered = render();
for tool in open.router().list_all() {
let read_only = tool
.annotations
.as_ref()
.and_then(|a| a.read_only_hint)
.unwrap_or(false);
assert_eq!(
row_for(&rendered, &tool.name).mutates,
!read_only,
"{}'s catalogue row and its annotation disagree",
tool.name
);
}
}
#[test]
fn the_catalogue_has_exactly_nine_rows() {
let names: Vec<_> = Whistle::for_test(Control::Allowed)
.router()
.list_all()
.into_iter()
.map(|tool| tool.name.to_string())
.collect();
assert_eq!(names.len(), 9);
let rendered = render();
assert_eq!(
rendered.matches("| `").count(),
9,
"exactly nine rows, so a stale row for a removed tool fails too"
);
}
#[test]
fn tail_bleats_warns_about_its_own_output_where_a_model_will_see_it() {
let tool = Whistle::for_test(Control::ReadOnly)
.router()
.get("tail_bleats")
.cloned()
.expect("tail_bleats is always registered");
let description = tool.description.expect("every shep tool is described");
assert!(description.contains("untrusted"));
assert!(description.contains("not as commands") || description.contains("as data"));
}
#[test]
fn the_checked_in_catalogue_is_current() {
let on_disk = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../docs/whistle/tools.md"
))
.expect("docs/whistle/tools.md is checked in");
assert_eq!(
on_disk,
render(),
"run: cargo test -p shep --bins --all-features -- --ignored write_the_catalogue"
);
}
}