use super::*;
#[test]
fn classify_sorts_alias_from_repo() {
for alias in ["my-project", "trusty-tools", "a", "x.y_z-1"] {
assert_eq!(
classify_run_target(alias).expect("alias classifies"),
RunTarget::Alias(alias.to_string()),
"{alias} must route to the standalone driver"
);
}
let target = classify_run_target("bobmatnyc/trusty-tools").expect("shorthand classifies");
assert!(
matches!(target, RunTarget::Repo { .. }),
"owner/repo must route to the managed cold start, got {target:?}"
);
}
#[test]
fn classify_resolves_shorthand() {
let target = classify_run_target("bobmatnyc/trusty-tools").expect("classifies");
assert_eq!(
target,
RunTarget::Repo {
owner: "bobmatnyc".into(),
repo: "trusty-tools".into(),
clone_url: "https://github.com/bobmatnyc/trusty-tools".into(),
account: None,
}
);
}
#[test]
fn classify_resolves_full_urls() {
let ssh = classify_run_target("git@github.com:bobmatnyc/trusty-tools.git").expect("ssh");
assert_eq!(
ssh,
RunTarget::Repo {
owner: "bobmatnyc".into(),
repo: "trusty-tools".into(),
clone_url: "git@github.com:bobmatnyc/trusty-tools.git".into(),
account: None,
}
);
let https = classify_run_target("https://gitlab.com/acme/widget.git").expect("https");
assert_eq!(
https,
RunTarget::Repo {
owner: "acme".into(),
repo: "widget".into(),
clone_url: "https://gitlab.com/acme/widget.git".into(),
account: None,
}
);
}
#[test]
fn absolute_path_derives_identity_from_the_last_two_segments() {
let target = classify_run_target("/Users/me/code/app").expect("absolute path classifies");
assert_eq!(
target,
RunTarget::Repo {
owner: "code".into(),
repo: "app".into(),
clone_url: "/Users/me/code/app".into(),
account: None,
}
);
}
#[test]
fn shorthand_identity_agrees_with_url_identity() {
for spec in [
"bobmatnyc/trusty-tools",
"Acme/Cool_App",
"123/repo",
"a-b/c.d",
] {
let url = crate::commands::register_args::resolved_url(spec).expect("resolves");
let from_shorthand = parse_owner_repo(spec).expect("shorthand parses");
let from_url = parse_github_path(&url).expect("url parses");
assert_eq!(
from_shorthand, from_url,
"identity for '{spec}' must not depend on which parser saw it"
);
}
}
#[test]
fn classify_rejects_browser_pastes_and_paths() {
for (bad, expected) in [
(
"https://github.com/bobmatnyc/trusty-tools/pull/4990",
"points inside a repository",
),
(
"https://github.com/bobmatnyc/trusty-tools/tree/main",
"points inside a repository",
),
("./some/dir", "is a relative path"),
("../sibling", "is a relative path"),
("https://github.com", "names a host"),
] {
let err = match classify_run_target(bad) {
Err(e) => e.to_string(),
Ok(t) => panic!("'{bad}' must be refused before any network call, got {t:?}"),
};
assert!(
err.contains(expected),
"rejection of '{bad}' must say '{expected}', got: {err}"
);
}
}
#[test]
fn classify_bare_accepts_repo_shapes() {
for spec in [
"bobmatnyc/mcp-a-protocol",
"https://github.com/bobmatnyc/mcp-a-protocol",
"https://github.com/bobmatnyc/mcp-a-protocol.git",
"git@github.com:bobmatnyc/mcp-a-protocol.git",
] {
let bare = classify_bare(spec)
.unwrap_or_else(|| panic!("'{spec}' is a repo shape, not a typo"))
.unwrap_or_else(|e| panic!("'{spec}' must resolve: {e}"));
assert_eq!(
bare,
classify_run_target(spec).expect("tm run accepts it too"),
"'{spec}' must mean the same thing bare as it does after `tm run`"
);
assert!(matches!(bare, RunTarget::Repo { .. }));
}
}
#[test]
fn classify_bare_declines_subcommand_typos() {
for not_a_repo in [
"statuss",
"sessionz",
"instal",
"notacommand",
"",
" ",
"./some/dir",
"../sibling",
] {
assert!(
classify_bare(not_a_repo).is_none(),
"'{not_a_repo}' must fall through to the usage-error path, not become a target"
);
}
}
#[test]
fn classify_bare_surfaces_resolved_url_errors() {
for (bad, expected) in [
("https://example.com/", "names a host"),
(
"https://github.com/bobmatnyc/trusty-tools/issues",
"points inside a repository",
),
] {
let err = classify_bare(bad)
.unwrap_or_else(|| panic!("'{bad}' 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 classify_rejects_empty_target() {
for empty in ["", " ", "\t"] {
let err = classify_run_target(empty).expect_err("empty must be refused");
assert!(
err.to_string().contains("needs a target"),
"message must name the remedy: {err}"
);
}
}
#[test]
fn classify_resolves_account_flag() {
let target =
classify_run_target_with_account("bobmatnyc/trusty-tools", Some("bob-duetto")).unwrap();
assert_eq!(
target,
RunTarget::Repo {
owner: "bobmatnyc".into(),
repo: "trusty-tools".into(),
clone_url: "https://github.com/bobmatnyc/trusty-tools".into(),
account: Some("bob-duetto".into()),
}
);
}
#[test]
fn classify_resolves_embedded_account() {
let target =
classify_run_target_with_account("bob-duetto@duettoresearch/poc-hotel-supply", None)
.unwrap();
assert_eq!(
target,
RunTarget::Repo {
owner: "duettoresearch".into(),
repo: "poc-hotel-supply".into(),
clone_url: "https://github.com/duettoresearch/poc-hotel-supply".into(),
account: Some("bob-duetto".into()),
}
);
}
#[test]
fn classify_rejects_conflicting_account() {
let err = classify_run_target_with_account(
"bob-duetto@duettoresearch/poc-hotel-supply",
Some("bobmatnyc"),
)
.expect_err("conflicting accounts must be refused");
assert!(
err.to_string().contains("conflicting account selection"),
"{err}"
);
}
#[test]
fn classify_accepts_agreeing_account_flag_and_shorthand() {
let target = classify_run_target_with_account(
"bob-duetto@duettoresearch/poc-hotel-supply",
Some("bob-duetto"),
)
.unwrap();
assert_eq!(
target,
RunTarget::Repo {
owner: "duettoresearch".into(),
repo: "poc-hotel-supply".into(),
clone_url: "https://github.com/duettoresearch/poc-hotel-supply".into(),
account: Some("bob-duetto".into()),
}
);
}
#[test]
fn classify_bare_accepts_account_flag_and_shorthand() {
let via_flag =
classify_bare_with_account("bobmatnyc/trusty-tools", Some("bob-duetto")).unwrap();
assert_eq!(
via_flag.unwrap(),
classify_run_target_with_account("bobmatnyc/trusty-tools", Some("bob-duetto")).unwrap()
);
let via_shorthand =
classify_bare_with_account("bob-duetto@duettoresearch/poc-hotel-supply", None).unwrap();
assert!(matches!(
via_shorthand.unwrap(),
RunTarget::Repo {
account: Some(a),
..
} if a == "bob-duetto"
));
}
#[test]
fn classify_bare_with_no_account_is_unchanged() {
let bare = classify_bare("bobmatnyc/trusty-tools").unwrap().unwrap();
assert!(matches!(bare, RunTarget::Repo { account: None, .. }));
}
fn toks(items: &[&str]) -> Vec<String> {
items.iter().map(ToString::to_string).collect()
}
#[test]
fn bare_form_lifts_a_trailing_user_flag() {
let (rest, account) = split_trailing_account(&toks(&[
"https://github.com/duettoresearch/jev-matching",
"--user",
"bob-duetto",
]))
.expect("a trailing --user must be lifted, not refused");
assert_eq!(
rest,
toks(&["https://github.com/duettoresearch/jev-matching"])
);
assert_eq!(account.as_deref(), Some("bob-duetto"));
}
#[test]
fn bare_form_lifts_an_inline_account_value() {
let (rest, account) =
split_trailing_account(&toks(&["acme/widget", "--account=bobmatnyc"])).expect("lifted");
assert_eq!(rest, toks(&["acme/widget"]));
assert_eq!(account.as_deref(), Some("bobmatnyc"));
}
#[test]
fn bare_form_rejects_a_trailing_flag_with_no_value() {
let err = split_trailing_account(&toks(&["acme/widget", "--user"]))
.expect_err("a valueless flag must refuse");
assert!(err.to_string().contains("needs a gh login"), "{err}");
}
#[test]
fn bare_form_leaves_unrelated_tokens_alone() {
let (rest, account) =
split_trailing_account(&toks(&["acme/widget", "extra"])).expect("no flag present");
assert_eq!(rest, toks(&["acme/widget", "extra"]));
assert_eq!(account, None);
}
#[test]
fn bare_form_refuses_two_different_accounts_around_the_repository() {
let err = reconcile_bare_account(Some("bobmatnyc".into()), Some("bob-duetto".into()))
.expect_err("a conflict must refuse");
assert!(err.to_string().contains("conflicting account"), "{err}");
assert_eq!(
reconcile_bare_account(Some("bob-duetto".into()), Some("bob-duetto".into()))
.expect("agreeing values collapse")
.as_deref(),
Some("bob-duetto")
);
}
#[test]
fn bare_form_rejects_an_empty_account_value() {
for tokens in [
&["acme/widget", "--user="][..],
&["acme/widget", "--account="][..],
&["acme/widget", "--user", ""][..],
&["acme/widget", "--account", " "][..],
&["acme/widget", "--user= "][..],
] {
let err = split_trailing_account(&toks(tokens))
.expect_err("an empty account value must refuse, not fall back to the global account");
assert!(
err.to_string().contains("needs a gh login"),
"{tokens:?}: {err}"
);
}
}
#[test]
fn bare_form_trailing_user_reaches_the_repo_target() {
use clap::Parser;
let cli = crate::cli::Cli::try_parse_from([
"tm",
"https://github.com/duettoresearch/jev-matching",
"--user",
"bob-duetto",
])
.expect("the bare form parses");
let Some(crate::cli::Command::External(tokens)) = cli.command else {
panic!("the bare form must reach the external catch-all");
};
let target = resolve_external(&tokens, cli.account)
.expect("a trailing --user must resolve, not refuse")
.expect("a repository URL is repo-shaped");
let RunTarget::Repo {
owner,
repo,
account,
..
} = target
else {
panic!("expected a repository target, got {target:?}");
};
assert_eq!(
(owner.as_str(), repo.as_str()),
("duettoresearch", "jev-matching")
);
assert_eq!(account.as_deref(), Some("bob-duetto"));
}
#[test]
fn bare_form_lifts_a_trailing_u_flag() {
for tokens in [
&["duettoresearch/duetto-blast-mfes", "--u", "bob-duetto"][..],
&["duettoresearch/duetto-blast-mfes", "--u=bob-duetto"][..],
] {
let (rest, account) = split_trailing_account(&toks(tokens)).expect("lifted");
assert_eq!(
rest,
toks(&["duettoresearch/duetto-blast-mfes"]),
"{tokens:?}"
);
assert_eq!(account.as_deref(), Some("bob-duetto"), "{tokens:?}");
}
}
fn selected_account(argv: &[&str]) -> Option<String> {
use clap::Parser;
let cli = crate::cli::Cli::try_parse_from(argv)
.unwrap_or_else(|e| panic!("{argv:?} must parse: {e}"));
match cli.command {
Some(crate::cli::Command::External(tokens)) => {
match resolve_external(&tokens, cli.account)
.unwrap_or_else(|e| panic!("{argv:?} must resolve: {e}"))
{
Some(RunTarget::Repo { account, .. }) => account,
other => panic!("{argv:?}: expected a repository target, got {other:?}"),
}
}
Some(crate::cli::Command::Run { .. } | crate::cli::Command::Register { .. }) => cli.account,
other => panic!("{argv:?}: unexpected command {other:?}"),
}
}
#[test]
fn every_account_spelling_selects_the_account_in_every_form_and_position() {
const REPO: &str = "duettoresearch/duetto-blast-mfes";
for flag in ["--account", "--user", "--u"] {
let cases: [Vec<&str>; 6] = [
vec!["tm", flag, "bob-duetto", REPO],
vec!["tm", REPO, flag, "bob-duetto"],
vec!["tm", "run", flag, "bob-duetto", REPO],
vec!["tm", "run", REPO, flag, "bob-duetto"],
vec!["tm", "register", flag, "bob-duetto", REPO],
vec!["tm", "register", REPO, flag, "bob-duetto"],
];
for argv in cases {
assert_eq!(
selected_account(&argv).as_deref(),
Some("bob-duetto"),
"{argv:?}"
);
}
}
}
#[test]
fn preflight_refuses_a_broken_accounts_table_naming_the_file() {
use trusty_mpm::core::gh_org_accounts::{AccountSource, OrgAccounts, ResolvedAccount};
let path = std::path::Path::new("/home/u/.trusty-mpm/config.toml");
let broken = OrgAccounts::from_toml("[accounts]\nduettoresearch = bob-duetto\n", path)
.expect_err("an unquoted login is a syntax error");
let origin = "https://github.com/duettoresearch/jev.git";
let err = preflight_verdict(origin, Err(broken.to_string()))
.expect_err("a broken table refuses the launch")
.to_string();
for needle in [origin, "/home/u/.trusty-mpm/config.toml", "not valid TOML"] {
assert!(err.contains(needle), "{needle}: {err}");
}
assert!(err.contains("No session was started"), "{err}");
let chosen = ResolvedAccount {
login: "bob-duetto".into(),
source: AccountSource::OrgMap,
};
preflight_verdict(origin, Ok(Some(chosen))).expect("a mapped account launches");
preflight_verdict(origin, Ok(None)).expect("the ambient identity launches");
}
#[test]
fn preflight_refusal_never_quotes_the_origin_token() {
const TOKEN: &str = "ghp_9124PreflightSyntheticToken000";
let origin = format!("https://octo:{TOKEN}@github.com/acme/widget.git");
let err = preflight_verdict(&origin, Err("no [accounts] row".to_string()))
.expect_err("an unresolved account refuses the launch")
.to_string();
assert!(!err.contains(TOKEN), "the token reached the refusal");
assert!(
err.contains("https://***@github.com/acme/widget.git"),
"{err}"
);
}
#[test]
fn a_token_in_the_url_never_reaches_the_identity_9124() {
for url in [
"https://qauser:SECRETQATOKEN2@example.invalid/x.git",
"https://qauser:SECRETQATOKEN2@example.invalid/org/x.git",
] {
let RunTarget::Repo {
owner,
repo,
account,
..
} = classify_run_target(url).unwrap()
else {
panic!("{url} is a repo");
};
let identity = format!("{owner}/{repo} {account:?}").to_ascii_lowercase();
for part in ["secretqatoken2", "qauser"] {
assert!(!identity.contains(part), "{url} -> {identity}");
}
assert_eq!(repo, "x");
}
}