diff --git a/apps/admin-api/src/service/demo.rs b/apps/admin-api/src/service/demo.rs index ad91171..6f91072 100644 --- a/apps/admin-api/src/service/demo.rs +++ b/apps/admin-api/src/service/demo.rs @@ -7,7 +7,7 @@ use crank_core::{ }; use crank_mapping::{JsonPathRoot, infer_mapping_from_samples}; use crank_mapping::{MappingRule, MappingSet}; -use crank_registry::{ListInvocationLogsQuery, OperationSummary, SampleKind}; +use crank_registry::{ListInvocationLogsQuery, OperationSummary, RegistryError, SampleKind}; use crank_schema::Schema; use serde_json::{Value, json}; @@ -99,11 +99,8 @@ impl AdminService { .name .starts_with("weather_current_open_meteo_smoke_") { - self.delete_operation( - workspace_id, - &OperationId::new(operation.id.as_str().to_owned()), - ) - .await?; + self.delete_legacy_demo_operation_if_safe(workspace_id, &operation.id) + .await?; } } @@ -113,17 +110,36 @@ impl AdminService { "weather_current_open_meteo", ] { if let Some(operation) = self.find_operation_by_name(workspace_id, name).await? { - self.delete_operation( - workspace_id, - &OperationId::new(operation.id.as_str().to_owned()), - ) - .await?; + self.delete_legacy_demo_operation_if_safe(workspace_id, &operation.id) + .await?; } } Ok(()) } + async fn delete_legacy_demo_operation_if_safe( + &self, + workspace_id: &WorkspaceId, + operation_id: &OperationId, + ) -> Result<(), ApiError> { + match self + .registry + .delete_operation(workspace_id, operation_id) + .await + { + Ok(()) => Ok(()), + Err(RegistryError::OperationHasPublishedAgentBindings { .. }) => { + tracing::warn!( + operation_id = %operation_id.as_str(), + "legacy demo operation is still bound to a published agent; leaving it in place" + ); + Ok(()) + } + Err(error) => Err(ApiError::from(error)), + } + } + async fn ensure_demo_platform_api_key( &self, workspace_id: &WorkspaceId, diff --git a/apps/admin-api/tests/integration/community_access_usage.rs b/apps/admin-api/tests/integration/community_access_usage.rs index 69b3c7e..0eaf80b 100644 --- a/apps/admin-api/tests/integration/community_access_usage.rs +++ b/apps/admin-api/tests/integration/community_access_usage.rs @@ -12,8 +12,8 @@ use std::{ use async_trait::async_trait; use axum::{Json, Router, routing::post}; use crank_core::{ - ExecutionConfig, HttpMethod, MembershipRole, OperationSecurityLevel, Protocol, - ResponseCachePolicy, RestTarget, SecretKind, Target, ToolDescription, WorkspaceId, + AgentId, ExecutionConfig, HttpMethod, MembershipRole, OperationId, OperationSecurityLevel, + Protocol, ResponseCachePolicy, RestTarget, SecretKind, Target, ToolDescription, WorkspaceId, }; use crank_core::{IdentityError, IdentityProvider, IdentityProviderKind, LoginOutcome}; use crank_mapping::{MappingRule, MappingSet}; @@ -27,7 +27,9 @@ use tokio::net::TcpListener; use admin_api::{ app::build_app, auth::{AuthSettings, BootstrapAdminConfig, hash_password}, - service::{AdminService, AdminServiceBuilder, OperationPayload}, + service::{ + AdminService, AdminServiceBuilder, AgentBindingPayload, AgentPayload, OperationPayload, + }, state::AppState, }; @@ -398,6 +400,61 @@ async fn seeds_demo_assets_for_live_ui() { service.bootstrap_admin_user().await.unwrap(); service.seed_demo_assets().await.unwrap(); + + let smoke_operation = service + .create_operation( + &WorkspaceId::new(DEFAULT_WORKSPACE_ID), + test_operation_payload("https://example.test", "internal_health_smoke_bound"), + ) + .await + .unwrap(); + let smoke_operation_id = OperationId::new(smoke_operation.operation_id); + service + .publish_operation( + &WorkspaceId::new(DEFAULT_WORKSPACE_ID), + &smoke_operation_id, + smoke_operation.version, + ) + .await + .unwrap(); + let smoke_agent = service + .create_agent( + &WorkspaceId::new(DEFAULT_WORKSPACE_ID), + AgentPayload { + slug: "legacy-smoke-agent".to_owned(), + display_name: "Legacy Smoke Agent".to_owned(), + description: "Keeps a legacy smoke operation published".to_owned(), + instructions: json!({}), + tool_selection_policy: json!({}), + }, + ) + .await + .unwrap(); + let smoke_agent_id = AgentId::new(smoke_agent.agent_id); + service + .save_agent_bindings( + &WorkspaceId::new(DEFAULT_WORKSPACE_ID), + &smoke_agent_id, + vec![AgentBindingPayload { + operation_id: smoke_operation_id.as_str().to_owned(), + operation_version: smoke_operation.version, + tool_name: "legacy_health_smoke".to_owned(), + tool_title: "Legacy health smoke".to_owned(), + tool_description_override: None, + enabled: true, + }], + ) + .await + .unwrap(); + service + .publish_agent( + &WorkspaceId::new(DEFAULT_WORKSPACE_ID), + &smoke_agent_id, + smoke_agent.version, + ) + .await + .unwrap(); + service.seed_demo_assets().await.unwrap(); let owner = registry @@ -431,15 +488,18 @@ async fn seeds_demo_assets_for_live_ui() { .iter() .any(|operation| operation.name == "crm_create_lead") ); + assert!(!operations.iter().any( + |operation| operation.name.starts_with("internal_health_smoke_") + && operation.name != "internal_health_smoke_bound" + )); assert!( - !operations + operations .iter() - .any(|operation| operation.name.starts_with("internal_health_smoke_")) + .any(|operation| operation.name == "internal_health_smoke_bound") ); let agents = service.list_agents(&default_workspace_id).await.unwrap(); - assert_eq!(agents.len(), 1); - assert_eq!(agents[0].slug, "currency-rates"); + assert!(agents.iter().any(|agent| agent.slug == "currency-rates")); assert!(agents.iter().any(|agent| agent.key_count > 0));