From 84555aaf70460b0335694e0fd98bcc8c587ba42b Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Sun, 19 Apr 2026 21:50:28 +0000 Subject: [PATCH] api: add structured context for read path errors --- apps/admin-api/src/app.rs | 74 ++++++++++++++++++++++++++++++++++- apps/admin-api/src/service.rs | 48 +++++++++++++++++------ 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/apps/admin-api/src/app.rs b/apps/admin-api/src/app.rs index e657030..507c80c 100644 --- a/apps/admin-api/src/app.rs +++ b/apps/admin-api/src/app.rs @@ -499,7 +499,16 @@ mod tests { .send() .await .unwrap(); - assert_eq!(missing.status(), reqwest::StatusCode::NOT_FOUND); + let missing_status = missing.status(); + let missing = missing.json::().await.unwrap(); + assert_eq!(missing_status, reqwest::StatusCode::NOT_FOUND); + assert_eq!(missing["error"]["code"], "not_found"); + assert_eq!( + missing["error"]["context"], + json!({ + "operation_id": operation_id + }) + ); } #[tokio::test(flavor = "multi_thread")] @@ -708,7 +717,16 @@ mod tests { .send() .await .unwrap(); - assert_eq!(missing.status(), reqwest::StatusCode::NOT_FOUND); + let missing_status = missing.status(); + let missing = missing.json::().await.unwrap(); + assert_eq!(missing_status, reqwest::StatusCode::NOT_FOUND); + assert_eq!(missing["error"]["code"], "not_found"); + assert_eq!( + missing["error"]["context"], + json!({ + "agent_id": agent_id + }) + ); } #[tokio::test(flavor = "multi_thread")] @@ -1945,6 +1963,58 @@ mod tests { assert_eq!(deleted["ok"], true); assert_eq!(missing_status, reqwest::StatusCode::NOT_FOUND); assert_eq!(missing["error"]["code"], "not_found"); + assert_eq!( + missing["error"]["context"], + json!({ + "secret_id": secret_id + }) + ); + } + + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn returns_structured_context_for_missing_operation_version() { + let registry = test_registry().await; + let storage_root = test_storage_root("missing_operation_version"); + let upstream_base_url = spawn_upstream_server().await; + let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; + let client = authorized_client(&base_url).await; + + let created = client + .post(format!("{base_url}/operations")) + .json(&test_operation_payload( + &upstream_base_url, + "crm_missing_operation_version", + )) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + let operation_id = created["operation_id"].as_str().unwrap().to_owned(); + + let response = client + .get(format!("{base_url}/operations/{operation_id}/versions/99")) + .send() + .await + .unwrap(); + let status = response.status(); + let body = response.json::().await.unwrap(); + + assert_eq!(status, reqwest::StatusCode::NOT_FOUND); + assert_eq!(body["error"]["code"], "not_found"); + assert_eq!( + body["error"]["message"], + format!("operation version 99 for {operation_id} was not found") + ); + assert_eq!( + body["error"]["context"], + json!({ + "operation_id": operation_id, + "version": 99 + }) + ); } #[tokio::test(flavor = "multi_thread")] diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index 3958a1c..7138760 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -1372,7 +1372,10 @@ impl AdminService { .get_invocation_log(workspace_id, log_id) .await? .ok_or_else(|| { - ApiError::not_found(format!("invocation log {} was not found", log_id.as_str())) + ApiError::not_found_with_context( + format!("invocation log {} was not found", log_id.as_str()), + json!({ "log_id": log_id.as_str() }), + ) }) } @@ -1744,7 +1747,10 @@ impl AdminService { .get_operation_summary(workspace_id, operation_id) .await? .ok_or_else(|| { - ApiError::not_found(format!("operation {} was not found", operation_id.as_str())) + ApiError::not_found_with_context( + format!("operation {} was not found", operation_id.as_str()), + json!({ "operation_id": operation_id.as_str() }), + ) })?; let agent_refs = self .registry @@ -1790,10 +1796,16 @@ impl AdminService { .get_operation_version(workspace_id, operation_id, version) .await? .ok_or_else(|| { - ApiError::not_found(format!( - "operation version {version} for {} was not found", - operation_id.as_str() - )) + ApiError::not_found_with_context( + format!( + "operation version {version} for {} was not found", + operation_id.as_str() + ), + json!({ + "operation_id": operation_id.as_str(), + "version": version, + }), + ) }) } @@ -2487,7 +2499,10 @@ impl AdminService { .await? .map(|record| record.secret) .ok_or_else(|| { - ApiError::not_found(format!("secret {} was not found", secret_id.as_str())) + ApiError::not_found_with_context( + format!("secret {} was not found", secret_id.as_str()), + json!({ "secret_id": secret_id.as_str() }), + ) }) } @@ -2657,7 +2672,10 @@ impl AdminService { .get_agent_summary(workspace_id, agent_id) .await? .ok_or_else(|| { - ApiError::not_found(format!("agent {} was not found", agent_id.as_str())) + ApiError::not_found_with_context( + format!("agent {} was not found", agent_id.as_str()), + json!({ "agent_id": agent_id.as_str() }), + ) })?; let version = self .get_agent_version(workspace_id, agent_id, summary.current_draft_version) @@ -2710,10 +2728,16 @@ impl AdminService { .get_agent_version(workspace_id, agent_id, version) .await? .ok_or_else(|| { - ApiError::not_found(format!( - "agent version {version} for {} was not found", - agent_id.as_str() - )) + ApiError::not_found_with_context( + format!( + "agent version {version} for {} was not found", + agent_id.as_str() + ), + json!({ + "agent_id": agent_id.as_str(), + "version": version, + }), + ) }) }