openhawk-core 0.1.0

Core runtime for OpenHawk Agent OS — agent lifecycle, orchestration, config, healing
Documentation
use crate::db::init_database;
use crate::self_healer::{HealingOutcome, SelfHealer};
use tempfile::NamedTempFile;

fn healer(max_retries: u32) -> (NamedTempFile, SelfHealer) {
    let f = NamedTempFile::new().unwrap();
    let db = init_database(f.path()).unwrap();
    (f, SelfHealer::new(db, max_retries))
}

fn failing_healer(max_retries: u32) -> (NamedTempFile, SelfHealer) {
    let f = NamedTempFile::new().unwrap();
    let db = init_database(f.path()).unwrap();
    (f, SelfHealer::new_with_simulator(db, max_retries, true))
}

#[test]
fn healing_reverts_to_snapshot_before_retry() {
    let (_f, h) = healer(3);
    let outcome = h.attempt_healing(1, "test error").unwrap();
    assert!(matches!(outcome, HealingOutcome::Recovered { attempt: 1, .. }));
}

#[test]
fn retry_limit_is_enforced() {
    let (_f, h) = failing_healer(3);
    let outcome = h.attempt_healing(2, "persistent error").unwrap();
    assert!(matches!(outcome, HealingOutcome::Escalated { attempts: 3, .. }));
}

#[test]
fn custom_retry_limit_respected() {
    let (_f, h) = failing_healer(5);
    let outcome = h.attempt_healing(3, "err").unwrap();
    assert!(matches!(outcome, HealingOutcome::Escalated { attempts: 5, .. }));
}

#[test]
fn successful_healing_records_recovery() {
    let (_f, h) = healer(3);
    let outcome = h.attempt_healing(10, "recoverable").unwrap();
    match outcome {
        HealingOutcome::Recovered { attempt, adjustment } => {
            assert_eq!(attempt, 1);
            assert_eq!(adjustment, "reduce_context");
        }
        _ => panic!("expected Recovered"),
    }
}

#[test]
fn exhausted_attempts_escalate_to_user() {
    let (_f, h) = failing_healer(3);
    let outcome = h.attempt_healing(20, "fatal").unwrap();
    assert!(matches!(outcome, HealingOutcome::Escalated { .. }));
}

#[test]
fn escalated_outcome_carries_original_error() {
    let (_f, h) = failing_healer(3);
    let outcome = h.attempt_healing(20, "fatal error msg").unwrap();
    match outcome {
        HealingOutcome::Escalated { last_error, .. } => assert_eq!(last_error, "fatal error msg"),
        _ => panic!("expected Escalated"),
    }
}

#[test]
fn healing_events_logged_in_sqlite() {
    let (_f, h) = healer(3);
    h.attempt_healing(30, "db test error").unwrap();
    let history = h.get_history(30).unwrap();
    assert_eq!(history.len(), 1);
    let ev = &history[0];
    assert_eq!(ev.agent_pid, 30);
    assert_eq!(ev.original_error, "db test error");
    assert_eq!(ev.outcome, "Success");
    assert!(!ev.timestamp.is_empty());
}

#[test]
fn failure_events_logged_in_sqlite() {
    let (_f, h) = failing_healer(3);
    h.attempt_healing(31, "fail error").unwrap();
    let history = h.get_history(31).unwrap();
    assert_eq!(history.len(), 1);
    assert_eq!(history[0].outcome, "Failure");
    assert_eq!(history[0].attempt_number, 3);
}

#[test]
fn adjustment_field_reflects_strategy() {
    let (_f, h) = healer(3);
    h.attempt_healing(40, "err").unwrap();
    let history = h.get_history(40).unwrap();
    assert_eq!(history[0].adjustment, "reduce_context");
}

#[test]
fn get_history_returns_chronological_events() {
    let (_f, h) = failing_healer(3);
    h.attempt_healing(50, "first").unwrap();
    h.attempt_healing(50, "second").unwrap();
    let history = h.get_history(50).unwrap();
    assert_eq!(history.len(), 2);
    assert_eq!(history[0].original_error, "first");
    assert_eq!(history[1].original_error, "second");
}

#[test]
fn get_history_filters_by_agent_pid() {
    let (_f, h) = failing_healer(3);
    h.attempt_healing(60, "agent-60-err").unwrap();
    h.attempt_healing(61, "agent-61-err").unwrap();
    let h60 = h.get_history(60).unwrap();
    let h61 = h.get_history(61).unwrap();
    assert_eq!(h60.len(), 1);
    assert_eq!(h61.len(), 1);
    assert_eq!(h60[0].agent_pid, 60);
    assert_eq!(h61[0].agent_pid, 61);
}