use super::super::*;
use super::{Catalog, Quirked, Scripted, deps, number_of};
async fn run_with(answers: &[&str]) -> (Plan, Scripted) {
let mut console = Scripted::new(answers);
let plan = run(
&mut console,
deps(
&AuthStore::new(),
&Catalog::Unavailable,
&Quirked::Unavailable,
),
)
.await
.expect("the wizard completes");
assert!(
console.is_drained(),
"the script left answers unused, so this asserts against a flow that did not happen"
);
(plan, console)
}
#[tokio::test]
async fn accepting_every_default_configures_the_local_provider() {
let (plan, _) = run_with(&["", "", "", "", "", ""]).await;
assert_eq!(plan.choices.len(), 1);
assert_eq!(plan.choices[0].preset.key, "local");
assert_eq!(plan.hooks, HookKind::PrePush);
assert!(plan.gitignore, "the default answer is yes");
assert!(plan.new_keys.is_empty(), "local needs no key");
}
#[tokio::test]
async fn the_chosen_number_selects_that_preset() {
let (plan, _) = run_with(&[&number_of("zai"), "", "", "", "", "", ""]).await;
assert_eq!(plan.choices[0].preset.key, "zai");
assert_eq!(
plan.choices[0].endpoint(),
Some("https://api.z.ai/api/coding/paas/v4")
);
assert_eq!(plan.choices[0].model, "glm-5.3");
}
#[tokio::test]
async fn codex_selection_checks_subscription_readiness_before_asking_for_a_model() {
let (plan, console) = run_with(&[&number_of("codex"), "", "", "", ""]).await;
assert_eq!(plan.choices[0].preset.key, "codex");
assert_eq!(plan.choices[0].endpoint(), None);
assert!(plan.new_keys.is_empty());
assert!(
console
.transcript()
.contains("Codex CLI test-version is authenticated through ChatGPT"),
"got {}",
console.transcript()
);
}
#[tokio::test]
async fn codex_selection_stops_before_writing_a_plan_when_readiness_fails() {
fn unavailable() -> Result<crate::llm::codex::CodexStatus, String> {
Err("Codex CLI was not found; install it and run `codex login`".to_owned())
}
let mut console = Scripted::new(&[&number_of("codex")]);
let err = run(
&mut console,
Deps {
codex_status: &unavailable,
..deps(
&AuthStore::new(),
&Catalog::Unavailable,
&Quirked::Unavailable,
)
},
)
.await
.expect_err("a subscription backend that cannot authenticate is unusable");
let message = err.to_string();
assert!(message.contains("Codex CLI was not found"), "got {message}");
assert!(message.contains("`codex login`"), "got {message}");
assert!(console.is_drained());
}
#[tokio::test]
async fn an_out_of_range_number_is_re_asked_rather_than_accepted() {
let (plan, console) = run_with(&["99", "0", "not a number", "1", "", "", "", "", ""]).await;
assert_eq!(plan.choices[0].preset.key, presets::PRESETS[0].key);
assert!(
console.transcript().contains("Enter a number from 1 to"),
"the user was told what is valid: {}",
console.transcript()
);
}
#[tokio::test]
async fn an_overridden_model_and_endpoint_reach_the_choice() {
let (plan, _) = run_with(&["1", "http://elsewhere:9/v1", "some-model", "", "", ""]).await;
assert_eq!(plan.choices[0].endpoint(), Some("http://elsewhere:9/v1"));
assert_eq!(plan.choices[0].model, "some-model");
}
#[tokio::test]
async fn a_preset_with_no_default_endpoint_re_asks_until_one_is_given() {
let (plan, console) = run_with(&[
&number_of("custom"),
"",
" ",
"https://mine/v1",
"", "my-model", "", "", "", ])
.await;
assert_eq!(plan.choices[0].endpoint(), Some("https://mine/v1"));
assert!(
console.transcript().contains("Endpoint cannot be empty"),
"got {}",
console.transcript()
);
}
#[tokio::test]
async fn a_fallback_becomes_a_second_entry_in_chain_order() {
let (plan, _) = run_with(&[
"1",
"",
"", "y", &number_of("openrouter"),
"",
"", "", "", "",
"", ])
.await;
assert_eq!(plan.choices.len(), 2);
assert_eq!(plan.choices[0].preset.key, "local");
assert_eq!(plan.choices[1].preset.key, "openrouter");
}
#[tokio::test]
async fn a_third_provider_is_reachable_too() {
let (plan, _) = run_with(&[
"1", "", "", "y", "1", "", "", "y", "1", "", "", "n", "", "",
])
.await;
assert_eq!(plan.choices.len(), 3);
}
#[tokio::test]
async fn the_fallback_question_defaults_to_no() {
let (plan, _) = run_with(&["1", "", "", "", "", ""]).await;
assert_eq!(plan.choices.len(), 1);
}
#[tokio::test]
async fn each_hook_number_maps_to_its_own_kind() {
let table = [
("1", HookKind::PrePush),
("2", HookKind::PreCommit),
("3", HookKind::Both),
("4", HookKind::None),
];
for (answer, expected) in table {
let (plan, _) = run_with(&["1", "", "", "", answer, ""]).await;
assert_eq!(plan.hooks, expected, "answer {answer}");
}
}
#[tokio::test]
async fn an_invalid_hook_number_is_re_asked() {
let (plan, console) = run_with(&["1", "", "", "", "9", "2", ""]).await;
assert_eq!(plan.hooks, HookKind::PreCommit);
assert!(console.transcript().contains("Enter a number from 1 to 4"));
}
#[tokio::test]
async fn the_gitignore_question_answers_both_ways() {
for (answer, expected) in [("y", true), ("n", false), ("", true)] {
let (plan, _) = run_with(&["1", "", "", "", "", answer]).await;
assert_eq!(plan.gitignore, expected, "answer {answer:?}");
}
}
#[tokio::test]
async fn a_yes_no_question_accepts_long_forms_and_case() {
for (answer, expected) in [("YES", true), ("No", false), ("yes", true), ("N", false)] {
let (plan, _) = run_with(&["1", "", "", "", "", answer]).await;
assert_eq!(plan.gitignore, expected, "answer {answer:?}");
}
}
#[tokio::test]
async fn an_unparseable_yes_no_answer_is_re_asked() {
let (plan, console) = run_with(&["1", "", "", "", "", "maybe", "n"]).await;
assert!(!plan.gitignore);
assert!(console.transcript().contains("Enter y or n"));
}
#[tokio::test]
async fn the_provider_list_shows_every_preset_with_its_description() {
let (_, console) = run_with(&["1", "", "", "", "", ""]).await;
let transcript = console.transcript();
for preset in presets::PRESETS {
assert!(
transcript.contains(preset.display_name),
"`{}` is missing from the list: {transcript}",
preset.key
);
assert!(
transcript.contains(preset.description),
"`{}` has no description in the list",
preset.key
);
}
}
#[tokio::test]
async fn the_fallback_prompt_names_which_fallback_it_is_asking_about() {
let (_, console) = run_with(&["1", "", "", "y", "1", "", "", "n", "", ""]).await;
assert!(
console.transcript().contains("fallback #1"),
"got {}",
console.transcript()
);
}