zag-agent 0.18.0

Core library for zag — a unified interface for AI coding agents
Documentation
use super::*;
use chrono::Duration;

fn make_hit(provider: &'static str, reset: Option<DateTime<Utc>>) -> UsageLimit {
    UsageLimit {
        provider,
        scope: UsageLimitScope::Session,
        reset_at: reset,
        raw: "test".to_string(),
    }
}

#[test]
fn compute_resume_uses_provider_reset_time_with_jitter() {
    let cfg = UsageLimitConfig::default();
    let target = Utc::now() + Duration::seconds(120);
    let hit = make_hit("claude", Some(target));

    let (scheduled, fallback_used) = compute_resume_at(&hit, &cfg);
    assert!(!fallback_used);
    // Jitter is 30s by default, so scheduled = target + 30s ± rounding.
    let diff = (scheduled - target).num_seconds();
    assert!(
        (30..=31).contains(&diff),
        "expected ~30s jitter, got {diff}"
    );
}

#[test]
fn compute_resume_falls_back_when_reset_is_none() {
    let cfg = UsageLimitConfig::default();
    let hit = make_hit("codex", None);
    let before = Utc::now();
    let (scheduled, fallback_used) = compute_resume_at(&hit, &cfg);
    assert!(fallback_used);
    // Default fallback is 3600s + 30s jitter = ~3630s out.
    let secs = (scheduled - before).num_seconds();
    assert!(
        (3600..=3700).contains(&secs),
        "expected ~3630s fallback, got {secs}"
    );
}

#[test]
fn compute_resume_respects_per_provider_fallback_override() {
    let mut cfg = UsageLimitConfig::default();
    cfg.providers.insert(
        "copilot".to_string(),
        UsageLimitProviderOverride {
            fallback_secs: Some(60),
            ..Default::default()
        },
    );
    let hit = make_hit("copilot", None);
    let before = Utc::now();
    let (scheduled, _) = compute_resume_at(&hit, &cfg);
    let secs = (scheduled - before).num_seconds();
    assert!((60..=100).contains(&secs), "got {secs}");
}

#[test]
fn compute_resume_caps_at_max_wait_secs() {
    let cfg = UsageLimitConfig {
        max_wait_secs: 10,
        ..Default::default()
    };
    // Reset is 1 year out — should be capped to now + 10s.
    let target = Utc::now() + Duration::days(365);
    let hit = make_hit("claude", Some(target));
    let before = Utc::now();
    let (scheduled, _) = compute_resume_at(&hit, &cfg);
    let secs = (scheduled - before).num_seconds();
    assert!(secs <= 12, "expected ≤10s cap (got {secs})");
}

#[test]
fn compute_resume_clamps_past_reset_to_near_now() {
    let cfg = UsageLimitConfig::default();
    let target = Utc::now() - Duration::seconds(60);
    let hit = make_hit("claude", Some(target));
    let before = Utc::now();
    let (scheduled, _) = compute_resume_at(&hit, &cfg);
    let secs = (scheduled - before).num_seconds();
    // Past reset is replaced by now + jitter (~30s), then capped.
    assert!((25..=35).contains(&secs), "got {secs}");
}

#[test]
fn enabled_for_respects_global_and_per_provider() {
    let mut cfg = UsageLimitConfig::default();
    assert!(cfg.enabled_for("claude"));

    cfg.enabled = false;
    assert!(!cfg.enabled_for("claude"));

    cfg.enabled = true;
    cfg.providers.insert(
        "codex".to_string(),
        UsageLimitProviderOverride {
            enabled: Some(false),
            ..Default::default()
        },
    );
    assert!(!cfg.enabled_for("codex"));
    assert!(cfg.enabled_for("claude"));
}

#[test]
fn default_max_attempts_is_twelve() {
    let cfg = UsageLimitConfig::default();
    assert_eq!(cfg.max_attempts, 12);
}

#[test]
fn max_attempts_round_trips_through_toml() {
    // Override-from-toml path: parse a partial table and confirm the
    // value lands. (Bare-bones — toml-rs is already a dev-dep via serde.)
    let parsed: UsageLimitConfig = toml::from_str("max_attempts = 0\n").unwrap();
    assert_eq!(parsed.max_attempts, 0);
    let parsed: UsageLimitConfig = toml::from_str("max_attempts = 50\n").unwrap();
    assert_eq!(parsed.max_attempts, 50);
}

#[test]
fn log_event_hit_carries_through_all_fields() {
    let hit = UsageLimit {
        provider: "claude",
        scope: UsageLimitScope::Weekly,
        reset_at: Some(Utc::now() + Duration::seconds(60)),
        raw: "boom".to_string(),
    };
    let when = Utc::now() + Duration::seconds(90);
    let kind = log_event_hit(&hit, "incident-xyz", Some(when), true);
    match kind {
        crate::session_log::LogEventKind::UsageLimitHit {
            provider,
            scope,
            reset_at,
            scheduled_resume_at,
            fallback_used,
            incident_id,
            raw,
        } => {
            assert_eq!(provider, "claude");
            assert_eq!(scope, "weekly");
            assert!(reset_at.is_some());
            assert!(scheduled_resume_at.is_some());
            assert!(fallback_used);
            assert_eq!(incident_id, "incident-xyz");
            assert_eq!(raw.as_deref(), Some("boom"));
        }
        _ => panic!("expected UsageLimitHit"),
    }
}

#[test]
fn to_log_event_hit_generates_fresh_incident() {
    let hit = UsageLimit {
        provider: "codex",
        scope: UsageLimitScope::Session,
        reset_at: None,
        raw: "boom".to_string(),
    };
    let a = to_log_event_hit(hit.clone());
    let b = to_log_event_hit(hit);
    match (a, b) {
        (
            crate::session_log::LogEventKind::UsageLimitHit {
                incident_id: i1, ..
            },
            crate::session_log::LogEventKind::UsageLimitHit {
                incident_id: i2, ..
            },
        ) => assert_ne!(i1, i2, "orphan emissions must each get a fresh incident id"),
        _ => panic!("expected UsageLimitHit"),
    }
}

#[test]
fn resume_message_per_provider_override() {
    let mut cfg = UsageLimitConfig::default();
    assert_eq!(cfg.resume_message_for("claude"), "Continue");
    cfg.providers.insert(
        "copilot".to_string(),
        UsageLimitProviderOverride {
            resume_message: Some("Please continue.".to_string()),
            ..Default::default()
        },
    );
    assert_eq!(cfg.resume_message_for("copilot"), "Please continue.");
    assert_eq!(cfg.resume_message_for("claude"), "Continue");
}