#![allow(dead_code, unused_imports)] use super::common::*; use crank_core::{ AgentOperationBinding, AgentStatus, InvocationSource, InvocationStatus, OnboardingStepId, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyKind, PlatformApiKeyScope, PlatformApiKeyStatus, ProductEventId, Workspace, WorkspaceId, }; use crank_registry::{ CreateAgentRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateWorkspaceRequest, OnboardingPresentationMilestone, PublishAgentRequest, PublishRequest, RecordOnboardingMilestoneRequest, }; use serde_json::json; use sqlx::Row; use time::{Duration, OffsetDateTime}; #[path = "onboarding_lifecycle.rs"] mod onboarding_lifecycle; fn truncate_to_micros(value: OffsetDateTime) -> OffsetDateTime { value .replace_nanosecond((value.nanosecond() / 1_000) * 1_000) .unwrap() } #[tokio::test] async fn migration_exposes_product_events_and_exact_invocation_key_scope() { let database = TestDatabase::new().await; let _registry = database.registry().await; let pool = database.raw_pool().await; let product_events = relation_exists(&pool, "product_events").await; let product_rollups = relation_exists(&pool, "product_event_daily_rollups").await; let key_scope = column_exists(&pool, "invocation_logs", "platform_api_key_id").await; assert!(product_events, "V11 must own immutable local ProductEvents"); assert!( product_rollups, "V11 must preserve bounded UTC product-metric denominators" ); assert!( key_scope, "successful MCP evidence must retain the exact non-secret key identity" ); database.cleanup().await; } #[tokio::test] async fn product_event_idempotency_is_scoped_to_workspace() { let database = TestDatabase::new().await; let registry = database.registry().await; let pool = database.raw_pool().await; create_workspace(®istry, "ws_onboarding_other", "onboarding-other").await; insert_product_event( &pool, "pe_start_default", "ws_default", "onboarding_started", "eligible:first-login", ) .await .unwrap(); let replay = insert_product_event( &pool, "pe_start_replay", "ws_default", "onboarding_started", "eligible:first-login", ) .await; assert!( replay.is_err(), "same workspace/idempotency key must not double-count a milestone" ); insert_product_event( &pool, "pe_start_other", "ws_onboarding_other", "onboarding_started", "eligible:first-login", ) .await .expect("an independent workspace may use the same local idempotency key"); let count: i64 = sqlx::query_scalar("select count(*) from product_events where idempotency_key = $1") .bind("eligible:first-login") .fetch_one(&pool) .await .unwrap(); assert_eq!(count, 2); database.cleanup().await; } #[tokio::test] async fn exact_key_success_is_authoritative_and_revocation_regresses_progress() { let database = TestDatabase::new().await; let registry = database.registry().await; let pool = database.raw_pool().await; let (operation_id, agent_id, key_id) = published_path(®istry).await; let log = test_invocation_log( "log_onboarding_first_call", &operation_id, Some(agent_id.clone()), InvocationStatus::Ok, 25, "2026-03-25T12:15:00Z", ); assert_eq!( registry .create_invocation_log(CreateInvocationLogRequest { log: &log }) .await, crank_registry::InvocationHistoryWriteOutcome::Recorded ); sqlx::query("update invocation_logs set platform_api_key_id = $1 where id = $2") .bind(key_id.as_str()) .bind(log.id.as_str()) .execute(&pool) .await .unwrap(); assert!( authoritative_first_call(&pool, "ws_default", agent_id.as_str(), key_id.as_str()).await, "an active exact-scope key and real successful Agent tool invocation complete first value" ); registry .revoke_platform_api_key_for_agent( &WorkspaceId::new("ws_default"), &agent_id, &key_id, &OffsetDateTime::now_utc(), ) .await .unwrap(); assert!( !authoritative_first_call(&pool, "ws_default", agent_id.as_str(), key_id.as_str()).await, "revocation must return connection/onboarding to an actionable state" ); database.cleanup().await; } #[tokio::test] async fn invocation_key_scope_rejects_cross_workspace_evidence() { let database = TestDatabase::new().await; let registry = database.registry().await; let pool = database.raw_pool().await; assert!( column_exists(&pool, "invocation_logs", "platform_api_key_id").await, "cross-workspace rejection requires the exact-key evidence column" ); let (operation_id, agent_id, _key_id) = published_path(®istry).await; create_workspace(®istry, "ws_onboarding_foreign", "onboarding-foreign").await; let foreign_key = PlatformApiKey { id: PlatformApiKeyId::new("pk_onboarding_foreign"), workspace_id: WorkspaceId::new("ws_onboarding_foreign"), agent_id: None, key_kind: PlatformApiKeyKind::McpClient, name: "foreign-onboarding-key".to_owned(), prefix: "crk_foreign".to_owned(), scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], status: PlatformApiKeyStatus::Active, created_at: OffsetDateTime::now_utc(), last_used_at: None, expires_at: None, allowed_origins: Vec::new(), }; registry .create_platform_api_key(CreatePlatformApiKeyRequest { api_key: &foreign_key, secret_hash: "foreign-secret-hash", }) .await .unwrap(); let log = test_invocation_log( "log_onboarding_cross_workspace", &operation_id, Some(agent_id), InvocationStatus::Ok, 25, "2026-03-25T12:16:00Z", ); assert_eq!( registry .create_invocation_log(CreateInvocationLogRequest { log: &log }) .await, crank_registry::InvocationHistoryWriteOutcome::Recorded ); let poisoned = sqlx::query( "update invocation_logs set platform_api_key_id = $1 where workspace_id = $2 and id = $3", ) .bind(foreign_key.id.as_str()) .bind("ws_default") .bind(log.id.as_str()) .execute(&pool) .await; assert!( poisoned.is_err(), "database constraints must prevent foreign-workspace key evidence" ); database.cleanup().await; } #[tokio::test] async fn projection_moves_from_empty_to_exact_terminal_success_and_regresses_on_revoke() { let database = TestDatabase::new().await; let registry = database.registry().await; let workspace_id = WorkspaceId::new("ws_default"); let empty = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!(!empty.completed); assert_eq!( empty.steps.iter().map(|step| step.id).collect::>(), OnboardingStepId::ORDERED ); let (operation_id, agent_id, key_id) = published_path(®istry).await; let mut test_log = test_invocation_log( "log_onboarding_admin_test", &operation_id, None, InvocationStatus::Ok, 20, "2026-03-25T12:14:00Z", ); test_log.source = InvocationSource::AdminTestRun; registry .create_invocation_log(CreateInvocationLogRequest { log: &test_log }) .await; registry .touch_platform_api_key(&workspace_id, &key_id, &OffsetDateTime::now_utc()) .await .unwrap(); let mut pending = test_invocation_log( "log_onboarding_approval_pending", &operation_id, Some(agent_id.clone()), InvocationStatus::Ok, 10, "2026-03-25T12:15:00Z", ); pending.platform_api_key_id = Some(key_id.clone()); pending.execution_stage = Some(crank_core::ExecutionStage::MandatoryPersistence); pending.created_at = OffsetDateTime::now_utc(); registry .create_invocation_log(CreateInvocationLogRequest { log: &pending }) .await; let not_terminal = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!(!not_terminal.completed); assert!( !not_terminal .step(OnboardingStepId::FirstCall) .unwrap() .completed ); let mut success = test_invocation_log( "log_onboarding_terminal_success", &operation_id, Some(agent_id.clone()), InvocationStatus::Ok, 25, "2026-03-25T12:16:00Z", ); success.platform_api_key_id = Some(key_id.clone()); success.created_at = truncate_to_micros(OffsetDateTime::now_utc()); registry .create_invocation_log(CreateInvocationLogRequest { log: &success }) .await; let complete = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!(complete.completed); assert_eq!(complete.platform_api_key_id, Some(key_id.clone())); assert_eq!(complete.first_call_log_id, Some(success.id)); assert_eq!( complete.first_call_tool_name.as_deref(), Some("create_lead") ); assert_eq!(complete.first_call_at, Some(success.created_at)); // A different active key/path must not replace the exact path that produced // the terminal onboarding call when that exact key is later revoked. let (_other_agent_id, _other_key_id) = publish_additional_agent_path(®istry, &operation_id).await; registry .revoke_platform_api_key_for_agent( &workspace_id, &agent_id, &key_id, &OffsetDateTime::now_utc(), ) .await .unwrap(); let regressed = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!(!regressed.completed); assert!(!regressed.step(OnboardingStepId::Key).unwrap().completed); assert!( !regressed .step(OnboardingStepId::FirstCall) .unwrap() .completed ); database.cleanup().await; } #[tokio::test] async fn current_agent_publication_and_key_expiry_invalidate_old_first_call_evidence() { let database = TestDatabase::new().await; let registry = database.registry().await; let pool = database.raw_pool().await; let workspace_id = WorkspaceId::new("ws_default"); let (operation_id, agent_id, key_id) = published_path(®istry).await; record_onboarding_test_and_call( ®istry, &operation_id, &agent_id, &key_id, "publication_before_republish", OffsetDateTime::now_utc(), ) .await; let completed = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!(completed.completed); let unpublished_at = OffsetDateTime::now_utc() + Duration::seconds(1); registry .unpublish_agent(&workspace_id, &agent_id, &unpublished_at, None) .await .unwrap(); let unpublished = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!(!unpublished.step(OnboardingStepId::Agent).unwrap().completed); let republished_at = unpublished_at + Duration::seconds(1); registry .publish_agent(PublishAgentRequest { workspace_id: &workspace_id, agent_id: &agent_id, version: 1, published_at: &republished_at, published_by: Some("onboarding-republish-test"), expected_state: None, }) .await .unwrap(); let republished = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!( !republished.step(OnboardingStepId::Agent).unwrap().completed, "republishing must not silently replace the exact catalog revision that completed onboarding" ); assert!( !republished .step(OnboardingStepId::FirstCall) .unwrap() .completed, "a call from an older Agent publication must not complete the current catalog revision" ); assert_ne!(republished.revision, completed.revision); let reset = registry .reset_onboarding_selection( &workspace_id, republished.revision, republished_at + Duration::milliseconds(1), ) .await .unwrap(); assert!(reset.step(OnboardingStepId::Agent).unwrap().completed); assert!(reset.step(OnboardingStepId::Key).unwrap().completed); record_onboarding_test_and_call( ®istry, &operation_id, &agent_id, &key_id, "publication_after_republish", republished_at + Duration::seconds(1), ) .await; let completed_again = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!(completed_again.completed); sqlx::query( "update platform_api_keys set expires_at = now() - interval '1 minute' where id = $1", ) .bind(key_id.as_str()) .execute(&pool) .await .unwrap(); let expired = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!(!expired.step(OnboardingStepId::Key).unwrap().completed); assert!(!expired.step(OnboardingStepId::FirstCall).unwrap().completed); assert_ne!(expired.revision, completed_again.revision); database.cleanup().await; } #[tokio::test] async fn archiving_selected_operation_or_agent_regresses_authoritative_projection() { let database = TestDatabase::new().await; let registry = database.registry().await; let workspace_id = WorkspaceId::new("ws_default"); let (operation_id, agent_id, key_id) = published_path(®istry).await; record_onboarding_test_and_call( ®istry, &operation_id, &agent_id, &key_id, "archive_agent_path", OffsetDateTime::now_utc(), ) .await; assert!( registry .get_onboarding_projection(&workspace_id) .await .unwrap() .completed ); // A second healthy path must not replace the exact lineage that produced // the authoritative first call when that lineage is later archived. let (alternative_operation_id, _, _) = publish_named_path( ®istry, "op_onboarding_alternative", "agent_onboarding_alternative", "pk_onboarding_alternative", ) .await; registry .archive_agent( &workspace_id, &agent_id, &(OffsetDateTime::now_utc() + Duration::seconds(1)), None, ) .await .unwrap(); let archived_agent = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert_eq!(archived_agent.operation_id, Some(operation_id.clone())); assert_eq!(archived_agent.agent_id, Some(agent_id)); assert_ne!(archived_agent.operation_id, Some(alternative_operation_id)); assert!( !archived_agent .step(OnboardingStepId::Agent) .unwrap() .completed ); assert!( !archived_agent .step(OnboardingStepId::FirstCall) .unwrap() .completed ); registry .archive_operation( &workspace_id, &operation_id, &(OffsetDateTime::now_utc() + Duration::seconds(2)), ) .await .unwrap(); let archived_operation = registry .get_onboarding_projection(&workspace_id) .await .unwrap(); assert!( !archived_operation .step(OnboardingStepId::Operation) .unwrap() .completed ); database.cleanup().await; } async fn published_path( registry: &crank_registry::PostgresRegistry, ) -> ( crank_core::OperationId, crank_core::AgentId, PlatformApiKeyId, ) { publish_named_path( registry, "op_onboarding", "agent_onboarding", "pk_onboarding_exact", ) .await } async fn publish_named_path( registry: &crank_registry::PostgresRegistry, operation_id: &str, agent_id: &str, key_id: &str, ) -> ( crank_core::OperationId, crank_core::AgentId, PlatformApiKeyId, ) { let workspace_id = WorkspaceId::new("ws_default"); let operation = test_operation(operation_id, 1, OperationStatus::Draft); registry .create_operation(&workspace_id, &operation, None) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &workspace_id, operation_id: &operation.id, version: 1, published_at: &OffsetDateTime::now_utc(), published_by: Some("onboarding-test"), }) .await .unwrap(); let agent = test_agent(agent_id, AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let bindings = vec![AgentOperationBinding { operation_id: operation.id.clone(), operation_version: 1, tool_name: "create_lead".to_owned(), tool_title: "Create lead".to_owned(), tool_description_override: None, enabled: true, }]; registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: &bindings, }) .await .unwrap(); registry .publish_agent(PublishAgentRequest { workspace_id: &workspace_id, agent_id: &agent.id, version: 1, published_at: &OffsetDateTime::now_utc(), published_by: Some("onboarding-test"), expected_state: None, }) .await .unwrap(); let key_id = PlatformApiKeyId::new(key_id); let key = PlatformApiKey { id: key_id.clone(), workspace_id, agent_id: Some(agent.id.clone()), key_kind: PlatformApiKeyKind::McpClient, name: format!("onboarding-{key_id}-key"), prefix: format!("crk_{}", key_id.as_str()), scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], status: PlatformApiKeyStatus::Active, created_at: OffsetDateTime::now_utc(), last_used_at: None, expires_at: None, allowed_origins: Vec::new(), }; registry .create_platform_api_key(CreatePlatformApiKeyRequest { api_key: &key, secret_hash: "exact-secret-hash", }) .await .unwrap(); (operation.id, agent.id, key_id) } async fn record_onboarding_test_and_call( registry: &crank_registry::PostgresRegistry, operation_id: &crank_core::OperationId, agent_id: &crank_core::AgentId, key_id: &PlatformApiKeyId, suffix: &str, occurred_at: OffsetDateTime, ) { let mut test_log = test_invocation_log( &format!("log_onboarding_test_{suffix}"), operation_id, None, InvocationStatus::Ok, 20, "2026-03-25T12:14:00Z", ); test_log.source = InvocationSource::AdminTestRun; test_log.created_at = truncate_to_micros(occurred_at); registry .create_invocation_log(CreateInvocationLogRequest { log: &test_log }) .await; registry .touch_platform_api_key( &WorkspaceId::new("ws_default"), key_id, &truncate_to_micros(occurred_at), ) .await .unwrap(); record_onboarding_call( registry, operation_id, agent_id, key_id, suffix, occurred_at, ) .await; } async fn record_onboarding_call( registry: &crank_registry::PostgresRegistry, operation_id: &crank_core::OperationId, agent_id: &crank_core::AgentId, key_id: &PlatformApiKeyId, suffix: &str, occurred_at: OffsetDateTime, ) { let mut success = test_invocation_log( &format!("log_onboarding_call_{suffix}"), operation_id, Some(agent_id.clone()), InvocationStatus::Ok, 25, "2026-03-25T12:16:00Z", ); success.platform_api_key_id = Some(key_id.clone()); success.created_at = truncate_to_micros(occurred_at); registry .create_invocation_log(CreateInvocationLogRequest { log: &success }) .await; } async fn publish_additional_agent_path( registry: &crank_registry::PostgresRegistry, operation_id: &crank_core::OperationId, ) -> (crank_core::AgentId, PlatformApiKeyId) { let workspace_id = WorkspaceId::new("ws_default"); let agent = test_agent("agent_onboarding_later", AgentStatus::Draft); let version = test_agent_version(&agent.id, 1, AgentStatus::Draft); let bindings = vec![AgentOperationBinding { operation_id: operation_id.clone(), operation_version: 1, tool_name: "create_lead".to_owned(), tool_title: "Create lead".to_owned(), tool_description_override: None, enabled: true, }]; registry .create_agent(CreateAgentRequest { agent: &agent, version: &version, bindings: &bindings, }) .await .unwrap(); registry .publish_agent(PublishAgentRequest { workspace_id: &workspace_id, agent_id: &agent.id, version: 1, published_at: &OffsetDateTime::now_utc(), published_by: Some("onboarding-test"), expected_state: None, }) .await .unwrap(); let key_id = PlatformApiKeyId::new("pk_onboarding_later"); let key = PlatformApiKey { id: key_id.clone(), workspace_id, agent_id: Some(agent.id.clone()), key_kind: PlatformApiKeyKind::McpClient, name: "onboarding-later-key".to_owned(), prefix: "crk_later".to_owned(), scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], status: PlatformApiKeyStatus::Active, created_at: OffsetDateTime::now_utc(), last_used_at: None, expires_at: None, allowed_origins: Vec::new(), }; registry .create_platform_api_key(CreatePlatformApiKeyRequest { api_key: &key, secret_hash: "later-secret-hash", }) .await .unwrap(); (agent.id, key_id) } async fn authoritative_first_call( pool: &sqlx::PgPool, workspace_id: &str, agent_id: &str, key_id: &str, ) -> bool { sqlx::query_scalar( "select exists ( select 1 from invocation_logs l join published_agents pa on pa.agent_id = l.agent_id join platform_api_keys k on k.id = l.platform_api_key_id and k.workspace_id = l.workspace_id and k.agent_id = l.agent_id where l.workspace_id = $1 and l.agent_id = $2 and l.platform_api_key_id = $3 and l.source = 'agent_tool_call' and l.status = 'ok' and k.status = 'active' and (k.expires_at is null or k.expires_at > now()) )", ) .bind(workspace_id) .bind(agent_id) .bind(key_id) .fetch_one(pool) .await .unwrap() } async fn insert_product_event( pool: &sqlx::PgPool, id: &str, workspace_id: &str, event_name: &str, idempotency_key: &str, ) -> Result<(), sqlx::Error> { sqlx::query( "insert into product_events ( id, workspace_id, event_name, schema_version, occurred_at, idempotency_key, properties_json ) values ($1, $2, $3, 1, now(), $4, $5)", ) .bind(id) .bind(workspace_id) .bind(event_name) .bind(idempotency_key) .bind(json!({"eligible": true})) .execute(pool) .await?; Ok(()) } async fn relation_exists(pool: &sqlx::PgPool, relation: &str) -> bool { sqlx::query_scalar( "select exists ( select 1 from information_schema.tables where table_schema = current_schema() and table_name = $1 )", ) .bind(relation) .fetch_one(pool) .await .unwrap() } async fn column_exists(pool: &sqlx::PgPool, relation: &str, column: &str) -> bool { sqlx::query_scalar( "select exists ( select 1 from information_schema.columns where table_schema = current_schema() and table_name = $1 and column_name = $2 )", ) .bind(relation) .bind(column) .fetch_one(pool) .await .unwrap() } async fn create_workspace(registry: &crank_registry::PostgresRegistry, id: &str, slug: &str) { let now = OffsetDateTime::now_utc(); let workspace = Workspace { id: WorkspaceId::new(id), slug: slug.to_owned(), display_name: slug.to_owned(), status: crank_core::WorkspaceStatus::Active, settings: json!({}), created_at: now, updated_at: now, }; registry .create_workspace(CreateWorkspaceRequest { workspace: &workspace, }) .await .unwrap(); }