use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::thread::JoinHandle;
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_supercode"))
}
fn fresh_home(tag: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"supercode-ux31-doctor-{tag}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn spawn_stub(
models_status: &'static str,
chat_body: Option<&'static str>,
) -> (SocketAddr, JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind stub listener");
let addr = listener.local_addr().unwrap();
let handle = std::thread::spawn(move || {
let (mut sock, _) = listener.accept().expect("accept /models connection");
let mut buf = [0u8; 4096];
let _ = sock.read(&mut buf);
let body = "{\"data\":[]}";
let resp = format!(
"HTTP/1.1 {models_status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
sock.write_all(resp.as_bytes())
.expect("write /models stub response");
sock.flush().ok();
if let Some(chat_body) = chat_body {
let (mut sock2, _) = listener
.accept()
.expect("accept /chat/completions connection");
let mut buf2 = [0u8; 8192];
let _ = sock2.read(&mut buf2);
let resp2 = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
chat_body.len(),
chat_body
);
sock2
.write_all(resp2.as_bytes())
.expect("write /chat/completions stub response");
sock2.flush().ok();
}
});
(addr, handle)
}
const PING_TOOL_CALL_SSE: &str = "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"function\":{\"name\":\"ping\",\"arguments\":\"{}\"}}]}}]}\n\ndata: [DONE]\n\n";
fn run(home: &Path, args: &[&str]) -> Output {
Command::new(bin())
.env("SUPERCODE_HOME", home)
.env_remove("OPENROUTER_API_KEY")
.env_remove("OPENAI_API_KEY")
.env_remove("ANTHROPIC_API_KEY")
.env_remove("NO_COLOR")
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn the supercode binary")
}
fn json_stdout(out: &Output) -> serde_json::Value {
assert!(
out.status.success(),
"expected success, got {:?}\nstdout: {}\nstderr: {}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
serde_json::from_slice(&out.stdout).unwrap_or_else(|e| {
panic!(
"stdout was not valid JSON: {e}\nstdout: {}",
String::from_utf8_lossy(&out.stdout)
)
})
}
fn tier<'a>(v: &'a serde_json::Value, name: &str) -> &'a serde_json::Value {
v["tiers"]
.as_array()
.expect("tiers must be an array")
.iter()
.find(|t| t["name"] == name)
.unwrap_or_else(|| panic!("no tier named {name} in {v}"))
}
#[test]
fn doctor_json_names_config_as_the_first_failing_tier_when_no_key_is_configured() {
let home = fresh_home("no-key");
let out = run(&home, &["doctor", "--json"]);
let v = json_stdout(&out);
assert_eq!(v["first_failing_tier"], "config");
assert_eq!(v["deep_probe_requested"], false);
let config = tier(&v, "config");
assert_eq!(config["status"], "fail");
assert!(config["hint"].as_str().unwrap().contains("supercode login"));
assert_eq!(tier(&v, "catalog")["status"], "skip");
assert_eq!(tier(&v, "deep")["status"], "skip");
}
#[test]
fn doctor_human_shows_diagnostic_tiers_and_the_first_failing_hint() {
let home = fresh_home("no-key-human");
let out = run(&home, &["doctor"]);
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("diagnostic tiers"), "got: {stdout}");
assert!(stdout.contains("tier · config"), "got: {stdout}");
assert!(stdout.contains("tier · catalog"), "got: {stdout}");
assert!(stdout.contains("tier · deep"), "got: {stdout}");
assert!(
stdout.contains("first failing tier: config"),
"expected a first-failing-tier hint line naming `config`, got: {stdout}"
);
assert!(serde_json::from_str::<serde_json::Value>(&stdout).is_err());
}
#[test]
fn doctor_json_tier2_passes_and_tier3_is_skipped_without_the_deep_flag() {
let (addr, server) = spawn_stub("200 OK", None);
let home = fresh_home("catalog-pass");
let out = run(
&home,
&[
"--api-key",
"sk-test-key",
"--base-url",
&format!("http://{addr}"),
"doctor",
"--json",
],
);
let v = json_stdout(&out);
assert_eq!(v["first_failing_tier"], serde_json::Value::Null);
assert_eq!(v["deep_probe_requested"], false);
assert_eq!(tier(&v, "config")["status"], "pass");
let catalog = tier(&v, "catalog");
assert_eq!(catalog["status"], "pass");
assert!(catalog["detail"].as_str().unwrap().contains("reachable"));
let deep = tier(&v, "deep");
assert_eq!(deep["status"], "skip");
assert!(deep["hint"].as_str().unwrap().contains("--deep"));
server.join().expect("stub server thread panicked");
}
#[test]
fn doctor_json_deep_probe_runs_and_passes_with_the_deep_flag() {
let (addr, server) = spawn_stub("200 OK", Some(PING_TOOL_CALL_SSE));
let home = fresh_home("deep-pass");
let out = run(
&home,
&[
"--api-key",
"sk-test-key",
"--base-url",
&format!("http://{addr}"),
"doctor",
"--json",
"--deep",
],
);
let v = json_stdout(&out);
assert_eq!(v["deep_probe_requested"], true);
assert_eq!(v["first_failing_tier"], serde_json::Value::Null);
let deep = tier(&v, "deep");
assert_eq!(deep["status"], "pass");
assert!(
deep["detail"].as_str().unwrap().contains("tool-call"),
"expected the round-trip's tool call to be observed, got: {v}"
);
server.join().expect("stub server thread panicked");
}
#[test]
fn doctor_json_names_catalog_as_the_first_failing_tier_when_the_key_is_rejected() {
let (addr, server) = spawn_stub("401 Unauthorized", None);
let home = fresh_home("catalog-fail");
let out = run(
&home,
&[
"--api-key",
"sk-bad-key",
"--base-url",
&format!("http://{addr}"),
"doctor",
"--json",
"--deep",
],
);
let v = json_stdout(&out);
assert_eq!(v["first_failing_tier"], "catalog");
assert_eq!(tier(&v, "config")["status"], "pass");
let catalog = tier(&v, "catalog");
assert_eq!(catalog["status"], "fail");
assert!(catalog["detail"].as_str().unwrap().contains("401"));
assert!(catalog["hint"].as_str().unwrap().contains("key/base-url"));
let deep = tier(&v, "deep");
assert_eq!(
deep["status"], "skip",
"tier 3 must not run once tier 2 has already failed"
);
server.join().expect("stub server thread panicked");
}