#![cfg(unix)]
use std::net::TcpListener;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use tempfile::TempDir;
const PAYLOAD: &str = r#"{"command":"echo hi","output":"hi","exit_code":0}"#;
fn run_bg_with_key(root: &Path, api_key: &str, listener: &TcpListener) {
let payload = root.join(".tmpAbC123");
std::fs::write(&payload, PAYLOAD).unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_oo"))
.args([
"_learn_bg",
payload.to_str().unwrap(),
root.to_str().unwrap(),
])
.env("OO_CONFIG_DIR", root.join("cfg"))
.env("OO_DATA_DIR", root.join("data"))
.env("ANTHROPIC_API_KEY", api_key)
.env(
"ANTHROPIC_API_URL",
format!("http://{}/v1/messages", listener.local_addr().unwrap()),
)
.env_remove("HTTP_PROXY")
.env_remove("HTTPS_PROXY")
.env_remove("ALL_PROXY")
.env_remove("http_proxy")
.env_remove("https_proxy")
.env_remove("all_proxy")
.env("NO_PROXY", "127.0.0.1,localhost")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if child.try_wait().unwrap().is_some() {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
let _ = child.kill();
let _ = child.wait();
}
fn assert_no_request_and_missing_key_status(api_key: &str) {
let root = TempDir::new().unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
listener.set_nonblocking(true).unwrap();
run_bg_with_key(root.path(), api_key, &listener);
assert!(
listener.accept().is_err(),
"API key {api_key:?} must not produce an HTTP request"
);
let status = std::fs::read_to_string(root.path().join("cfg").join("learn-status.log"))
.expect("missing key must be recorded in learn-status.log");
assert!(
status.contains("Set ANTHROPIC_API_KEY environment variable"),
"{status}"
);
}
#[test]
fn empty_api_key_is_treated_as_unset() {
assert_no_request_and_missing_key_status("");
}
#[test]
fn whitespace_only_api_key_is_treated_as_unset() {
assert_no_request_and_missing_key_status(" \t ");
}