#![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, Secret, SecretId, SecretKind, SecretStatus, 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, CreateSecretRequest, CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata, MASTER_KEY_CIPHER_CONTRACT, MasterKeyIdentityCandidate, 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() } async fn create_test_secret(registry: &PostgresRegistry, id: &SecretId, name: &str) { registry .verify_or_register_master_key_identity(MasterKeyIdentityCandidate { epoch: 1, fingerprint: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", cipher_contract: MASTER_KEY_CIPHER_CONTRACT, observed_at: ×tamp("2026-03-25T12:00:00Z"), }) .await .unwrap(); let secret = Secret { id: id.clone(), workspace_id: test_workspace_id(), name: name.to_owned(), kind: SecretKind::Token, status: SecretStatus::Active, current_version: 1, created_at: timestamp("2026-03-25T12:00:00Z"), updated_at: timestamp("2026-03-25T12:00:00Z"), last_used_at: None, }; registry .create_secret(CreateSecretRequest { secret: &secret, ciphertext: "test-ciphertext", key_version: "test-key-v1", master_key_epoch: 1, created_by: None, }) .await .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::OperationStaleVersion { 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 + 1) .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 published_version_is_not_rewritten_by_a_later_save() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_immutable_publish", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &operation, Some("alice")) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation.id, version: 1, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); let published_before = registry .get_published_operation(&operation.id) .await .unwrap() .unwrap(); let mut changed = operation.clone(); changed.display_name = "MUTATED AFTER PUBLISH".to_owned(); changed.target = Target::Rest(RestTarget { base_url: "https://mutated.example.com".to_owned(), method: HttpMethod::Post, path_template: "/mutated".to_owned(), static_headers: BTreeMap::new(), }); changed.updated_at = timestamp("2026-03-25T12:20:00Z"); registry .update_operation_draft(&test_workspace_id(), &changed) .await .unwrap(); let published_after = registry .get_published_operation(&operation.id) .await .unwrap() .unwrap(); assert_eq!(published_after, published_before); assert_eq!( registry .get_operation_summary(&test_workspace_id(), &operation.id) .await .unwrap() .unwrap() .current_draft_version, 2, "saving after publish must append a new Draft revision" ); database.cleanup().await; } #[tokio::test] async fn database_guard_rejects_published_update_and_parent_cascade_delete() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_db_immutable", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &operation, Some("alice")) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation.id, version: 1, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); let update = sqlx::query( "update operation_versions set display_name = 'tampered' where operation_id = $1 and version = 1", ) .bind(operation.id.as_str()) .execute(registry.pool()) .await; assert!(update.is_err()); let rewind = sqlx::query("update operations set latest_published_version = null where id = $1") .bind(operation.id.as_str()) .execute(registry.pool()) .await; assert!(rewind.is_err()); let pointer_delete = sqlx::query("delete from published_operations where operation_id = $1") .bind(operation.id.as_str()) .execute(registry.pool()) .await; assert!(pointer_delete.is_err()); let delete = sqlx::query("delete from operations where id = $1") .bind(operation.id.as_str()) .execute(registry.pool()) .await; assert!(delete.is_err()); assert!( registry .get_published_operation(&operation.id) .await .unwrap() .is_some() ); database.cleanup().await; } #[tokio::test] async fn concurrent_saves_from_one_base_have_one_winner_and_no_version_gap() { let database = TestDatabase::new().await; let registry = database.registry().await; let operation = test_operation("op_concurrent_save", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &operation, Some("alice")) .await .unwrap(); let mut tasks = Vec::new(); for contender in 0..32_u32 { let registry = registry.clone(); let mut candidate = operation.clone(); candidate.display_name = format!("Contender {contender}"); candidate.updated_at = timestamp("2026-03-25T12:20:00Z"); tasks.push(tokio::spawn(async move { registry .update_operation_draft(&test_workspace_id(), &candidate) .await })); } let mut successes = 0; let mut stale = 0; for task in tasks { match task.await.unwrap() { Ok(()) => successes += 1, Err(RegistryError::OperationStaleVersion { .. }) => stale += 1, Err(error) => panic!("unexpected contender error: {error}"), } } assert_eq!(successes, 1); assert_eq!(stale, 31); let versions = registry .list_operation_versions(&test_workspace_id(), &operation.id) .await .unwrap(); assert_eq!( versions .iter() .map(|record| record.version) .collect::>(), vec![1, 2] ); database.cleanup().await; } #[tokio::test] async fn concurrent_same_name_create_maps_unique_loser_to_typed_conflict() { let database = TestDatabase::new().await; let registry = database.registry().await; let first = test_operation("op_same_name_first", 1, OperationStatus::Draft); let mut second = test_operation("op_same_name_second", 1, OperationStatus::Draft); second.name = first.name.clone(); let first_registry = registry.clone(); let second_registry = registry.clone(); let workspace = test_workspace_id(); let first_workspace = workspace.clone(); let second_workspace = workspace.clone(); let (first_result, second_result) = tokio::join!( async move { first_registry .create_operation(&first_workspace, &first, Some("alice")) .await }, async move { second_registry .create_operation(&second_workspace, &second, Some("alice")) .await } ); let results = [first_result, second_result]; assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1); assert_eq!( results .iter() .filter(|result| matches!(result, Err(RegistryError::OperationAlreadyExists { .. }))) .count(), 1 ); database.cleanup().await; } #[tokio::test] async fn archive_preserves_published_version_and_delete_is_restricted_to_unused_draft() { let database = TestDatabase::new().await; let registry = database.registry().await; let published = test_operation("op_archive_preserve", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &published, Some("alice")) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &published.id, version: 1, published_at: ×tamp("2026-03-25T12:10:00Z"), published_by: Some("alice"), }) .await .unwrap(); registry .archive_operation( &test_workspace_id(), &published.id, ×tamp("2026-03-25T12:20:00Z"), ) .await .unwrap(); registry .archive_operation( &test_workspace_id(), &published.id, ×tamp("2026-03-25T12:21:00Z"), ) .await .unwrap(); assert!(matches!( registry .delete_operation(&test_workspace_id(), &published.id) .await, Err(RegistryError::OperationDeleteForbidden { .. }) )); assert_eq!( registry .get_published_operation(&published.id) .await .unwrap() .unwrap() .status, OperationStatus::Published ); let draft = test_operation("op_delete_unused", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &draft, Some("alice")) .await .unwrap(); registry .delete_operation(&test_workspace_id(), &draft.id) .await .unwrap(); assert!( registry .get_operation_summary(&test_workspace_id(), &draft.id) .await .unwrap() .is_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(); create_test_secret( ®istry, &SecretId::new("secret_crank_api_key"), "Crank API key", ) .await; 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"); create_test_secret(®istry, &primary_secret_id, "Primary secret").await; create_test_secret(®istry, &secondary_secret_id, "Secondary secret").await; 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; }