Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion src/agent/ingestion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,19 @@ async fn process_file(
let final_status = if had_failure { "failed" } else { "completed" };
complete_ingestion_file(&deps.sqlite_pool, &hash, final_status).await?;

// Clean up chunk-level progress records
if had_failure {
// Keep the source file and progress rows so the next poll cycle can
// resume from where it left off. Deleting on failure would cause data
// loss when a provider error interrupts mid-ingestion (fixes #48).
tracing::warn!(
file = %filename,
chunks = total_chunks,
"file ingestion had failures — keeping file and progress for retry"
);
return Ok(());
}

// Full success: clean up progress rows and remove the source file.
delete_progress(&deps.sqlite_pool, &hash).await?;

tokio::fs::remove_file(path)
Expand Down Expand Up @@ -525,4 +537,37 @@ mod tests {
let hash2 = content_hash("hello world!");
assert_ne!(hash1, hash2);
}

/// Regression test for #48: when any chunk errors, had_failure must be true
/// and the source file must be kept for retry. Tests the pure flag logic that
/// guards the delete path without requiring a live SQLite/filesystem setup.
#[test]
fn test_failure_flag_prevents_delete() {
let mut had_failure = false;

// Simulate a chunk that errors (e.g. provider 401)
let chunk_result: anyhow::Result<()> = Err(anyhow::anyhow!("provider error"));
if chunk_result.is_err() {
had_failure = true;
}

assert!(had_failure, "had_failure must be true after a chunk error");
// The guard `if had_failure { return Ok(()); }` means remove_file is
// never reached — assert the condition that triggers the early return.
assert!(had_failure, "early return condition must hold to skip file deletion");
}

/// Complement to test_failure_flag_prevents_delete: a clean run must reach
/// the delete path (had_failure stays false).
#[test]
fn test_success_flag_allows_delete() {
let mut had_failure = false;

let chunk_result: anyhow::Result<()> = Ok(());
if chunk_result.is_err() {
had_failure = true;
}

assert!(!had_failure, "had_failure must stay false when all chunks succeed");
}
}