use super::*;
use std::io::{Read as _, Write as _};
use std::net::TcpListener;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
const BIND_ENV: &str = "MOADIM_BIND_ADDR";
const UNREACHABLE_ADDR: &str = "127.0.0.1:1";
fn argv(args: &[&str]) -> Vec<String> {
args.iter().map(ToString::to_string).collect()
}
struct EnvGuard {
name: &'static str,
previous: Option<std::ffi::OsString>,
}
impl EnvGuard {
fn set(name: &'static str, value: &str) -> Self {
let previous = std::env::var_os(name);
unsafe {
std::env::set_var(name, value);
}
Self { name, previous }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match self.previous.take() {
Some(value) => std::env::set_var(self.name, value),
None => std::env::remove_var(self.name),
}
}
}
}
struct FakeServer {
addr: String,
stop: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl FakeServer {
fn start(status: u16, body: &str) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port");
let addr = listener.local_addr().expect("local addr").to_string();
listener.set_nonblocking(true).expect("set nonblocking");
let stop = Arc::new(AtomicBool::new(false));
let stop_loop = Arc::clone(&stop);
let response = format!(
"HTTP/1.1 {status} OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
let handle = std::thread::spawn(move || {
while !stop_loop.load(Ordering::SeqCst) {
match listener.accept() {
Ok((mut stream, _)) => {
let mut buf = [0u8; 2048];
let _ = stream.read(&mut buf);
let _ = stream.write_all(response.as_bytes());
}
Err(ref err) if err.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(2));
}
Err(_) => break,
}
}
});
Self {
addr,
stop,
handle: Some(handle),
}
}
}
impl Drop for FakeServer {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
#[test]
fn help_and_version_return_zero() {
assert_eq!(run(argv(&["--help"])), 0);
assert_eq!(run(argv(&["routines", "--help"])), 0);
assert_eq!(run(argv(&["--version"])), 0);
}
#[test]
fn usage_errors_return_two() {
assert_eq!(run(argv(&[])), 2);
assert_eq!(run(argv(&["nonsense"])), 2);
assert_eq!(run(argv(&["routines"])), 2);
}
#[test]
fn invalid_json_flags_return_two_without_a_server() {
assert_eq!(
run(argv(&[
"routines",
"create",
"--schedule",
"* * * * *",
"--title",
"t",
"--agent",
"a",
"--prompt",
"p",
"--repositories",
"{bad",
])),
2
);
assert_eq!(
run(argv(&[
"routines",
"replace",
"id",
"--schedule",
"* * * * *",
"--title",
"t",
"--agent",
"a",
"--prompt",
"p",
"--repositories",
"{bad",
])),
2
);
assert_eq!(
run(argv(&[
"routines",
"update",
"id",
"--repositories",
"{bad"
])),
2
);
assert_eq!(
run(argv(&["routines", "update", "id", "--machines", "{bad"])),
2
);
}
#[test]
fn every_subcommand_succeeds_against_a_2xx_server() {
let server = FakeServer::start(200, "{\"ok\":true}");
let _addr = EnvGuard::set(BIND_ENV, &server.addr);
let calls: &[&[&str]] = &[
&[
"routines",
"create",
"--schedule",
"* * * * *",
"--title",
"t",
"--agent",
"a",
"--prompt",
"p",
],
&[
"routine",
"create",
"--schedule",
"* * * * *",
"--title",
"t",
"--agent",
"a",
"--model",
"claude-sonnet-4-6",
"--prompt",
"p",
"--disabled",
"--repositories",
"[]",
"--tag",
"triage",
"--tag",
"nightly",
],
&["routines", "list"],
&["routines", "get", "rid"],
&[
"routines",
"update",
"rid",
"--title",
"t2",
"--model",
"",
"--repositories",
"[]",
"--enabled",
"false",
"--ttl-secs",
"10",
"--max-runtime-secs",
"20",
"--tag",
"ops",
],
&[
"routines",
"replace",
"rid",
"--schedule",
"* * * * *",
"--title",
"t",
"--agent",
"a",
"--prompt",
"p",
],
&["routines", "delete", "rid"],
&["routines", "trigger", "rid"],
&["routines", "logs", "rid"],
&["routines", "ical"],
&["schedule", "trigger", "sid"],
&["sched", "trigger", "sid"],
&["agents"],
&["echo", "hello"],
];
for call in calls {
assert_eq!(run(argv(call)), 0, "call {call:?}");
}
}
#[test]
fn logs_print_raw_when_body_is_not_json() {
let server = FakeServer::start(200, "plain log line\nsecond line");
let _addr = EnvGuard::set(BIND_ENV, &server.addr);
assert_eq!(run(argv(&["routines", "logs", "abc"])), 0);
}
#[test]
fn empty_body_prints_nothing_and_succeeds() {
let server = FakeServer::start(200, "");
let _addr = EnvGuard::set(BIND_ENV, &server.addr);
assert_eq!(run(argv(&["agents"])), 0);
}
#[test]
fn non_2xx_status_returns_one() {
{
let server = FakeServer::start(404, "{\"error\":\"not found\"}");
let _addr = EnvGuard::set(BIND_ENV, &server.addr);
assert_eq!(run(argv(&["routines", "get", "missing"])), 1);
}
{
let server = FakeServer::start(500, "");
let _addr = EnvGuard::set(BIND_ENV, &server.addr);
assert_eq!(run(argv(&["routines", "list"])), 1);
}
}
#[test]
fn no_server_returns_not_running_exit_code() {
let _addr = EnvGuard::set(BIND_ENV, UNREACHABLE_ADDR);
assert_eq!(
run(argv(&["routines", "list"])),
crate::cli::EXIT_NOT_RUNNING
);
assert_eq!(
run(argv(&["schedule", "trigger", "sid"])),
crate::cli::EXIT_NOT_RUNNING
);
}
#[test]
fn insert_opt_only_inserts_present_values() {
let mut map = Map::new();
insert_opt(&mut map, "a", Some(Value::Bool(true)));
insert_opt(&mut map, "b", None);
assert_eq!(map.get("a"), Some(&Value::Bool(true)));
assert!(!map.contains_key("b"));
}
#[test]
fn object_and_to_body_build_compact_json() {
let body = object([("message", Value::String("hi".to_string()))]);
assert_eq!(body, "{\"message\":\"hi\"}");
}
#[test]
fn routine_body_serializes_all_fields() {
let value: Value = serde_json::from_str(
&routine_body(
"* * * * *".into(),
"title".into(),
"agent".into(),
Some("claude-sonnet-4-6".into()),
"prompt".into(),
Some("[]".into()),
Some("[\"work\"]".into()),
Some(30),
Some(60),
vec!["triage".to_string(), "nightly".to_string()],
false,
)
.unwrap(),
)
.unwrap();
assert_eq!(value["title"], Value::String("title".to_string()));
assert_eq!(
value["model"],
Value::String("claude-sonnet-4-6".to_string())
);
assert_eq!(value["repositories"], Value::Array(vec![]));
assert_eq!(
value["machines"],
Value::Array(vec![Value::String("work".to_string())])
);
assert_eq!(value["ttl_secs"], Value::from(30));
assert_eq!(
value["tags"],
Value::Array(vec![
Value::String("triage".to_string()),
Value::String("nightly".to_string()),
])
);
assert_eq!(value["enabled"], Value::Bool(true));
}
#[test]
fn routine_body_rejects_bad_repositories() {
assert_eq!(
routine_body(
"* * * * *".into(),
"t".into(),
"a".into(),
None,
"p".into(),
Some("{bad".into()),
None,
None,
None,
vec![],
false,
),
Err(2)
);
}
#[test]
fn routine_body_rejects_bad_machines() {
assert_eq!(
routine_body(
"* * * * *".into(),
"t".into(),
"a".into(),
None,
"p".into(),
None,
Some("{bad".into()),
None,
None,
vec![],
false,
),
Err(2)
);
}