Unify task status error schema and classify PDF AI failures

This commit is contained in:
Ahmed Bakr Ali
2026-03-24 23:30:46 +02:00
parent a6e0cab0b3
commit b2a7678848
5 changed files with 232 additions and 33 deletions

View File

@@ -0,0 +1,32 @@
"""Tests for PDF AI Celery task error payloads."""
from app.services.pdf_ai_service import PdfAiError
from app.tasks.pdf_ai_tasks import _build_pdf_ai_error_payload
def test_build_pdf_ai_error_payload_contains_classified_fields():
"""Should include error_code and user_message for task status normalization."""
error = PdfAiError(
"AI service is experiencing high demand. Please wait a moment and try again.",
error_code="OPENROUTER_RATE_LIMIT",
)
payload = _build_pdf_ai_error_payload("task-123", error, "chat-pdf")
assert payload["status"] == "failed"
assert payload["error_code"] == "OPENROUTER_RATE_LIMIT"
assert "user_message" in payload
assert payload["task_id"] == "task-123"
def test_build_pdf_ai_error_payload_includes_detail_when_available():
"""Should preserve machine-searchable detail context when provided."""
error = PdfAiError(
"Failed to extract text from PDF.",
error_code="PDF_TEXT_EXTRACTION_FAILED",
detail="EOF marker not found",
)
payload = _build_pdf_ai_error_payload("task-456", error, "summarize-pdf")
assert payload["error_code"] == "PDF_TEXT_EXTRACTION_FAILED"
assert payload["detail"] == "EOF marker not found"

View File

@@ -88,7 +88,7 @@ class TestTaskStatus:
assert has_task_access(user['id'], 'web', 'local-download-id') is True
def test_failure_task(self, client, monkeypatch):
"""Should return FAILURE state with error message."""
"""Should return FAILURE state with normalized error payload."""
mock_result = MagicMock()
mock_result.state = 'FAILURE'
mock_result.info = Exception('Conversion failed due to corrupt PDF.')
@@ -102,10 +102,50 @@ class TestTaskStatus:
assert response.status_code == 200
data = response.get_json()
assert data['state'] == 'FAILURE'
assert 'error' in data
assert data['error']['error_code'] == 'TASK_FAILURE'
assert 'user_message' in data['error']
assert data['error']['task_id'] == 'failed-id'
def test_failure_task_unregistered_maps_to_specific_code(self, client):
"""Should classify unregistered celery task errors."""
mock_result = MagicMock()
mock_result.state = 'FAILURE'
mock_result.info = Exception("Received unregistered task of type 'app.tasks.missing_task'.")
with client.session_transaction() as session:
session[TASK_ACCESS_SESSION_KEY] = ['missing-task-id']
with patch('app.routes.tasks.AsyncResult', return_value=mock_result):
response = client.get('/api/tasks/missing-task-id/status')
assert response.status_code == 200
data = response.get_json()
assert data['error']['error_code'] == 'CELERY_NOT_REGISTERED'
def test_success_failed_result_returns_normalized_error(self, client):
"""Should normalize task-level failure payload returned inside SUCCESS state."""
mock_result = MagicMock()
mock_result.state = 'SUCCESS'
mock_result.result = {
'status': 'failed',
'error_code': 'OPENROUTER_RATE_LIMIT',
'user_message': 'AI service is experiencing high demand.',
}
with client.session_transaction() as session:
session[TASK_ACCESS_SESSION_KEY] = ['ai-failed-id']
with patch('app.routes.tasks.AsyncResult', return_value=mock_result):
response = client.get('/api/tasks/ai-failed-id/status')
assert response.status_code == 200
data = response.get_json()
assert data['state'] == 'SUCCESS'
assert data['error']['error_code'] == 'OPENROUTER_RATE_LIMIT'
assert data['error']['task_id'] == 'ai-failed-id'
def test_unknown_task_without_access_returns_404(self, client):
"""Should not expose task state without session or API ownership."""
response = client.get('/api/tasks/unknown-task/status')
assert response.status_code == 404
assert response.status_code == 404