n8n: add Fleet Ops endpoint to receive workflow error reports
New POST /api/v1/integrations/n8n/workflow-error, service-token authenticated, for the central "Fleet Ops — Workflow Error Handler" n8n workflow to report a bounded, secret-free failure (workflow id/ name, execution id, safe error category, trigger context, correlation id, attempt, retry action). Idempotent on execution_id via the same audit-event precheck pattern used by /return-callback, so a redelivered error report is not registered twice. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e0c107a94a
commit
bbdb4a9ae8
@@ -122,3 +122,72 @@ def test_scheduled_scan_is_idempotent_across_repeated_triggers(client):
|
||||
assert first.status_code == 200
|
||||
assert second.status_code == 200
|
||||
assert second.json()["created"] == {}
|
||||
|
||||
|
||||
def _workflow_error_body(execution_id: str, **overrides):
|
||||
body = {
|
||||
"workflow_id": "mobilityops-return-processing",
|
||||
"workflow_name": "Fleet Ops — Vehicle Return Orchestration",
|
||||
"execution_id": execution_id,
|
||||
"failed_at": "2026-08-04T10:15:00Z",
|
||||
"error_category": "httpError",
|
||||
"error_summary": "Callback request failed with status 500",
|
||||
"trigger_context": "webhook",
|
||||
"correlation_id": None,
|
||||
"attempt": 1,
|
||||
"retry_action": "n8n will retry automatically",
|
||||
}
|
||||
body.update(overrides)
|
||||
return body
|
||||
|
||||
|
||||
def test_workflow_error_rejects_wrong_service_token(client):
|
||||
response = client.post(
|
||||
"/api/v1/integrations/n8n/workflow-error",
|
||||
json=_workflow_error_body(str(uuid.uuid4())),
|
||||
headers={"X-Service-Token": "wrong-token"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_workflow_error_rejects_unknown_category(client):
|
||||
settings = get_settings()
|
||||
response = client.post(
|
||||
"/api/v1/integrations/n8n/workflow-error",
|
||||
json=_workflow_error_body(str(uuid.uuid4()), error_category="somethingElse"),
|
||||
headers={"X-Service-Token": settings.n8n_callback_token},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_workflow_error_registers_and_is_idempotent_by_execution_id(client, ops_client):
|
||||
settings = get_settings()
|
||||
headers = {"X-Service-Token": settings.n8n_callback_token}
|
||||
execution_id = str(uuid.uuid4())
|
||||
body = _workflow_error_body(execution_id)
|
||||
|
||||
first = client.post("/api/v1/integrations/n8n/workflow-error", json=body, headers=headers)
|
||||
second = client.post("/api/v1/integrations/n8n/workflow-error", json=body, headers=headers)
|
||||
|
||||
assert first.status_code == 200
|
||||
assert first.json()["status"] == "registered"
|
||||
assert second.status_code == 200
|
||||
assert second.json()["status"] == "already_registered"
|
||||
|
||||
audit_events = ops_client.get(
|
||||
"/api/v1/audit", params={"action": "n8n_workflow_failure_registered"}
|
||||
).json()
|
||||
matching = [e for e in audit_events if e["metadata"]["execution_id"] == execution_id]
|
||||
assert len(matching) == 1
|
||||
assert matching[0]["after"]["error_category"] == "httpError"
|
||||
assert matching[0]["after"]["retry_action"] == "n8n will retry automatically"
|
||||
|
||||
|
||||
def test_workflow_error_bounds_summary_length(client):
|
||||
settings = get_settings()
|
||||
response = client.post(
|
||||
"/api/v1/integrations/n8n/workflow-error",
|
||||
json=_workflow_error_body(str(uuid.uuid4()), error_summary="x" * 501),
|
||||
headers={"X-Service-Token": settings.n8n_callback_token},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
Reference in New Issue
Block a user