qqq-cli 0.7.0

Local-first task queue for coding agent sessions
use crate::errors::{Code, Info};
use anyhow::Error;

pub(crate) fn discovery_unavailable(error: &Error) -> bool {
    error.downcast_ref::<Info>().is_some_and(|info| {
        matches!(info.code, Code::DispatchError)
            || info
                .details
                .get("reason")
                .and_then(serde_json::Value::as_str)
                .is_some_and(|reason| {
                    matches!(
                        reason,
                        "missing_project_agent"
                            | "ambiguous_project_agent"
                            | "missing_herdr_context"
                            | "missing_herdr_pane"
                            | "missing_agent_identity"
                            | "missing_agent_kind"
                    )
                })
    })
}

pub(super) fn can_force_after_error(error: &Error) -> bool {
    discovery_unavailable(error)
        || error
            .downcast_ref::<Info>()
            .is_some_and(|info| matches!(info.code, Code::OwnershipMismatch))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn completion_recovery_accepts_discovery_and_owner_failures() {
        for error in [
            Info::new(Code::DispatchError, "Herdr failed").into(),
            Info::invalid_argument("--session", "Missing identity")
                .detail("reason", "missing_agent_identity")
                .into(),
            Info::new(Code::OwnershipMismatch, "Owner changed").into(),
        ] {
            assert!(can_force_after_error(&error));
        }
    }

    #[test]
    fn completion_recovery_preserves_database_and_invalid_input_errors() {
        for error in [
            Error::new(rusqlite::Error::InvalidQuery),
            Info::new(Code::DatabaseError, "DB damaged").into(),
            Info::invalid_argument("--session", "Session must not be empty").into(),
            anyhow::anyhow!("unclassified failure"),
        ] {
            assert!(!can_force_after_error(&error));
        }
    }
}