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(),
}
}
#[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);
}
}
}