roboticus-api 0.11.3

HTTP routes, WebSocket, auth, rate limiting, and dashboard for the Roboticus agent runtime
Documentation
/// Classify an opaque delegation error string into a typed `DelegationError`.
pub fn classify_delegation_error(err: &str) -> roboticus_core::delegation_error::DelegationError {
    use roboticus_core::delegation_error::DelegationError;
    let err_lower = err.to_ascii_lowercase();

    if err_lower.contains("policy denied") || err_lower.contains("not allowed") {
        let parts: Vec<&str> = err.splitn(3, ':').collect();
        return DelegationError::PolicyDenied {
            rule: parts
                .get(1)
                .map(|s| s.trim().to_string())
                .unwrap_or_else(|| "unknown".into()),
            reason: parts
                .get(2)
                .map(|s| s.trim().to_string())
                .unwrap_or_else(|| err.to_string()),
        };
    }

    if err_lower.contains("timeout") || err_lower.contains("timed out") {
        let duration_ms = err
            .split(|c: char| !c.is_ascii_digit())
            .filter(|s: &&str| !s.is_empty())
            .find_map(|s| s.parse::<u64>().ok())
            .unwrap_or(0);
        return DelegationError::Timeout { duration_ms };
    }

    if err_lower.contains("unavailable")
        || err_lower.contains("not found")
        || err_lower.contains("not running")
        || err_lower.contains("stopped")
    {
        let name = err.split('\'').nth(1).unwrap_or("unknown").to_string();
        return DelegationError::SubagentUnavailable {
            name,
            state: "unavailable".into(),
        };
    }

    DelegationError::LlmCallFailed {
        provider: "unknown".into(),
        model: "unknown".into(),
        reason: err.to_string(),
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────

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

    #[test]
    fn classifies_policy_denied() {
        use roboticus_core::delegation_error::DelegationError;
        let result = classify_delegation_error("Policy denied: max_tasks: limit exceeded");
        assert!(
            matches!(result, DelegationError::PolicyDenied { .. }),
            "expected PolicyDenied, got {result:?}"
        );
        if let DelegationError::PolicyDenied { rule, reason } = result {
            assert_eq!(rule, "max_tasks");
            assert_eq!(reason, "limit exceeded");
        }
    }

    #[test]
    fn classifies_timeout() {
        use roboticus_core::delegation_error::DelegationError;
        let result = classify_delegation_error("operation timed out after 30000ms");
        assert!(
            matches!(result, DelegationError::Timeout { duration_ms: 30000 }),
            "expected Timeout with 30000ms, got {result:?}"
        );
    }

    #[test]
    fn classifies_unavailable() {
        use roboticus_core::delegation_error::DelegationError;
        let result = classify_delegation_error("subagent 'analyst' not running");
        assert!(
            matches!(result, DelegationError::SubagentUnavailable { .. }),
            "expected SubagentUnavailable, got {result:?}"
        );
        if let DelegationError::SubagentUnavailable { name, .. } = result {
            assert_eq!(name, "analyst");
        }
    }

    #[test]
    fn defaults_to_llm_call_failed() {
        use roboticus_core::delegation_error::DelegationError;
        let result = classify_delegation_error("some unknown error occurred");
        assert!(
            matches!(result, DelegationError::LlmCallFailed { .. }),
            "expected LlmCallFailed, got {result:?}"
        );
    }

    #[test]
    fn task_event_row_can_be_constructed_for_each_lifecycle_state() {
        use roboticus_db::task_events::{TaskEventRow, TaskLifecycleState};

        let states = [
            (TaskLifecycleState::Pending, "Task created"),
            (TaskLifecycleState::Assigned, "Assigned to code-analyst"),
            (TaskLifecycleState::Running, "Subagent processing"),
            (TaskLifecycleState::Completed, "Analysis complete"),
            (TaskLifecycleState::Failed, "LLM call failed"),
            (TaskLifecycleState::Cancelled, "User cancelled"),
        ];

        for (state, summary) in &states {
            let event = TaskEventRow {
                id: uuid::Uuid::new_v4().to_string(),
                task_id: "task-1".into(),
                parent_task_id: None,
                assigned_to: Some("code-analyst".into()),
                event_type: *state,
                summary: Some(summary.to_string()),
                detail_json: None,
                percentage: if *state == TaskLifecycleState::Completed {
                    Some(100.0)
                } else {
                    None
                },
                retry_count: 0,
                created_at: "2026-03-23T12:00:00".into(),
            };
            assert_eq!(event.event_type, *state);
        }
    }
}