use clap::{CommandFactory as _, Parser as _};
use crate::cli::{Cli, Command};
use crate::commands::run_target::{RunTarget, classify_bare};
fn external_token(argv: &[&str]) -> String {
let cli = Cli::try_parse_from(argv).expect("bare token must parse");
match cli.command.expect("a token is not the no-subcommand case") {
Command::External(tokens) => tokens.first().cloned().expect("one token"),
other => panic!("{argv:?} must reach the External arm, got {other:?}"),
}
}
#[test]
fn cli_parses_bare_github_url() {
let token = external_token(&["tm", "https://github.com/bobmatnyc/mcp-a-protocol"]);
let target = classify_bare(&token)
.expect("a github URL is a repo, not a typo")
.expect("it resolves");
assert_eq!(
target,
RunTarget::Repo {
owner: "bobmatnyc".into(),
repo: "mcp-a-protocol".into(),
clone_url: "https://github.com/bobmatnyc/mcp-a-protocol".into(),
account: None,
}
);
}
#[test]
fn cli_parses_bare_owner_repo() {
let token = external_token(&["tm", "bobmatnyc/mcp-a-protocol"]);
let target = classify_bare(&token)
.expect("shorthand is a repo")
.expect("it resolves");
assert_eq!(
target,
RunTarget::Repo {
owner: "bobmatnyc".into(),
repo: "mcp-a-protocol".into(),
clone_url: "https://github.com/bobmatnyc/mcp-a-protocol".into(),
account: None,
}
);
}
#[test]
fn cli_bare_infers_abbreviated_subcommand_over_external() {
for (argv, label) in [
(["tm", "doc"], "doctor"),
(["tm", "heal"], "health"),
(["tm", "rest"], "restart"),
(["tm", "instal"], "install"),
] {
let cli = Cli::try_parse_from(argv).expect("abbreviation must parse");
let command = cli.command.expect("an abbreviation names a subcommand");
assert!(
!matches!(command, Command::External(_)),
"'{}' must infer {label}, not fall through to External — got {command:?}",
argv[1]
);
}
assert!(matches!(
Cli::try_parse_from(["tm", "heal"])
.expect("parses")
.command
.expect("has a command"),
Command::Health
));
}
#[test]
fn cli_bare_ambiguous_prefix_is_not_a_repo() {
let token = external_token(&["tm", "sta"]);
assert_eq!(token, "sta");
assert!(
classify_bare(&token).is_none(),
"an ambiguous prefix must reach the usage-error path, not a managed run"
);
}
#[test]
fn cli_bare_unknown_subcommand_is_not_a_repo() {
for typo in ["statuss", "sessionz", "notacommand"] {
let token = external_token(&["tm", typo]);
assert_eq!(token, typo);
assert!(
classify_bare(&token).is_none(),
"'{typo}' has no '/' or ':', so it must fall back to the usage error"
);
}
}
#[test]
fn cli_bare_retired_subcommand_is_not_a_repo() {
let token = external_token(&["tm", "coordinator-tui"]);
assert_eq!(token, "coordinator-tui");
assert!(
classify_bare(&token).is_none(),
"a retired subcommand must never become a managed run"
);
}
#[test]
fn cli_bare_non_repo_url_surfaces_the_descriptive_error() {
for (bad, expected) in [
("https://example.com/", "names a host"),
(
"https://github.com/bobmatnyc/trusty-tools/pull/6441",
"points inside a repository",
),
] {
let token = external_token(&["tm", bad]);
let err = classify_bare(&token)
.expect("a URL is repo-shaped, so it is never the typo path")
.expect_err("but it does not name a repository");
assert!(
err.to_string().contains(expected),
"rejection of '{bad}' must say '{expected}', got: {err}"
);
}
}
#[test]
fn reject_unknown_subcommand_rebuild_reproduces_claps_error() {
let argv = ["tm".to_string(), "sta".to_string()];
let flag_flip = Cli::command().allow_external_subcommands(false);
assert!(
flag_flip.try_get_matches_from(argv.clone()).is_ok(),
"if this now ERRORS, clap has changed and the rebuild may be simplifiable"
);
let err = crate::commands::run_target::without_external_catch_all()
.try_get_matches_from(argv)
.expect_err("an ambiguous prefix must not parse");
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidSubcommand);
let rendered = err.to_string();
for fragment in [
"unrecognized subcommand 'sta'",
"tip: some similar subcommands exist",
"'status'",
"'start'",
"Usage: tm [OPTIONS] [COMMAND]",
] {
assert!(
rendered.contains(fragment),
"the rebuilt error must still say '{fragment}':\n{rendered}"
);
}
}
#[test]
fn command_name_matches_the_rebuild() {
assert_eq!(
Cli::command().get_name(),
crate::commands::run_target::CLI_NAME
);
}