use chrono::Utc;
use prodigy::cook::execution::dlq::{
DLQFilter, DeadLetterQueue, DeadLetteredItem, ErrorType, FailureDetail,
};
use prodigy::cook::execution::events::{EventLogger, JsonlEventWriter};
use prodigy::cook::execution::mapreduce::dlq_integration::agent_result_to_dlq_item;
use prodigy::cook::execution::mapreduce::{AgentResult, AgentStatus};
use prodigy::cook::execution::mapreduce_resume::{EnhancedResumeOptions, MapReduceResumeManager};
use prodigy::cook::execution::state::DefaultJobStateManager;
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
#[tokio::test]
async fn test_json_log_location_preserved_in_dlq() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
let job_id = "test-json-log-job";
let events_dir = project_root.join(".prodigy/events").join(job_id);
tokio::fs::create_dir_all(&events_dir).await.unwrap();
let event_writer = Box::new(
JsonlEventWriter::new(events_dir.join("events.jsonl"))
.await
.unwrap(),
);
let event_logger = Arc::new(EventLogger::new(vec![event_writer]));
let dlq = DeadLetterQueue::new(
job_id.to_string(),
project_root.clone(),
1000,
30,
Some(event_logger.clone()),
)
.await
.unwrap();
let dlq_item = DeadLetteredItem {
item_id: "item-with-log".to_string(),
item_data: json!({"id": 1, "data": "test"}),
first_attempt: Utc::now(),
last_attempt: Utc::now(),
failure_count: 1,
failure_history: vec![FailureDetail {
attempt_number: 1,
timestamp: Utc::now(),
error_type: ErrorType::CommandFailed { exit_code: 1 },
error_message: "Command failed".to_string(),
error_context: None,
stack_trace: None,
agent_id: "agent-1".to_string(),
step_failed: "step-1".to_string(),
duration_ms: 1000,
json_log_location: Some("/path/to/claude/log.json".to_string()),
}],
error_signature: "test-signature".to_string(),
worktree_artifacts: None,
reprocess_eligible: true,
manual_review_required: false,
};
dlq.add(dlq_item).await.unwrap();
let items = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(items.len(), 1);
let item = &items[0];
assert_eq!(
item.failure_history[0].json_log_location,
Some("/path/to/claude/log.json".to_string()),
"JSON log location should be preserved"
);
}
#[tokio::test]
async fn test_successful_retry_removes_from_dlq() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
let job_id = "test-retry-success-job";
let events_dir = project_root.join(".prodigy/events").join(job_id);
tokio::fs::create_dir_all(&events_dir).await.unwrap();
let event_writer = Box::new(
JsonlEventWriter::new(events_dir.join("events.jsonl"))
.await
.unwrap(),
);
let event_logger = Arc::new(EventLogger::new(vec![event_writer]));
let dlq = DeadLetterQueue::new(
job_id.to_string(),
project_root.clone(),
1000,
30,
Some(event_logger),
)
.await
.unwrap();
for i in 0..3 {
let item = DeadLetteredItem {
item_id: format!("retry-item-{}", i),
item_data: json!({"id": i, "data": format!("test-{}", i)}),
first_attempt: Utc::now(),
last_attempt: Utc::now(),
failure_count: 1,
failure_history: vec![FailureDetail {
attempt_number: 1,
timestamp: Utc::now(),
error_type: ErrorType::CommandFailed { exit_code: 1 },
error_message: "Temporary failure".to_string(),
error_context: None,
stack_trace: None,
agent_id: format!("agent-{}", i),
step_failed: "process".to_string(),
duration_ms: 1000,
json_log_location: None,
}],
error_signature: "temp-failure".to_string(),
worktree_artifacts: None,
reprocess_eligible: true,
manual_review_required: false,
};
dlq.add(item).await.unwrap();
}
let items = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(items.len(), 3, "Should have 3 items in DLQ");
dlq.remove("retry-item-0").await.unwrap();
let items = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(items.len(), 2, "Should have 2 items remaining in DLQ");
assert!(
!items.iter().any(|i| i.item_id == "retry-item-0"),
"Item should be removed"
);
}
#[tokio::test]
async fn test_failed_retry_updates_dlq_failure_count() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
let job_id = "test-retry-failure-job";
let events_dir = project_root.join(".prodigy/events").join(job_id);
tokio::fs::create_dir_all(&events_dir).await.unwrap();
let event_writer = Box::new(
JsonlEventWriter::new(events_dir.join("events.jsonl"))
.await
.unwrap(),
);
let event_logger = Arc::new(EventLogger::new(vec![event_writer]));
let dlq = DeadLetterQueue::new(
job_id.to_string(),
project_root.clone(),
1000,
30,
Some(event_logger),
)
.await
.unwrap();
let mut item = DeadLetteredItem {
item_id: "failing-item".to_string(),
item_data: json!({"id": 1, "data": "test"}),
first_attempt: Utc::now(),
last_attempt: Utc::now(),
failure_count: 1,
failure_history: vec![FailureDetail {
attempt_number: 1,
timestamp: Utc::now(),
error_type: ErrorType::CommandFailed { exit_code: 1 },
error_message: "First failure".to_string(),
error_context: None,
stack_trace: None,
agent_id: "agent-1".to_string(),
step_failed: "process".to_string(),
duration_ms: 1000,
json_log_location: None,
}],
error_signature: "failure-sig".to_string(),
worktree_artifacts: None,
reprocess_eligible: true,
manual_review_required: false,
};
dlq.add(item.clone()).await.unwrap();
item.failure_count = 2;
item.last_attempt = Utc::now();
item.failure_history.push(FailureDetail {
attempt_number: 2,
timestamp: Utc::now(),
error_type: ErrorType::CommandFailed { exit_code: 1 },
error_message: "Second failure".to_string(),
error_context: None,
stack_trace: None,
agent_id: "agent-1".to_string(),
step_failed: "process".to_string(),
duration_ms: 1200,
json_log_location: None,
});
dlq.remove(&item.item_id).await.unwrap();
dlq.add(item).await.unwrap();
let items = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(items.len(), 1);
let updated_item = &items[0];
assert_eq!(
updated_item.failure_count, 2,
"Failure count should be incremented"
);
assert_eq!(
updated_item.failure_history.len(),
2,
"Should have 2 failure entries"
);
assert_eq!(updated_item.failure_history[1].attempt_number, 2);
assert_eq!(
updated_item.failure_history[1].error_message,
"Second failure"
);
}
#[tokio::test]
async fn test_resume_manager_dlq_integration() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
let job_id = "test-resume-dlq-job";
let state_dir = project_root.join(".prodigy/state").join(job_id);
tokio::fs::create_dir_all(&state_dir).await.unwrap();
let events_dir = project_root.join(".prodigy/events").join(job_id);
tokio::fs::create_dir_all(&events_dir).await.unwrap();
let event_writer = Box::new(
JsonlEventWriter::new(events_dir.join("events.jsonl"))
.await
.unwrap(),
);
let event_logger = Arc::new(EventLogger::new(vec![event_writer]));
let dlq = DeadLetterQueue::new(
job_id.to_string(),
project_root.clone(),
1000,
30,
Some(event_logger.clone()),
)
.await
.unwrap();
for i in 0..3 {
let item = DeadLetteredItem {
item_id: format!("dlq-item-{}", i),
item_data: json!({"id": format!("dlq-item-{}", i), "data": format!("dlq-data-{}", i)}),
first_attempt: Utc::now(),
last_attempt: Utc::now(),
failure_count: 1,
failure_history: vec![FailureDetail {
attempt_number: 1,
timestamp: Utc::now(),
error_type: ErrorType::CommandFailed { exit_code: 1 },
error_message: "Test failure".to_string(),
error_context: None,
stack_trace: None,
agent_id: format!("agent-{}", i),
step_failed: "process".to_string(),
duration_ms: 1000,
json_log_location: Some(format!("/path/to/log-{}.json", i)),
}],
error_signature: "test-error".to_string(),
worktree_artifacts: None,
reprocess_eligible: true,
manual_review_required: false,
};
dlq.add(item).await.unwrap();
}
let dlq_items = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(dlq_items.len(), 3, "DLQ should have 3 items");
let state_manager = Arc::new(DefaultJobStateManager::new(state_dir.clone()));
let resume_manager = MapReduceResumeManager::new(
job_id.to_string(),
state_manager.clone(),
event_logger.clone(),
project_root.clone(),
)
.await
.unwrap();
let _ = resume_manager;
let options_with_dlq = EnhancedResumeOptions {
include_dlq_items: true,
reset_failed_agents: false,
force: false,
max_additional_retries: 0,
skip_validation: false,
from_checkpoint: None,
max_parallel: None,
force_recreation: false,
validate_environment: false,
};
let options_without_dlq = EnhancedResumeOptions {
include_dlq_items: false,
reset_failed_agents: false,
force: false,
max_additional_retries: 0,
skip_validation: false,
from_checkpoint: None,
max_parallel: None,
force_recreation: false,
validate_environment: false,
};
assert!(
options_with_dlq.include_dlq_items,
"DLQ items should be includable in resume"
);
assert!(
!options_without_dlq.include_dlq_items,
"DLQ items can be excluded from resume"
);
for item in dlq_items.iter() {
assert!(!item.item_id.is_empty(), "Item ID should not be empty");
assert!(
!item.failure_history.is_empty(),
"Should have failure history"
);
assert!(
item.failure_history[0].json_log_location.is_some(),
"Should have JSON log location for debugging"
);
assert!(
item.reprocess_eligible,
"Items should be marked as reprocess eligible"
);
}
}
#[tokio::test]
async fn test_agent_result_to_dlq_item_conversion() {
let work_item = json!({"id": "test-item", "data": "test-data"});
let failed_result = AgentResult {
item_id: "test-item".to_string(),
status: AgentStatus::Failed("Command failed with exit code 1".to_string()),
output: None,
commits: vec![],
files_modified: vec![],
duration: Duration::from_secs(10),
error: Some("Command failed with exit code 1".to_string()),
worktree_path: None,
branch_name: None,
worktree_session_id: None,
json_log_location: Some("/path/to/log.json".to_string()),
cleanup_status: None,
};
let dlq_item = agent_result_to_dlq_item(&failed_result, &work_item, 1);
assert!(dlq_item.is_some(), "Failed result should create DLQ item");
let item = dlq_item.unwrap();
assert_eq!(item.item_id, "test-item");
assert_eq!(item.failure_count, 1);
assert_eq!(item.failure_history.len(), 1);
assert_eq!(
item.failure_history[0].json_log_location,
Some("/path/to/log.json".to_string())
);
assert!(item.failure_history[0]
.error_message
.contains("Command failed"));
let timeout_result = AgentResult {
item_id: "test-item".to_string(),
status: AgentStatus::Timeout,
output: None,
commits: vec![],
files_modified: vec![],
duration: Duration::from_secs(60),
error: Some("Agent execution timed out".to_string()),
worktree_path: None,
branch_name: None,
worktree_session_id: None,
json_log_location: Some("/path/to/timeout-log.json".to_string()),
cleanup_status: None,
};
let timeout_dlq_item = agent_result_to_dlq_item(&timeout_result, &work_item, 1);
assert!(
timeout_dlq_item.is_some(),
"Timeout result should create DLQ item"
);
let timeout_item = timeout_dlq_item.unwrap();
assert_eq!(timeout_item.item_id, "test-item");
assert_eq!(
timeout_item.failure_history[0].error_type,
ErrorType::Timeout
);
assert_eq!(
timeout_item.failure_history[0].json_log_location,
Some("/path/to/timeout-log.json".to_string())
);
let success_result = AgentResult {
item_id: "test-item".to_string(),
status: AgentStatus::Success,
output: Some("Success output".to_string()),
commits: vec!["abc123".to_string()],
files_modified: vec![],
duration: Duration::from_secs(5),
error: None,
worktree_path: None,
branch_name: None,
worktree_session_id: None,
json_log_location: Some("/path/to/success-log.json".to_string()),
cleanup_status: None,
};
let success_dlq_item = agent_result_to_dlq_item(&success_result, &work_item, 1);
assert!(
success_dlq_item.is_none(),
"Success result should not create DLQ item"
);
}
#[tokio::test]
async fn test_dlq_retry_reprocesses_failed_items() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
let job_id = "test-dlq-retry-job";
let events_dir = project_root.join(".prodigy/events").join(job_id);
tokio::fs::create_dir_all(&events_dir).await.unwrap();
let event_writer = Box::new(
JsonlEventWriter::new(events_dir.join("events.jsonl"))
.await
.unwrap(),
);
let event_logger = Arc::new(EventLogger::new(vec![event_writer]));
let dlq = DeadLetterQueue::new(
job_id.to_string(),
project_root.clone(),
1000,
3, Some(event_logger),
)
.await
.unwrap();
for i in 0..3 {
let failure_count = i + 1; let item = DeadLetteredItem {
item_id: format!("reprocess-item-{}", i),
item_data: json!({"id": i, "data": format!("reprocess-{}", i)}),
first_attempt: Utc::now(),
last_attempt: Utc::now(),
failure_count,
failure_history: (0..failure_count)
.map(|attempt| FailureDetail {
attempt_number: attempt + 1,
timestamp: Utc::now(),
error_type: ErrorType::CommandFailed { exit_code: 1 },
error_message: format!("Attempt {} failed", attempt + 1),
error_context: None,
stack_trace: None,
agent_id: format!("agent-{}", i),
step_failed: "process".to_string(),
duration_ms: 1000,
json_log_location: Some(format!("/path/to/log-{}-{}.json", i, attempt)),
})
.collect(),
error_signature: "network-error".to_string(),
worktree_artifacts: None,
reprocess_eligible: failure_count < 3,
manual_review_required: failure_count >= 3,
};
dlq.add(item).await.unwrap();
}
let all_items = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(all_items.len(), 3, "DLQ should have 3 items");
let filter = DLQFilter {
reprocess_eligible: Some(true),
..Default::default()
};
let reprocessable_items = dlq.list_items(filter).await.unwrap();
assert_eq!(
reprocessable_items.len(),
2,
"Only 2 items should be reprocessable (failure_count < 3)"
);
for item in reprocessable_items.iter() {
assert!(item.reprocess_eligible, "Should be reprocess eligible");
assert!(item.failure_count < 3, "Should have less than max retries");
assert!(
!item.manual_review_required,
"Should not require manual review"
);
}
dlq.remove("reprocess-item-0").await.unwrap();
let after_success = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(
after_success.len(),
2,
"DLQ should have 2 items after successful retry"
);
assert!(
!after_success
.iter()
.any(|item| item.item_id == "reprocess-item-0"),
"Successful item should be removed"
);
let mut item_1 = after_success
.iter()
.find(|item| item.item_id == "reprocess-item-1")
.unwrap()
.clone();
item_1.failure_count = 3; item_1.last_attempt = Utc::now();
item_1.reprocess_eligible = false;
item_1.manual_review_required = true;
item_1.failure_history.push(FailureDetail {
attempt_number: 3,
timestamp: Utc::now(),
error_type: ErrorType::CommandFailed { exit_code: 1 },
error_message: "Third attempt failed".to_string(),
error_context: None,
stack_trace: None,
agent_id: "agent-1".to_string(),
step_failed: "process".to_string(),
duration_ms: 1200,
json_log_location: Some("/path/to/log-1-3.json".to_string()),
});
dlq.remove(&item_1.item_id).await.unwrap();
dlq.add(item_1).await.unwrap();
let after_failure = dlq.list_items(DLQFilter::default()).await.unwrap();
let updated_item_1 = after_failure
.iter()
.find(|item| item.item_id == "reprocess-item-1")
.unwrap();
assert_eq!(updated_item_1.failure_count, 3, "Should have 3 failures");
assert_eq!(
updated_item_1.failure_history.len(),
3,
"Should have 3 failure history entries"
);
assert!(
!updated_item_1.reprocess_eligible,
"Should not be reprocess eligible at max retries"
);
assert!(
updated_item_1.manual_review_required,
"Should require manual review at max retries"
);
let manual_review_items = dlq
.list_items(DLQFilter {
reprocess_eligible: Some(false),
..Default::default()
})
.await
.unwrap();
assert_eq!(
manual_review_items.len(),
2,
"Should have 2 items requiring manual review"
);
for item in manual_review_items.iter() {
assert!(
!item.reprocess_eligible,
"Items requiring manual review should not be reprocess eligible"
);
assert!(
item.manual_review_required,
"Should be marked for manual review"
);
}
}
#[tokio::test]
async fn test_dlq_concurrent_operations() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
let job_id = "test-concurrent-dlq";
let events_dir = project_root.join(".prodigy/events").join(job_id);
tokio::fs::create_dir_all(&events_dir).await.unwrap();
let event_writer = Box::new(
JsonlEventWriter::new(events_dir.join("events.jsonl"))
.await
.unwrap(),
);
let event_logger = Arc::new(EventLogger::new(vec![event_writer]));
let dlq = Arc::new(
DeadLetterQueue::new(
job_id.to_string(),
project_root.clone(),
1000,
30,
Some(event_logger),
)
.await
.unwrap(),
);
let mut handles = vec![];
for agent_id in 0..10 {
let dlq_clone = dlq.clone();
let handle = tokio::spawn(async move {
for item_num in 0..5 {
let item = DeadLetteredItem {
item_id: format!("agent-{}-item-{}", agent_id, item_num),
item_data: json!({"agent": agent_id, "item": item_num}),
first_attempt: Utc::now(),
last_attempt: Utc::now(),
failure_count: 1,
failure_history: vec![FailureDetail {
attempt_number: 1,
timestamp: Utc::now(),
error_type: ErrorType::CommandFailed { exit_code: 1 },
error_message: format!("Agent {} item {} failed", agent_id, item_num),
error_context: None,
stack_trace: None,
agent_id: format!("agent-{}", agent_id),
step_failed: "process".to_string(),
duration_ms: 1000,
json_log_location: Some(format!(
"/path/to/log-{}-{}.json",
agent_id, item_num
)),
}],
error_signature: "concurrent-test".to_string(),
worktree_artifacts: None,
reprocess_eligible: true,
manual_review_required: false,
};
dlq_clone.add(item).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
let all_items = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(
all_items.len(),
50,
"Should have 50 items (10 agents × 5 items)"
);
let mut seen_ids = std::collections::HashSet::new();
for item in all_items.iter() {
assert!(
seen_ids.insert(item.item_id.clone()),
"Item ID {} should be unique",
item.item_id
);
}
for item in all_items.iter() {
assert!(
!item.failure_history.is_empty(),
"Should have failure history"
);
assert_eq!(item.failure_count, 1, "Should have failure count of 1");
assert!(
item.failure_history[0].json_log_location.is_some(),
"Should have JSON log location"
);
}
}
#[tokio::test]
async fn test_dlq_filtering_capabilities() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
let job_id = "test-filtering-dlq";
let events_dir = project_root.join(".prodigy/events").join(job_id);
tokio::fs::create_dir_all(&events_dir).await.unwrap();
let event_writer = Box::new(
JsonlEventWriter::new(events_dir.join("events.jsonl"))
.await
.unwrap(),
);
let event_logger = Arc::new(EventLogger::new(vec![event_writer]));
let dlq = DeadLetterQueue::new(
job_id.to_string(),
project_root.clone(),
1000,
30,
Some(event_logger),
)
.await
.unwrap();
let test_items = vec![
(
"item-network-1",
ErrorType::CommandFailed { exit_code: 1 },
"network-error",
1,
true,
false,
),
(
"item-network-2",
ErrorType::CommandFailed { exit_code: 1 },
"network-error",
2,
true,
false,
),
(
"item-timeout-1",
ErrorType::Timeout,
"timeout",
1,
true,
false,
),
(
"item-syntax-1",
ErrorType::CommandFailed { exit_code: 2 },
"syntax-error",
3,
false,
true,
),
(
"item-syntax-2",
ErrorType::CommandFailed { exit_code: 2 },
"syntax-error",
3,
false,
true,
),
];
for (item_id, error_type, signature, failure_count, reprocessable, manual_review) in test_items
{
let item = DeadLetteredItem {
item_id: item_id.to_string(),
item_data: json!({"id": item_id}),
first_attempt: Utc::now(),
last_attempt: Utc::now(),
failure_count,
failure_history: vec![FailureDetail {
attempt_number: 1,
timestamp: Utc::now(),
error_type,
error_message: format!("{} failure", signature),
error_context: None,
stack_trace: None,
agent_id: format!("agent-{}", item_id),
step_failed: "process".to_string(),
duration_ms: 1000,
json_log_location: Some(format!("/path/to/log-{}.json", item_id)),
}],
error_signature: signature.to_string(),
worktree_artifacts: None,
reprocess_eligible: reprocessable,
manual_review_required: manual_review,
};
dlq.add(item).await.unwrap();
}
let reprocessable = dlq
.list_items(DLQFilter {
reprocess_eligible: Some(true),
..Default::default()
})
.await
.unwrap();
assert_eq!(reprocessable.len(), 3, "Should have 3 reprocessable items");
let manual_review = dlq
.list_items(DLQFilter {
reprocess_eligible: Some(false),
..Default::default()
})
.await
.unwrap();
assert_eq!(
manual_review.len(),
2,
"Should have 2 items requiring manual review"
);
for item in manual_review.iter() {
assert!(
item.manual_review_required,
"Should be marked for manual review"
);
assert!(!item.reprocess_eligible, "Should not be reprocessable");
}
let network_errors = dlq
.list_items(DLQFilter {
error_signature: Some("network-error".to_string()),
..Default::default()
})
.await
.unwrap();
assert_eq!(network_errors.len(), 2, "Should have 2 network errors");
let timeout_errors = dlq
.list_items(DLQFilter {
error_signature: Some("timeout".to_string()),
..Default::default()
})
.await
.unwrap();
assert_eq!(timeout_errors.len(), 1, "Should have 1 timeout error");
let reprocessable_network = dlq
.list_items(DLQFilter {
reprocess_eligible: Some(true),
error_signature: Some("network-error".to_string()),
..Default::default()
})
.await
.unwrap();
assert_eq!(
reprocessable_network.len(),
2,
"Should have 2 reprocessable network errors"
);
let all_items = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(all_items.len(), 5, "Should have 5 total items");
}
#[tokio::test]
async fn test_dlq_metadata_completeness() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
let job_id = "test-metadata-dlq";
let events_dir = project_root.join(".prodigy/events").join(job_id);
tokio::fs::create_dir_all(&events_dir).await.unwrap();
let event_writer = Box::new(
JsonlEventWriter::new(events_dir.join("events.jsonl"))
.await
.unwrap(),
);
let event_logger = Arc::new(EventLogger::new(vec![event_writer]));
let dlq = DeadLetterQueue::new(
job_id.to_string(),
project_root.clone(),
1000,
30,
Some(event_logger),
)
.await
.unwrap();
let now = Utc::now();
let mut item = DeadLetteredItem {
item_id: "metadata-test-item".to_string(),
item_data: json!({"id": "test", "data": "important data"}),
first_attempt: now,
last_attempt: now,
failure_count: 3,
failure_history: vec![],
error_signature: "test-error-signature".to_string(),
worktree_artifacts: None,
reprocess_eligible: true,
manual_review_required: false,
};
for attempt in 1u32..=3u32 {
item.failure_history.push(FailureDetail {
attempt_number: attempt,
timestamp: now + chrono::Duration::seconds(attempt as i64 * 60),
error_type: ErrorType::CommandFailed { exit_code: 1 },
error_message: format!("Attempt {} failed with specific error", attempt),
error_context: None,
stack_trace: Some(format!("Stack trace for attempt {}", attempt)),
agent_id: format!("agent-{}", attempt),
step_failed: format!("step-{}", attempt),
duration_ms: 1000 + (attempt as u64 * 100),
json_log_location: Some(format!("/path/to/detailed/log-{}.json", attempt)),
});
}
dlq.add(item).await.unwrap();
let items = dlq.list_items(DLQFilter::default()).await.unwrap();
assert_eq!(items.len(), 1, "Should have 1 item");
let retrieved_item = &items[0];
assert_eq!(retrieved_item.item_id, "metadata-test-item");
assert_eq!(retrieved_item.failure_count, 3);
assert_eq!(retrieved_item.error_signature, "test-error-signature");
assert_eq!(retrieved_item.item_data["id"], "test");
assert_eq!(retrieved_item.item_data["data"], "important data");
assert_eq!(
retrieved_item.failure_history.len(),
3,
"Should have 3 failure history entries"
);
for (idx, failure) in retrieved_item.failure_history.iter().enumerate() {
let attempt = (idx + 1) as u32;
assert_eq!(
failure.attempt_number, attempt,
"Attempt number should be {}",
attempt
);
assert!(
!failure.error_message.is_empty(),
"Error message should not be empty"
);
assert!(
failure.stack_trace.is_some(),
"Stack trace should be present"
);
assert!(!failure.agent_id.is_empty(), "Agent ID should not be empty");
assert!(
!failure.step_failed.is_empty(),
"Step failed should not be empty"
);
assert!(failure.duration_ms > 0, "Duration should be positive");
assert!(
failure.json_log_location.is_some(),
"JSON log location should be present"
);
let log_location = failure.json_log_location.as_ref().unwrap();
assert!(
log_location.ends_with(".json"),
"Log location should be a JSON file"
);
assert!(
log_location.contains(&attempt.to_string()),
"Log location should contain attempt number"
);
}
for i in 1..retrieved_item.failure_history.len() {
assert!(
retrieved_item.failure_history[i].timestamp
>= retrieved_item.failure_history[i - 1].timestamp,
"Failure timestamps should be ordered"
);
}
}