#![allow(dead_code, unused_imports)] use super::common::*; use std::collections::BTreeMap; use crank_core::{ AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig, AuthConfig, AuthKind, AuthProfile, ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, InvocationLog, MembershipRole, OperationId, OperationSecurityLevel, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId, Target, ToolDescription, ToolExample, User, UserId, UserSessionId, WizardState, Workspace, WorkspaceId, }; use crank_mapping::{MappingRule, MappingSet}; use crank_schema::{Schema, SchemaKind}; use serde_json::json; use sqlx::{Executor, PgPool, postgres::PgPoolOptions}; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use crank_registry::{ CreateAgentRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind, SaveAgentCatalogConfigRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, UsageBucket, UsageOutcomeGroup, UsageQuery, WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus, }; fn test_workspace_id() -> WorkspaceId { WorkspaceId::new("ws_default") } fn timestamp(value: &str) -> OffsetDateTime { OffsetDateTime::parse(value, &Rfc3339).unwrap() } #[tokio::test] async fn manages_agent_read_paths() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_agent_01", 1, OperationStatus::Draft); let agent = test_agent("agent_01", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let bindings = vec![AgentOperationBinding { operation_id: operation.id.clone(), operation_version: operation.version, tool_name: "create_lead".to_owned(), tool_title: "Create lead".to_owned(), tool_description_override: Some("Creates CRM lead".to_owned()), enabled: true, }]; registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: &bindings, }) .await .unwrap(); let listed = registry.list_agents(&test_workspace_id()).await.unwrap(); let summary = registry .get_agent_summary(&test_workspace_id(), &agent.id) .await .unwrap() .unwrap(); let loaded_version = registry .get_agent_version(&test_workspace_id(), &agent.id, version.version) .await .unwrap() .unwrap(); assert_eq!(listed, vec![summary.clone()]); assert_eq!(summary.id, agent.id); assert_eq!(summary.slug, agent.slug); assert_eq!(summary.display_name, agent.display_name); assert_eq!(summary.status, agent.status); assert_eq!(loaded_version.agent_id, agent.id); assert_eq!(loaded_version.version, version.version); assert_eq!(loaded_version.status, version.status); assert_eq!(loaded_version.snapshot, version); assert_eq!(loaded_version.bindings, bindings); database.cleanup().await; } #[tokio::test] async fn manages_published_agent_tool_reads() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_agent_pub_01", 1, OperationStatus::Draft); let operation_v2 = test_operation("op_agent_pub_01", 2, OperationStatus::Draft); let agent = test_agent("agent_pub_01", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let bindings = vec![AgentOperationBinding { operation_id: operation.id.clone(), operation_version: operation_v2.version, tool_name: "create_lead".to_owned(), tool_title: "Create lead".to_owned(), tool_description_override: Some("Creates CRM lead".to_owned()), enabled: true, }]; registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); registry .create_version(CreateVersionRequest { workspace_id: &test_workspace_id(), snapshot: &operation_v2, change_note: Some("publishable"), created_by: Some("alice"), }) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation.id, version: operation_v2.version, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: &bindings, }) .await .unwrap(); registry .publish_agent(PublishAgentRequest { workspace_id: &test_workspace_id(), agent_id: &agent.id, version: version.version, published_at: ×tamp("2026-03-25T12:11:00Z"), published_by: Some("alice"), expected_state: None, }) .await .unwrap(); let tools = registry .get_published_agent_tools_by_slug("default", &agent.slug) .await .unwrap(); assert_eq!(tools.len(), 1); assert_eq!(tools[0].workspace_id, test_workspace_id()); assert_eq!(tools[0].workspace_slug, "default"); assert_eq!(tools[0].agent_id, agent.id); assert_eq!(tools[0].agent_slug, agent.slug); assert_eq!(tools[0].tool_name, bindings[0].tool_name); assert_eq!(tools[0].tool_title, bindings[0].tool_title); assert_eq!(tools[0].tool_description, "Creates CRM lead"); assert_eq!(tools[0].operation.id, operation_v2.id); assert_eq!(tools[0].operation.version, operation_v2.version); assert_eq!(tools[0].operation.protocol, operation_v2.protocol); assert!(tools[0].operation.is_published()); database.cleanup().await; } #[tokio::test] async fn published_agent_snapshot_remains_immutable_after_draft_edit() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_agent_immutable_01", 1, OperationStatus::Draft); let agent = test_agent("agent_immutable_01", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let published_binding = AgentOperationBinding { operation_id: operation.id.clone(), operation_version: operation.version, tool_name: "create_lead_immutable".to_owned(), tool_title: "Create lead".to_owned(), tool_description_override: Some("Published immutable binding".to_owned()), enabled: true, }; registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation.id, version: operation.version, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: std::slice::from_ref(&published_binding), }) .await .unwrap(); registry .publish_agent(PublishAgentRequest { workspace_id: &test_workspace_id(), agent_id: &agent.id, version: version.version, published_at: ×tamp("2026-03-25T12:11:00Z"), published_by: Some("alice"), expected_state: None, }) .await .unwrap(); let before = registry .get_published_agent_tools_by_slug("default", &agent.slug) .await .unwrap(); assert_eq!(before.len(), 1); assert_eq!(before[0].tool_name, published_binding.tool_name); registry .save_agent_catalog_config(SaveAgentCatalogConfigRequest { workspace_id: &test_workspace_id(), agent_id: &agent.id, agent_version: version.version, bindings: &[], tool_selection_policy: &Default::default(), expected_state: None, }) .await .expect_err("editing after publish must not mutate the published Agent Version"); let after = registry .get_published_agent_tools_by_slug("default", &agent.slug) .await .unwrap(); assert_eq!(after, before); database.cleanup().await; } #[tokio::test] async fn stale_agent_revision_rejects_mutation() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_agent_stale_01", 1, OperationStatus::Draft); let agent = test_agent("agent_stale_01", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let binding = AgentOperationBinding { operation_id: operation.id.clone(), operation_version: operation.version, tool_name: "create_lead_stale".to_owned(), tool_title: "Create lead".to_owned(), tool_description_override: None, enabled: true, }; registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation.id, version: operation.version, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: std::slice::from_ref(&binding), }) .await .unwrap(); registry .publish_agent(PublishAgentRequest { workspace_id: &test_workspace_id(), agent_id: &agent.id, version: version.version, published_at: ×tamp("2026-03-25T12:11:00Z"), published_by: Some("alice"), expected_state: None, }) .await .unwrap(); let stale_save = registry .save_agent_catalog_config(SaveAgentCatalogConfigRequest { workspace_id: &test_workspace_id(), agent_id: &agent.id, agent_version: version.version, bindings: &[], tool_selection_policy: &Default::default(), expected_state: None, }) .await; assert!( stale_save.is_err(), "stale write against already-published Agent Version must be rejected" ); database.cleanup().await; } #[tokio::test] async fn published_agent_catalog_revision_is_durable_and_monotonic() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_agent_revision_01", 1, OperationStatus::Draft); let agent = test_agent("agent_revision_01", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let binding = AgentOperationBinding { operation_id: operation.id.clone(), operation_version: operation.version, tool_name: "create_lead_revision".to_owned(), tool_title: "Create lead".to_owned(), tool_description_override: None, enabled: true, }; registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation.id, version: operation.version, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: std::slice::from_ref(&binding), }) .await .unwrap(); registry .publish_agent(PublishAgentRequest { workspace_id: &test_workspace_id(), agent_id: &agent.id, version: version.version, published_at: ×tamp("2026-03-25T12:11:00Z"), published_by: Some("alice"), expected_state: None, }) .await .unwrap(); let first = registry .get_published_agent_catalog_by_slug("default", &agent.slug) .await .unwrap() .catalog_revision; registry .unpublish_agent( &test_workspace_id(), &agent.id, ×tamp("2026-03-25T12:12:00Z"), None, ) .await .unwrap(); registry .publish_agent(PublishAgentRequest { workspace_id: &test_workspace_id(), agent_id: &agent.id, version: version.version, published_at: ×tamp("2026-03-25T12:13:00Z"), published_by: Some("alice"), expected_state: None, }) .await .unwrap(); let second = registry .get_published_agent_catalog_by_slug("default", &agent.slug) .await .unwrap() .catalog_revision; assert_ne!( first, second, "unpublish/re-publish of the same Agent Version must invalidate stale catalog search results" ); database.cleanup().await; } #[tokio::test] async fn database_rejects_direct_published_agent_snapshot_mutation() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_agent_db_guard_01", 1, OperationStatus::Draft); let agent = test_agent("agent_db_guard_01", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let binding = AgentOperationBinding { operation_id: operation.id.clone(), operation_version: operation.version, tool_name: "create_lead_db_guard".to_owned(), tool_title: "Create lead".to_owned(), tool_description_override: None, enabled: true, }; registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation.id, version: operation.version, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: std::slice::from_ref(&binding), }) .await .unwrap(); registry .publish_agent(PublishAgentRequest { workspace_id: &test_workspace_id(), agent_id: &agent.id, version: version.version, published_at: ×tamp("2026-03-25T12:11:00Z"), published_by: Some("alice"), expected_state: None, }) .await .unwrap(); let policy_update = sqlx::query( "update agent_versions set tool_selection_policy_json = '{\"changed\":true}'::jsonb where agent_id = $1 and version = 1", ) .bind(agent.id.as_str()) .execute(registry.pool()) .await; assert!( policy_update.is_err(), "database trigger must reject direct Published Agent Version mutation" ); let binding_delete = sqlx::query( "delete from agent_operation_bindings where agent_id = $1 and agent_version = 1", ) .bind(agent.id.as_str()) .execute(registry.pool()) .await; assert!( binding_delete.is_err(), "database trigger must reject direct Published Agent binding deletion" ); let pointer_rewind = sqlx::query( "update published_agents set catalog_revision = catalog_revision where agent_id = $1", ) .bind(agent.id.as_str()) .execute(registry.pool()) .await; assert!( pointer_rewind.is_err(), "database trigger must reject non-increasing catalog revision" ); database.cleanup().await; } #[tokio::test] async fn manages_operation_usage_and_agent_ref_reads() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_usage_01", 1, OperationStatus::Draft); let operation_v2 = test_operation("op_usage_01", 2, OperationStatus::Draft); let agent = test_agent("agent_usage_01", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let bindings = vec![AgentOperationBinding { operation_id: operation.id.clone(), operation_version: operation_v2.version, tool_name: "create_lead".to_owned(), tool_title: "Create lead".to_owned(), tool_description_override: None, enabled: true, }]; registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); registry .create_version(CreateVersionRequest { workspace_id: &test_workspace_id(), snapshot: &operation_v2, change_note: Some("publishable"), created_by: Some("alice"), }) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation.id, version: operation_v2.version, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: &bindings, }) .await .unwrap(); registry .publish_agent(PublishAgentRequest { workspace_id: &test_workspace_id(), agent_id: &agent.id, version: version.version, published_at: ×tamp("2026-03-25T12:11:00Z"), published_by: Some("alice"), expected_state: None, }) .await .unwrap(); assert_eq!( registry .create_invocation_log(CreateInvocationLogRequest { log: &test_invocation_log( "log_usage_ok", &operation.id, Some(agent.id.clone()), crank_core::InvocationStatus::Ok, 120, "2026-03-25T12:20:00Z", ), }) .await, crank_registry::InvocationHistoryWriteOutcome::Recorded ); let stored = registry .get_invocation_log( &test_workspace_id(), &crank_core::InvocationLogId::new("log_usage_ok"), ) .await .unwrap() .expect("typed invocation history"); assert_eq!(stored.log.operation_version, Some(1)); assert_eq!( stored.log.execution_stage, Some(crank_core::ExecutionStage::Runtime) ); assert_eq!( stored.log.retryability, Some(crank_core::Retryability::Never) ); assert_eq!( stored.log.outcome_certainty, Some(crank_core::OutcomeCertainty::Certain) ); assert_eq!( registry .create_invocation_log(CreateInvocationLogRequest { log: &test_invocation_log( "log_usage_err", &operation.id, Some(agent.id.clone()), crank_core::InvocationStatus::Error, 240, "2026-03-25T12:21:00Z", ), }) .await, crank_registry::InvocationHistoryWriteOutcome::Recorded ); let has_bindings = registry .has_published_agent_bindings_for_operation(&test_workspace_id(), &operation.id) .await .unwrap(); let agent_refs = registry .list_operation_agent_refs(&test_workspace_id()) .await .unwrap(); let usage = registry .list_operation_usage_summaries(&test_workspace_id(), "2026-03-25T12:00:00Z") .await .unwrap(); assert!(has_bindings); assert_eq!(agent_refs.len(), 1); assert_eq!(agent_refs[0].operation_id, operation.id); assert_eq!(agent_refs[0].agent_id, agent.id); assert_eq!(agent_refs[0].agent_slug, agent.slug); assert_eq!(agent_refs[0].display_name, agent.display_name); assert_eq!(usage.len(), 1); assert_eq!(usage[0].operation_id, operation.id); assert_eq!(usage[0].calls_today, 2); assert_eq!(usage[0].error_rate_pct, 50.0); assert_eq!(usage[0].avg_latency_ms, 180); database.cleanup().await; } #[tokio::test] async fn invocation_history_filters_cursor_and_usage_outcomes_are_bounded() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_history_01", 1, OperationStatus::Draft); let agent = test_agent("agent_history_01", AgentStatus::Draft); registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &test_agent_version(&agent.id, 1, AgentStatus::Draft), bindings: &[], }) .await .unwrap(); let mut success = test_invocation_log( "log_history_success", &operation.id, Some(agent.id.clone()), crank_core::InvocationStatus::Ok, 50, "2026-03-25T12:00:00Z", ); success.execution_error_code = None; success.message = "=formula must remain data".to_owned(); let mut upstream = test_invocation_log( "log_history_upstream", &operation.id, Some(agent.id.clone()), crank_core::InvocationStatus::Error, 150, "2026-03-25T12:01:00Z", ); upstream.execution_error_code = Some(crank_core::ExecutionErrorCode::UpstreamTimeout); let mut client = test_invocation_log( "log_history_client", &operation.id, Some(agent.id.clone()), crank_core::InvocationStatus::Error, 250, "2026-03-25T12:02:00Z", ); client.execution_error_code = Some(crank_core::ExecutionErrorCode::InputSchemaInvalid); let mut schema = test_invocation_log( "log_history_schema", &operation.id, Some(agent.id.clone()), crank_core::InvocationStatus::Error, 350, "2026-03-25T12:03:00Z", ); schema.execution_error_code = Some(crank_core::ExecutionErrorCode::OutputSchemaInvalid); let mut crank = test_invocation_log( "log_history_crank", &operation.id, Some(agent.id.clone()), crank_core::InvocationStatus::Error, 450, "2026-03-25T12:04:00Z", ); crank.execution_error_code = Some(crank_core::ExecutionErrorCode::RuntimeInternal); for log in [&success, &upstream, &client, &schema, &crank] { assert_eq!( registry .create_invocation_log(CreateInvocationLogRequest { log }) .await, crank_registry::InvocationHistoryWriteOutcome::Recorded ); } let first_page = registry .list_invocation_logs(crank_registry::ListInvocationLogsQuery { workspace_id: &test_workspace_id(), level: None, status: Some(crank_core::InvocationStatus::Error), outcome_group: None, search_text: None, source: None, operation_id: None, agent_id: None, created_after: Some("2026-03-25T12:00:00Z"), created_before: Some("2026-03-25T12:04:00Z"), cursor_created_at: None, cursor_id: None, limit: 2, }) .await .unwrap(); assert_eq!(first_page.len(), 2); assert_eq!(first_page[0].log.id.as_str(), "log_history_schema"); assert_eq!(first_page[1].log.id.as_str(), "log_history_client"); let second_page = registry .list_invocation_logs(crank_registry::ListInvocationLogsQuery { workspace_id: &test_workspace_id(), level: None, status: Some(crank_core::InvocationStatus::Error), outcome_group: None, search_text: None, source: None, operation_id: None, agent_id: None, created_after: Some("2026-03-25T12:00:00Z"), created_before: Some("2026-03-25T12:04:00Z"), cursor_created_at: Some("2026-03-25T12:02:00Z"), cursor_id: Some(&crank_core::InvocationLogId::new("log_history_client")), limit: 2, }) .await .unwrap(); assert_eq!(second_page.len(), 1); assert_eq!(second_page[0].log.id.as_str(), "log_history_upstream"); let upstream_only = registry .list_invocation_logs(crank_registry::ListInvocationLogsQuery { workspace_id: &test_workspace_id(), level: None, status: None, outcome_group: Some(UsageOutcomeGroup::Upstream), search_text: None, source: None, operation_id: None, agent_id: None, created_after: Some("2026-03-25T12:00:00Z"), created_before: Some("2026-03-25T12:05:00Z"), cursor_created_at: None, cursor_id: None, limit: 10, }) .await .unwrap(); assert_eq!(upstream_only.len(), 1); assert_eq!(upstream_only[0].log.id.as_str(), "log_history_upstream"); let usage = registry .list_usage_outcomes(UsageQuery { workspace_id: &test_workspace_id(), period: crank_core::UsagePeriod::Last24Hours, source: None, created_after: "2026-03-25T12:00:00Z", created_before: "2026-03-25T12:05:00Z", bucket: UsageBucket::Hour, }) .await .unwrap(); let by_group = usage .iter() .map(|item| (item.group, item.calls_total)) .collect::>(); assert_eq!(by_group.get(&UsageOutcomeGroup::Success), Some(&1)); assert_eq!(by_group.get(&UsageOutcomeGroup::Upstream), Some(&1)); assert_eq!(by_group.get(&UsageOutcomeGroup::Client), Some(&1)); assert_eq!(by_group.get(&UsageOutcomeGroup::Schema), Some(&1)); assert_eq!(by_group.get(&UsageOutcomeGroup::Crank), Some(&1)); let half_open = registry .summarize_usage(UsageQuery { workspace_id: &test_workspace_id(), period: crank_core::UsagePeriod::Last24Hours, source: None, created_after: "2026-03-25T12:00:00Z", created_before: "2026-03-25T12:04:00Z", bucket: UsageBucket::Hour, }) .await .unwrap(); assert_eq!(half_open.rollup.calls_total, 4); let retention = registry .delete_invocation_logs_before(timestamp("2026-03-25T12:02:00Z")) .await .unwrap(); assert_eq!(retention.deleted_records, 2); assert_eq!( retention.status, crank_registry::InvocationRetentionStatus::Completed ); assert_eq!(retention.policy.preserved_usage_window_days, 90); let retained = registry .list_invocation_logs(crank_registry::ListInvocationLogsQuery { workspace_id: &test_workspace_id(), level: None, status: None, outcome_group: None, search_text: None, source: None, operation_id: None, agent_id: None, created_after: Some("2026-03-25T12:00:00Z"), created_before: Some("2026-03-25T12:05:00Z"), cursor_created_at: None, cursor_id: None, limit: 10, }) .await .unwrap(); assert_eq!(retained.len(), 3); assert_eq!( retained.last().unwrap().log.id.as_str(), "log_history_client" ); database.cleanup().await; }