use super::{Decided, FAILED, REFUSED, Verdict, run, usage, version};
fn argv(words: &[&str]) -> Vec<String> {
words.iter().map(|w| (*w).to_string()).collect()
}
fn said(words: &[&str]) -> Verdict {
match run(argv(words)) {
Decided::Say(verdict) => verdict,
_ => panic!("{words:?} decided to do something rather than say something"),
}
}
#[test]
fn version_names_the_crate_and_its_version() {
assert_eq!(version(), format!("thrall {}", env!("CARGO_PKG_VERSION")));
}
#[test]
fn usage_leads_with_the_version_line_then_says_what_thrall_is() {
let text = usage();
assert!(
text.starts_with(&version()),
"usage did not lead with the version: {text}"
);
assert!(
text.contains("thrall is the foot"),
"usage did not say what thrall is"
);
assert!(
text.contains("never speaks first"),
"usage dropped the dial-in invariant"
);
}
#[test]
fn usage_names_the_verb_and_what_it_reads() {
let text = usage();
assert!(text.contains("thrall run"), "{text}");
assert!(text.contains("tools.json"), "{text}");
assert!(text.contains("wire/workspaces/"), "{text}");
assert!(text.contains("dialled again"), "{text}");
}
#[test]
fn ok_carries_the_success_code() {
let v = Verdict::ok("said".to_string());
assert_eq!(v.code, 0);
assert_eq!(v.text, "said");
}
#[test]
fn every_refusal_names_what_it_refused_and_still_teaches() {
let v = Verdict::refused("that is not a verb".to_string());
assert_eq!(v.code, REFUSED);
assert_eq!(v.text, format!("thrall: that is not a verb\n\n{}", usage()));
}
#[test]
fn a_failure_says_only_what_happened() {
let v = Verdict::failed("this box holds no channel".to_string());
assert_eq!(v.code, FAILED);
assert_eq!(v.text, "thrall: this box holds no channel");
assert!(!v.text.contains("usage:"), "{}", v.text);
}
#[test]
fn both_version_spellings_print_the_version_and_succeed() {
for spelling in ["--version", "-V"] {
let v = said(&[spelling]);
assert_eq!(v.code, 0, "{spelling} did not succeed");
assert_eq!(v.text, version(), "{spelling} printed something else");
}
}
#[test]
fn both_help_spellings_print_the_usage_and_succeed() {
for spelling in ["--help", "-h"] {
let v = said(&[spelling]);
assert_eq!(v.code, 0, "{spelling} did not succeed");
assert_eq!(v.text, usage(), "{spelling} printed something else");
}
}
#[test]
fn the_example_document_is_printed_and_succeeds() {
let v = said(&["--example-tools"]);
assert_eq!(v.code, 0);
assert!(v.text.starts_with('['), "not a JSON document: {}", v.text);
assert!(
usage().contains("--example-tools"),
"the usage no longer names the flag, so nobody finds it"
);
}
#[test]
fn the_verb_decides_to_serve() {
assert!(matches!(run(argv(&["run"])), Decided::Serve));
}
#[test]
fn the_bridge_takes_the_tool_before_the_dashes_and_the_server_after() {
match run(argv(&["mcp", "fetch", "--", "uvx", "mcp-server-fetch"])) {
Decided::Bridge { tool, server } => {
assert_eq!(tool, "fetch");
assert_eq!(
server,
vec!["uvx".to_string(), "mcp-server-fetch".to_string()]
);
}
_ => panic!("mcp did not decide to bridge"),
}
}
#[test]
fn pin_is_the_operator_verb_and_carries_only_the_server() {
match run(argv(&["mcp", "pin", "--", "uvx", "mcp-server-fetch"])) {
Decided::Pin { server } => {
assert_eq!(
server,
vec!["uvx".to_string(), "mcp-server-fetch".to_string()]
);
}
_ => panic!("mcp pin did not decide to pin"),
}
}
#[test]
fn a_tool_named_pin_is_the_operator_verb_and_the_usage_says_so() {
assert!(matches!(
run(argv(&["mcp", "pin", "--", "a-server"])),
Decided::Pin { .. }
));
assert!(
usage().contains("`pin` is therefore a tool"),
"the usage does not warn that the name is taken: {}",
usage()
);
}
#[test]
fn an_mcp_without_a_tool_and_a_server_argv_refuses_and_shows_the_shape() {
for words in [
vec!["mcp"],
vec!["mcp", "fetch"],
vec!["mcp", "fetch", "--"],
vec!["mcp", "--", "uvx"],
] {
let v = said(&words);
assert_eq!(v.code, REFUSED, "{words:?}");
assert!(
v.text.contains("thrall mcp fetch -- uvx mcp-server-fetch"),
"{words:?}: {}",
v.text
);
}
}
#[test]
fn a_bare_invocation_refuses_and_names_the_verb() {
let v = said(&[]);
assert_eq!(v.code, REFUSED);
assert!(v.text.contains("`thrall run` is the verb"), "{}", v.text);
assert!(
v.text.contains("usage: thrall"),
"a refusal must still teach: {}",
v.text
);
}
#[test]
fn an_unrecognised_argument_refuses_and_quotes_every_word_of_it() {
let v = said(&["seat", "--ws", "Example"]);
assert_eq!(v.code, REFUSED);
assert!(
v.text.contains("unrecognised argument: seat --ws Example"),
"the refusal did not name what it refused: {}",
v.text
);
assert!(v.text.contains("usage: thrall"), "{}", v.text);
}
#[test]
fn a_recognised_word_with_extra_words_is_not_recognised() {
for extra in [["--version", "--now"], ["run", "--now"]] {
let v = said(&extra);
assert_eq!(v.code, REFUSED);
assert!(
v.text
.contains(&format!("unrecognised argument: {}", extra.join(" "))),
"{}",
v.text
);
}
}