use std::fs;
use std::sync::mpsc;
use tempfile::tempdir;
use crate::cli_outbound::{Chunk, Cli, ExitInfo, Streamed};
use crate::model_pick::query::{EMPTY_ROSTER, Roster, model_ids, start};
const LIVE_PAYLOAD: &str = r#"{"models":[{"default":false,"id":"gpt-5.6-sol"},{"default":false,"id":"gpt-5.6-terra"},{"default":false,"id":"gpt-5.4"}]}"#;
const SERVED_PAYLOAD: &str = r#"{"models":[{"default":false,"id":"gemini-3-pro","context_window":1048576,"max_output_tokens":65536,"display_name":"Gemini 3 Pro"},{"default":false,"id":"gemini-3-flash"}]}"#;
fn settled(chunks: Vec<Chunk>) -> Roster {
let (tx, rx) = mpsc::channel();
for chunk in chunks {
tx.send(chunk).unwrap();
}
drop(tx);
let mut roster = Roster::from_streamed(Streamed::from_rx(rx), "codex");
while roster.poll() {}
roster
}
fn stdout(text: &str) -> Chunk {
Chunk::Stdout(text.as_bytes().to_vec())
}
#[test]
fn model_ids_projects_the_live_payload_in_order() {
assert_eq!(
model_ids(LIVE_PAYLOAD),
["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.4"]
);
assert!(!model_ids(LIVE_PAYLOAD).contains(&"false".to_string()));
}
#[test]
fn model_ids_folds_every_unusable_payload_to_nothing() {
for payload in [
"",
"not json",
"[]",
r#"{"providers":[]}"#,
r#"{"models":"nope"}"#,
r#"{"models":[{"slug":"x"}]}"#,
] {
assert!(model_ids(payload).is_empty(), "{payload}");
}
}
#[test]
fn model_ids_reads_a_metadata_carrying_roster_the_same_way() {
assert_eq!(
model_ids(SERVED_PAYLOAD),
["gemini-3-pro", "gemini-3-flash"]
);
}
#[test]
fn a_clean_run_settles_into_the_roster() {
let roster = settled(vec![
stdout(&format!("{LIVE_PAYLOAD}\n")),
Chunk::Exited(ExitInfo::Code(0)),
]);
let view = roster.view();
assert!(!view.in_flight);
assert_eq!(view.models, ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.4"]);
assert_eq!(view.error, None);
assert_eq!(view.fallback, None);
assert_eq!(roster.provider(), "codex");
}
#[test]
fn a_torn_payload_reassembles_before_it_is_parsed() {
let (head, tail) = LIVE_PAYLOAD.split_at(20);
let roster = settled(vec![
stdout(head),
stdout(tail),
Chunk::Exited(ExitInfo::Code(0)),
]);
assert_eq!(roster.view().models.len(), 3);
}
#[test]
fn an_empty_roster_is_named_not_shown_as_an_empty_list() {
let roster = settled(vec![
stdout("{\"models\":[]}\n"),
Chunk::Exited(ExitInfo::Code(0)),
]);
let view = roster.view();
assert!(view.models.is_empty());
assert_eq!(view.error.as_deref(), Some(EMPTY_ROSTER));
assert!(
view.fallback.is_some(),
"the run-by-hand command is offered"
);
}
#[test]
fn a_failing_run_surfaces_its_stderr_and_the_run_by_hand_command() {
let roster = settled(vec![
Chunk::Stderr(b"401 Unauthorized\n".to_vec()),
Chunk::Exited(ExitInfo::Code(69)),
]);
let view = roster.view();
assert_eq!(view.error.as_deref(), Some("401 Unauthorized"));
assert_eq!(view.fallback.as_deref(), Some("bz --list-models"));
assert!(view.models.is_empty());
}
#[test]
fn a_silent_failure_renders_its_exit_code() {
let roster = settled(vec![Chunk::Exited(ExitInfo::Signal(15))]);
assert_eq!(
roster.view().error.as_deref(),
Some("bz --list-models exited 143")
);
}
#[test]
fn polling_a_settled_roster_is_a_no_op() {
let mut roster = settled(vec![
stdout(&format!("{LIVE_PAYLOAD}\n")),
Chunk::Exited(ExitInfo::Code(0)),
]);
let before = roster.view();
assert!(!roster.poll());
assert_eq!(roster.view(), before);
}
#[test]
fn a_pending_child_stays_in_flight() {
let (tx, rx) = mpsc::channel();
let mut roster = Roster::from_streamed(Streamed::from_rx(rx), "codex");
assert!(roster.poll(), "nothing ready yet");
assert!(roster.view().in_flight);
tx.send(stdout("{\"models\":[\n")).unwrap();
assert!(roster.poll(), "a line arrived, still live");
assert!(roster.view().in_flight);
assert!(roster.view().models.is_empty(), "nothing is offered yet");
tx.send(stdout("partial")).unwrap();
assert!(roster.poll(), "still live");
drop(tx);
}
#[test]
fn start_spawns_the_documented_argv_and_reads_its_roster() {
let dir = tempdir().unwrap();
let path = dir.path().join("fake-bz");
let seen = dir.path().join("argv");
crate::test_support::write_exec(
&path,
&format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" > {}\nprintf '%s\\n' '{LIVE_PAYLOAD}'\n",
seen.display()
),
);
let mut roster = start(&Cli::new(&path), "codex");
while roster.poll() {}
let view = roster.view();
assert_eq!(view.error, None, "the child exited clean");
assert_eq!(view.models, ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.4"]);
assert_eq!(
fs::read_to_string(&seen).unwrap().trim(),
"--list-models --provider codex --json"
);
}
#[test]
fn start_folds_a_spawn_failure_into_the_error_view() {
let roster = start(&Cli::new("/definitely/not/a/real/bz-xyz"), "codex");
let view = roster.view();
assert!(!view.in_flight);
assert!(view.error.is_some(), "the spawn failure is rendered");
assert_eq!(
view.fallback.as_deref(),
Some("/definitely/not/a/real/bz-xyz --list-models --provider codex --json")
);
}