#![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, SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, 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 stores_versions_and_published_operations() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation_v1 = test_operation("op_rest_01", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &operation_v1, Some("alice")) .await .unwrap(); let operation_v2 = test_operation("op_rest_01", 2, OperationStatus::Draft); registry .create_version(CreateVersionRequest { workspace_id: &test_workspace_id(), snapshot: &operation_v2, change_note: Some("add output mapping"), created_by: Some("alice"), }) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation_v2.id, version: operation_v2.version, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); let summary = registry .get_operation_summary(&test_workspace_id(), &operation_v2.id) .await .unwrap() .unwrap(); let versions = registry .list_operation_versions(&test_workspace_id(), &operation_v2.id) .await .unwrap(); let published = registry .get_published_operation(&operation_v2.id) .await .unwrap() .unwrap(); let published_list = registry.list_published_operations().await.unwrap(); assert_eq!(summary.current_draft_version, 2); assert_eq!(summary.latest_published_version, Some(2)); assert_eq!(summary.status, OperationStatus::Published); assert_eq!(versions.len(), 2); assert_eq!( versions[1].change_note.as_deref(), Some("add output mapping") ); assert_eq!( versions[1] .snapshot .wizard_state .as_ref() .unwrap() .test_input, Some(json!({ "email": "test-v2@example.com" })) ); assert_eq!(published.version, 2); assert_eq!( published.wizard_state.as_ref().unwrap().output_sample, Some(json!({ "id": "lead_2" })) ); assert!(published.is_published()); assert_eq!(published_list, vec![published.clone()]); database.cleanup().await; } #[tokio::test] async fn rejects_out_of_order_versions() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_rest_02", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); let invalid = test_operation("op_rest_02", 3, OperationStatus::Draft); let error = registry .create_version(CreateVersionRequest { workspace_id: &test_workspace_id(), snapshot: &invalid, change_note: None, created_by: None, }) .await .unwrap_err(); assert!(matches!( error, RegistryError::InvalidVersionSequence { expected: 2, actual: 3, .. } )); database.cleanup().await; } #[tokio::test] async fn update_operation_draft_persists_optional_json_columns_as_sql_null() { let database = TestDatabase::new().await; let registry = database.registry().await; let mut operation = test_operation("op_rest_02b", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); operation.generated_draft = None; operation.samples = None; operation.config_export = None; operation.wizard_state = None; operation.updated_at = timestamp("2026-03-25T12:34:00Z"); registry .update_operation_draft(&test_workspace_id(), &operation) .await .unwrap(); let stored = registry .get_operation_version(&test_workspace_id(), &operation.id, operation.version) .await .unwrap() .unwrap(); assert_eq!(stored.snapshot.generated_draft, None); assert_eq!(stored.snapshot.samples, None); assert_eq!(stored.snapshot.config_export, None); assert_eq!(stored.snapshot.wizard_state, None); database.cleanup().await; } #[tokio::test] async fn stores_auth_profiles_and_artifact_metadata() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_rest_03", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); let auth_profile = AuthProfile { id: "auth_crank".into(), workspace_id: test_workspace_id(), name: "Crank API key".to_owned(), kind: AuthKind::ApiKeyHeader, config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig { header_name: "X-Api-Key".to_owned(), secret_id: SecretId::new("secret_crank_api_key"), }), created_at: timestamp("2026-03-25T12:00:00Z"), updated_at: timestamp("2026-03-25T12:00:00Z"), }; registry .save_auth_profile(SaveAuthProfileRequest { workspace_id: &test_workspace_id(), profile: &auth_profile, }) .await .unwrap(); let input_sample = OperationSampleMetadata { id: "sample_input".into(), operation_id: operation.id.clone(), version: 1, sample_kind: SampleKind::InputJson, storage_ref: "file:///tmp/input.json".to_owned(), content_type: "application/json".to_owned(), file_name: Some("input.json".to_owned()), created_at: timestamp("2026-03-25T12:01:00Z"), }; let descriptor = DescriptorMetadata { id: "descriptor_01".into(), operation_id: Some(operation.id.clone()), version: Some(1), descriptor_kind: DescriptorKind::DescriptorSet, storage_ref: "file:///tmp/schema.desc".to_owned(), source_name: Some("schema.desc".to_owned()), package_index: Some(json!({ "crm.v1": ["LeadService"] })), created_at: timestamp("2026-03-25T12:02:00Z"), }; registry .save_sample_metadata(SaveSampleMetadataRequest { sample: &input_sample, }) .await .unwrap(); registry .save_descriptor_metadata(SaveDescriptorMetadataRequest { descriptor: &descriptor, }) .await .unwrap(); let auth_profiles = registry .list_auth_profiles(&test_workspace_id()) .await .unwrap(); let samples = registry .list_sample_metadata(&operation.id, 1) .await .unwrap(); let descriptors = registry .list_descriptor_metadata(&operation.id, 1) .await .unwrap(); assert_eq!(auth_profiles, vec![auth_profile]); assert_eq!(samples, vec![input_sample]); assert_eq!(descriptors, vec![descriptor]); database.cleanup().await; } #[tokio::test] async fn lists_auth_profiles_referencing_secret() { let database = TestDatabase::new().await; let registry = database.registry().await; let primary_secret_id = SecretId::new("secret_primary"); let secondary_secret_id = SecretId::new("secret_secondary"); let profile = AuthProfile { id: "auth_crank".into(), workspace_id: test_workspace_id(), name: "Crank basic auth".to_owned(), kind: AuthKind::Basic, config: AuthConfig::Basic(crank_core::BasicAuthConfig { username_secret_id: primary_secret_id.clone(), password_secret_id: secondary_secret_id.clone(), }), created_at: timestamp("2026-03-25T12:00:00Z"), updated_at: timestamp("2026-03-25T12:00:00Z"), }; registry .save_auth_profile(SaveAuthProfileRequest { workspace_id: &test_workspace_id(), profile: &profile, }) .await .unwrap(); let profiles = registry .list_auth_profiles_referencing_secret(&test_workspace_id(), &primary_secret_id) .await .unwrap(); assert_eq!(profiles, vec![profile]); database.cleanup().await; }