#![allow(dead_code, unused_imports)] use super::common::*; use std::collections::BTreeMap; use crank_core::{ AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig, ApprovalRequest, ApprovalRequestId, ApprovalRequestStatus, AuthConfig, AuthKind, AuthProfile, ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, InvocationLog, MembershipRole, OperationApprovalRiskLevel, OperationId, OperationSecurityLevel, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyKind, 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, CreateApprovalRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata, FinishApprovalRequest, ListApprovalRequestsQuery, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind, SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, UpdateWorkspaceRequest, 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_and_finishes_yaml_import_jobs() { let database = TestDatabase::new().await; let registry = database.registry().await; let job_id = YamlImportJobId::new("job_yaml_01"); let operation = test_operation("op_rest_04", 1, OperationStatus::Draft); registry .create_operation(&test_workspace_id(), &operation, None) .await .unwrap(); registry .create_yaml_import_job(CreateYamlImportJobRequest { id: &job_id, source_sample_id: None, format_version: "v1", mode: ExportMode::Portable, created_at: ×tamp("2026-03-25T12:00:00Z"), }) .await .unwrap(); registry .finish_yaml_import_job( &job_id, &YamlImportJobCompletion { status: YamlImportJobStatus::Completed, result_operation_id: Some(operation.id.clone()), result_version: Some(2), error_text: None, finished_at: timestamp("2026-03-25T12:05:00Z"), }, ) .await .unwrap(); let job = registry .get_yaml_import_job(&job_id) .await .unwrap() .unwrap(); assert_eq!(job.status, YamlImportJobStatus::Completed); assert_eq!(job.result_version, Some(2)); assert_eq!(job.mode, ExportMode::Portable); database.cleanup().await; } #[tokio::test] async fn manages_workspace_read_paths() { let database = TestDatabase::new().await; let registry = database.registry().await; let workspace = Workspace { id: WorkspaceId::new("ws_extra_01"), slug: "extra".to_owned(), display_name: "Extra Workspace".to_owned(), status: crank_core::WorkspaceStatus::Active, settings: json!({"region":"eu"}), created_at: timestamp("2026-03-25T12:00:00Z"), updated_at: timestamp("2026-03-25T12:00:00Z"), }; let mut user = User { id: UserId::new("user_extra_01"), email: "owner@example.com".to_owned(), display_name: "Owner".to_owned(), status: crank_core::UserStatus::Active, created_at: timestamp("2026-03-25T11:00:00Z"), }; registry .create_workspace(CreateWorkspaceRequest { workspace: &workspace, }) .await .unwrap(); let user_id = registry .upsert_bootstrap_user(&user.email, &user.display_name, "hashed-password") .await .unwrap(); user.id = user_id; registry .ensure_membership(&workspace.id, &user.id, MembershipRole::Owner) .await .unwrap(); let all_workspaces = registry.list_workspaces().await.unwrap(); let user_workspaces = registry.list_workspaces_for_user(&user.id).await.unwrap(); let memberships = registry.list_memberships(&workspace.id).await.unwrap(); let loaded = registry .get_workspace(&workspace.id) .await .unwrap() .unwrap(); assert!( all_workspaces .iter() .any(|record| record.workspace == workspace) ); assert_eq!(user_workspaces.len(), 1); assert_eq!(user_workspaces[0].workspace, workspace); assert_eq!(user_workspaces[0].role, MembershipRole::Owner); assert_eq!(memberships.len(), 1); assert_eq!(memberships[0].workspace_id, workspace.id); assert_eq!(memberships[0].user.id, user.id); assert_eq!(memberships[0].user.email, user.email); assert_eq!(memberships[0].user.display_name, user.display_name); assert_eq!(memberships[0].user.status, user.status); assert_eq!(memberships[0].role, MembershipRole::Owner); assert_eq!(loaded, WorkspaceRecord { workspace }); database.cleanup().await; } #[tokio::test] async fn lists_default_workspace_first_for_community_sessions() { let database = TestDatabase::new().await; let registry = database.registry().await; let default_workspace = Workspace { id: WorkspaceId::new("ws_default"), slug: "solo".to_owned(), display_name: "Solo".to_owned(), status: crank_core::WorkspaceStatus::Active, settings: json!({}), created_at: timestamp("2026-03-25T12:00:00Z"), updated_at: timestamp("2026-03-25T12:00:00Z"), }; let legacy_workspace = Workspace { id: WorkspaceId::new("ws_growth_lab"), slug: "growth-lab".to_owned(), display_name: "Growth Lab".to_owned(), status: crank_core::WorkspaceStatus::Active, settings: json!({}), created_at: timestamp("2026-03-25T12:01:00Z"), updated_at: timestamp("2026-03-25T12:01:00Z"), }; let user_id = registry .upsert_bootstrap_user("community-owner@example.com", "Owner", "hashed-password") .await .unwrap(); registry .create_workspace(CreateWorkspaceRequest { workspace: &legacy_workspace, }) .await .unwrap(); registry .update_workspace(UpdateWorkspaceRequest { workspace: &default_workspace, }) .await .unwrap(); registry .ensure_membership(&legacy_workspace.id, &user_id, MembershipRole::Owner) .await .unwrap(); registry .ensure_membership(&default_workspace.id, &user_id, MembershipRole::Owner) .await .unwrap(); let workspaces = registry.list_workspaces_for_user(&user_id).await.unwrap(); assert_eq!(workspaces[0].workspace.id, WorkspaceId::new("ws_default")); assert_eq!(workspaces[0].workspace.slug, "solo"); let session_id = UserSessionId::new("sess_workspace_default_first"); let secret_hash = "test-secret-hash"; registry .create_user_session( &session_id, &user_id, Some(&legacy_workspace.id), secret_hash, ×tamp("2027-03-25T13:00:00Z"), ) .await .unwrap(); let session = registry .get_user_session(&session_id, secret_hash) .await .unwrap() .unwrap(); assert_eq!( session.current_workspace_id, Some(WorkspaceId::new("ws_default")) ); database.cleanup().await; } #[tokio::test] async fn manages_user_profile_and_workspace_access_reads() { let database = TestDatabase::new().await; let registry = database.registry().await; let workspace = Workspace { id: WorkspaceId::new("ws_profile_01"), slug: "profile".to_owned(), display_name: "Profile Workspace".to_owned(), status: crank_core::WorkspaceStatus::Active, settings: json!({}), created_at: timestamp("2026-03-25T12:00:00Z"), updated_at: timestamp("2026-03-25T12:00:00Z"), }; let other_workspace = Workspace { id: WorkspaceId::new("ws_profile_02"), slug: "profile-other".to_owned(), display_name: "Other Workspace".to_owned(), status: crank_core::WorkspaceStatus::Active, settings: json!({}), created_at: timestamp("2026-03-25T12:01:00Z"), updated_at: timestamp("2026-03-25T12:01:00Z"), }; let user_id = registry .upsert_bootstrap_user("profile@example.com", "Owner", "hashed-password") .await .unwrap(); registry .create_workspace(CreateWorkspaceRequest { workspace: &workspace, }) .await .unwrap(); registry .create_workspace(CreateWorkspaceRequest { workspace: &other_workspace, }) .await .unwrap(); registry .ensure_membership(&workspace.id, &user_id, MembershipRole::Owner) .await .unwrap(); let updated = registry .update_user_profile(&user_id, "updated@example.com", "Updated Owner") .await .unwrap(); let has_access = registry .user_has_workspace_access(&user_id, &workspace.id) .await .unwrap(); let lacks_access = registry .user_has_workspace_access(&user_id, &other_workspace.id) .await .unwrap(); assert_eq!(updated.id, user_id); assert_eq!(updated.email, "updated@example.com"); assert_eq!(updated.display_name, "Updated Owner"); assert!(has_access); assert!(!lacks_access); assert!(updated.created_at.unix_timestamp() > 0); database.cleanup().await; } #[tokio::test] async fn creates_and_loads_user_sessions_with_typed_expiration() { let database = TestDatabase::new().await; let registry = database.registry().await; let workspace = Workspace { id: WorkspaceId::new("ws_session_01"), slug: "session".to_owned(), display_name: "Session Workspace".to_owned(), status: crank_core::WorkspaceStatus::Active, settings: json!({}), created_at: timestamp("2026-03-25T12:00:00Z"), updated_at: timestamp("2026-03-25T12:00:00Z"), }; let user_id = registry .upsert_bootstrap_user("session@example.com", "Owner", "hashed-password") .await .unwrap(); let session_id = UserSessionId::new("sess_01"); let expires_at = timestamp("2030-04-06T12:05:00Z"); registry .create_workspace(CreateWorkspaceRequest { workspace: &workspace, }) .await .unwrap(); registry .ensure_membership(&workspace.id, &user_id, MembershipRole::Owner) .await .unwrap(); registry .create_user_session( &session_id, &user_id, Some(&workspace.id), "secret-hash-01", &expires_at, ) .await .unwrap(); let session = registry .get_user_session(&session_id, "secret-hash-01") .await .unwrap() .unwrap(); assert_eq!(session.session_id, session_id); assert_eq!(session.current_workspace_id, Some(workspace.id.clone())); assert_eq!(session.user.id, user_id); assert!(session.user.created_at.unix_timestamp() > 0); database.cleanup().await; } #[tokio::test] async fn manages_platform_api_key_read_paths() { let database = TestDatabase::new().await; let registry = database.registry().await; let workspace = Workspace { id: WorkspaceId::new("ws_keys_01"), slug: "keys".to_owned(), display_name: "Keys Workspace".to_owned(), status: crank_core::WorkspaceStatus::Active, settings: json!({}), created_at: timestamp("2026-03-25T12:00:00Z"), updated_at: timestamp("2026-03-25T12:00:00Z"), }; let agent = test_agent("agent_keys_01", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let api_key = PlatformApiKey { id: PlatformApiKeyId::new("key_01"), workspace_id: workspace.id.clone(), agent_id: Some(agent.id.clone()), key_kind: PlatformApiKeyKind::McpClient, name: "Primary".to_owned(), prefix: "crk_live".to_owned(), scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], status: PlatformApiKeyStatus::Active, created_at: timestamp("2026-03-25T12:01:00Z"), last_used_at: None, expires_at: None, allowed_origins: Vec::new(), }; let secret_hash = "secret_hash_01"; registry .create_workspace(CreateWorkspaceRequest { workspace: &workspace, }) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: &[], }) .await .unwrap(); registry .create_platform_api_key(CreatePlatformApiKeyRequest { api_key: &api_key, secret_hash, }) .await .unwrap(); let listed = registry .list_platform_api_keys(&workspace.id) .await .unwrap(); let listed_for_agent = registry .list_platform_api_keys_for_agent(&workspace.id, &agent.id) .await .unwrap(); assert_eq!( listed, vec![PlatformApiKeyRecord { api_key: api_key.clone() }] ); assert_eq!( listed_for_agent, vec![PlatformApiKeyRecord { api_key: api_key.clone() }] ); let resolved = registry .get_platform_api_key_by_secret_for_agent_slug(&workspace.slug, &agent.slug, secret_hash) .await .unwrap() .unwrap(); assert_eq!(resolved.api_key, api_key); registry .touch_platform_api_key( &workspace.id, &PlatformApiKeyId::new("key_01"), ×tamp("2026-03-25T12:05:00Z"), ) .await .unwrap(); let touched = registry .list_platform_api_keys(&workspace.id) .await .unwrap(); assert_eq!( touched[0].api_key.last_used_at, Some(timestamp("2026-03-25T12:05:00Z")) ); database.cleanup().await; } #[tokio::test] async fn manages_approval_request_lifecycle() { let database = TestDatabase::new().await; let registry = database.registry().await; let workspace_id = test_workspace_id(); let operation = test_operation("op_approval_01", 1, OperationStatus::Draft); let agent = test_agent("agent_approval_01", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let approval_key = PlatformApiKey { id: PlatformApiKeyId::new("key_approval_01"), workspace_id: workspace_id.clone(), agent_id: Some(agent.id.clone()), key_kind: PlatformApiKeyKind::Approval, name: "Approval key".to_owned(), prefix: "crk_appr".to_owned(), scopes: vec![PlatformApiKeyScope::Approve, PlatformApiKeyScope::Deny], status: PlatformApiKeyStatus::Active, created_at: timestamp("2026-03-25T12:00:00Z"), last_used_at: None, expires_at: None, allowed_origins: Vec::new(), }; let approval = ApprovalRequest { id: ApprovalRequestId::new("approval_01"), workspace_id: workspace_id.clone(), agent_id: agent.id.clone(), operation_id: operation.id.clone(), operation_version: 1, status: ApprovalRequestStatus::Pending, risk_level: OperationApprovalRiskLevel::Dangerous, confirmation_title: "Confirm action".to_owned(), confirmation_body: "Check payload before running.".to_owned(), request_payload: json!({"amount": 100}), response_payload: None, created_at: timestamp("2026-03-25T12:01:00Z"), expires_at: timestamp("2027-03-25T12:06:00Z"), decided_at: None, decided_by_key_id: None, decision_note: None, }; registry .create_operation(&workspace_id, &operation, None) .await .unwrap(); registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: &[], }) .await .unwrap(); registry .create_platform_api_key(CreatePlatformApiKeyRequest { api_key: &approval_key, secret_hash: "approval-secret-hash", }) .await .unwrap(); registry .create_approval_request(CreateApprovalRequest { approval: &approval, }) .await .unwrap(); let pending = registry .list_pending_approval_requests_for_agent(&workspace_id, &agent.id) .await .unwrap(); assert_eq!(pending.len(), 1); assert_eq!(pending[0].approval, approval); let workspace_approvals = registry .list_approval_requests(ListApprovalRequestsQuery { workspace_id: &workspace_id, status: None, limit: 10, }) .await .unwrap(); assert_eq!(workspace_approvals.len(), 1); assert_eq!(workspace_approvals[0].approval.id, approval.id); let fetched = registry .get_approval_request(&workspace_id, &approval.id) .await .unwrap() .unwrap(); assert_eq!(fetched.approval, approval); let decided = registry .decide_approval_request(DecideApprovalRequest { workspace_id: &workspace_id, agent_id: &agent.id, approval_id: &approval.id, status: ApprovalRequestStatus::Approved, decided_at: timestamp("2026-03-25T12:02:00Z"), decided_by_key_id: &approval_key.id, response_payload: Some(json!({"approve": "yes"})), decision_note: Some("confirmed in test"), }) .await .unwrap() .unwrap(); assert_eq!(decided.approval.status, ApprovalRequestStatus::Approved); assert_eq!( decided.approval.decided_by_key_id, Some(approval_key.id.clone()) ); assert_eq!( decided.approval.response_payload, Some(json!({"approve": "yes"})) ); let completed = registry .finish_approval_request(FinishApprovalRequest { workspace_id: &workspace_id, agent_id: &agent.id, approval_id: &approval.id, status: ApprovalRequestStatus::Completed, response_payload: Some(json!({"id": "lead_123"})), decision_note: None, }) .await .unwrap() .unwrap(); assert_eq!(completed.approval.status, ApprovalRequestStatus::Completed); assert_eq!( completed.approval.response_payload, Some(json!({"id": "lead_123"})) ); let completed_by_status = registry .list_approval_requests(ListApprovalRequestsQuery { workspace_id: &workspace_id, status: Some(ApprovalRequestStatus::Completed), limit: 10, }) .await .unwrap(); assert_eq!(completed_by_status.len(), 1); assert_eq!( completed_by_status[0].approval.status, ApprovalRequestStatus::Completed ); let pending_after_decision = registry .list_pending_approval_requests_for_agent(&workspace_id, &agent.id) .await .unwrap(); assert!(pending_after_decision.is_empty()); let repeated_decision = registry .decide_approval_request(DecideApprovalRequest { workspace_id: &workspace_id, agent_id: &agent.id, approval_id: &approval.id, status: ApprovalRequestStatus::Denied, decided_at: timestamp("2026-03-25T12:03:00Z"), decided_by_key_id: &approval_key.id, response_payload: Some(json!({"approve": "no"})), decision_note: None, }) .await .unwrap(); assert!(repeated_decision.is_none()); database.cleanup().await; }