use std::path::Path;
use std::time::Duration;
use assert_cmd::Command;
use predicates::prelude::*;
use predicates::str::contains;
use serial_test::serial;
mod common;
fn bin() -> Command {
let mut c = Command::cargo_bin("zad").expect("zad binary built");
c.env("ZAD_SECRETS_MEMORY", "1");
c
}
fn seed_deadline(home: &Path, service: &str, secs_in_future: i64) {
let path = home
.join(".zad")
.join("state")
.join(service)
.join("rate_limit.json");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let deadline = jiff::Timestamp::now()
.checked_add(jiff::Span::new().seconds(secs_in_future))
.unwrap();
let body = format!(r#"{{"retry_after_utc":"{deadline}"}}"#);
std::fs::write(&path, body).unwrap();
}
#[test]
#[serial]
fn without_wait_a_pending_deadline_fails_fast_with_helpful_message() {
let home = tempfile::tempdir().unwrap();
seed_deadline(home.path(), "discord", 600);
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.args(["discord", "channels"])
.assert()
.failure()
.stderr(contains("rate-limited"))
.stderr(contains("--wait"));
}
#[test]
#[serial]
fn with_wait_and_no_deadline_is_a_noop_and_does_not_block() {
let home = tempfile::tempdir().unwrap();
let start = std::time::Instant::now();
let out = bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.args(["--wait", "discord", "channels"])
.timeout(Duration::from_secs(15))
.assert()
.failure();
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(10),
"--wait with no state must not block; took {elapsed:?}"
);
out.stderr(contains("rate-limited").not());
}
#[test]
#[serial]
fn json_flag_renders_structured_rate_limit_payload_to_stdout() {
let home = tempfile::tempdir().unwrap();
seed_deadline(home.path(), "discord", 1234);
let out = bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.args(["discord", "channels", "--json"])
.assert()
.failure();
let stdout = String::from_utf8(out.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("\"error\""),
"expected JSON payload on stdout; got: {stdout}"
);
assert!(stdout.contains("\"rate_limited\""), "got: {stdout}");
assert!(stdout.contains("\"service\""), "got: {stdout}");
assert!(stdout.contains("\"discord\""), "got: {stdout}");
assert!(stdout.contains("\"retry_after_seconds\""), "got: {stdout}");
assert!(stdout.contains("\"retry_after_utc\""), "got: {stdout}");
let stderr = String::from_utf8(out.get_output().stderr.clone()).unwrap();
assert!(
!stderr.contains("rate-limited"),
"human error should be suppressed under --json; got stderr: {stderr}"
);
}