use crank_registry::{MigrationAuthority, MigrationPreflight, PostgresRegistry}; use sqlx::Row; static EVENT_TRIGGER_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); #[tokio::test] async fn controlled_authority_is_versioned_and_safe_under_concurrent_apply() { let database_url = crank_test_support::postgres_schema_url("test_core_migration").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); let (first, second) = tokio::join!( MigrationAuthority::apply(&pool), MigrationAuthority::apply(&pool), ); first.expect("first controlled runner must apply the sequence"); second.expect("second controlled runner must observe the applied sequence"); let first = PostgresRegistry::connect(&database_url) .await .expect("service startup must verify the migrated schema"); let rows = sqlx::query("select version, name, checksum from __crank_migrations order by version") .fetch_all(first.pool()) .await .expect("migration ledger must be readable"); assert_eq!(rows.len(), 3); assert_eq!(rows[0].get::("version"), 1); assert_eq!(rows[0].get::("name"), "community-baseline-v1"); assert_eq!( rows[0].get::("checksum"), "crank-community-baseline-v1" ); assert_eq!(rows[1].get::("version"), 2); assert_eq!(rows[1].get::("name"), "legacy-consolidation-v2"); assert_eq!(rows[1].get::("checksum").len(), 64); assert_eq!(rows[2].get::("version"), 3); assert_eq!( rows[2].get::("name"), "request-trace-identity-v3" ); assert_eq!(rows[2].get::("checksum").len(), 64); let approval_columns = sqlx::query( "select column_name from information_schema.columns where table_schema = current_schema() and table_name = 'approval_requests' and column_name in ('execution_started_at', 'execution_attempts', 'request_fingerprint')", ) .fetch_all(first.pool()) .await .expect("approval schema must be readable"); assert_eq!(approval_columns.len(), 3); assert_eq!( MigrationAuthority::preflight(first.pool()).await.unwrap(), MigrationPreflight::Current { version: 3 } ); } #[tokio::test] async fn service_connect_is_read_only_on_fresh_database() { let database_url = crank_test_support::postgres_schema_url("test_read_only_startup").await; let error = PostgresRegistry::connect(&database_url) .await .expect_err("fresh schema must require the controlled migration command"); assert!(error.to_string().contains("schema_missing")); let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); let ledger = sqlx::query("select to_regclass('__crank_migrations')::text as name") .fetch_one(&pool) .await .unwrap() .try_get::, _>("name") .unwrap(); assert_eq!( ledger, None, "startup compatibility check must not create DDL" ); } #[tokio::test] async fn changed_checksum_fails_closed_without_repair() { let database_url = crank_test_support::postgres_schema_url("test_changed_checksum").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query("update __crank_migrations set checksum = 'changed' where version = 1") .execute(&pool) .await .unwrap(); let error = MigrationAuthority::apply(&pool) .await .expect_err("published checksum mismatch must fail closed"); assert_eq!(error.code(), "checksum_mismatch"); let checksum = sqlx::query("select checksum from __crank_migrations where version = 1") .fetch_one(&pool) .await .unwrap() .get::("checksum"); assert_eq!( checksum, "changed", "authority must not rewrite corrupt history" ); } #[tokio::test] async fn legacy_core_baseline_is_consolidated_without_data_loss() { let database_url = crank_test_support::postgres_schema_url("test_legacy_core").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query( "insert into workspaces (id, slug, display_name, status, settings_json, created_at, updated_at) values ('ws_preserved', 'preserved', 'Preserved', 'active', '{}'::jsonb, now(), now())", ) .execute(&pool) .await .unwrap(); sqlx::raw_sql( "insert into operations (id, workspace_id, name, display_name, protocol, status, created_at, updated_at) values ('op_preserved', 'ws_default', 'preserved', 'Preserved', 'rest', 'draft', now(), now()); insert into operation_versions (operation_id, version, status, target_json, input_schema_json, output_schema_json, input_mapping_json, output_mapping_json, execution_config_json, tool_description_json, created_at) values ('op_preserved', 1, 'draft', '{}'::jsonb, '{}'::jsonb, '{}'::jsonb, '{}'::jsonb, '{}'::jsonb, '{}'::jsonb, '{}'::jsonb, now()); insert into agents (id, workspace_id, slug, display_name, description, status, created_at, updated_at) values ('agent_preserved', 'ws_default', 'preserved', 'Preserved', '', 'draft', now(), now()); insert into agent_versions (agent_id, version, status, instructions_json, tool_selection_policy_json, created_at) values ('agent_preserved', 1, 'draft', '{}'::jsonb, '{}'::jsonb, now()); insert into platform_api_keys (id, workspace_id, agent_id, name, prefix, secret_hash, scopes_json, status, created_at) values ('key_preserved', 'ws_default', 'agent_preserved', 'Preserved', 'cp_', 'hash', '[]'::jsonb, 'active', now()); insert into approval_requests (id, workspace_id, agent_id, operation_id, operation_version, status, risk_level, request_payload_json, created_at, expires_at) values ('approval_preserved', 'ws_default', 'agent_preserved', 'op_preserved', 1, 'pending', 'high', '{}'::jsonb, now(), now() + interval '1 hour'); insert into invocation_logs (id, workspace_id, agent_id, operation_id, source, level, status, tool_name, message, duration_ms, request_preview_json, response_preview_json, created_at) values ('log_preserved', 'ws_default', 'agent_preserved', 'op_preserved', 'mcp', 'info', 'success', 'preserved', 'safe', 1, '{}'::jsonb, '{}'::jsonb, now());", ) .execute(&pool) .await .unwrap(); remove_v3_schema(&pool).await; let tables = [ "operations", "operation_versions", "agents", "agent_versions", "platform_api_keys", "approval_requests", "invocation_logs", ]; let mut before = Vec::new(); for table in tables { let row = if table == "invocation_logs" { "to_jsonb(t) - 'trace_id'" } else { "to_jsonb(t)" }; let sql = format!("select jsonb_agg({row} order by {row}::text)::text from {table} t"); before.push( sqlx::query_scalar::<_, Option>(sqlx::AssertSqlSafe(sql)) .fetch_one(&pool) .await .unwrap(), ); } sqlx::query( "drop table __crank_migrations, __crank_migration_legacy_audit, __crank_mcp_migrations, mcp_transport_sessions, __crank_ext_migrations", ) .execute(&pool) .await .unwrap(); assert_eq!( MigrationAuthority::preflight(&pool).await.unwrap(), MigrationPreflight::MigrationRequired { current: 1, target: 3, } ); MigrationAuthority::apply(&pool).await.unwrap(); let display_name = sqlx::query("select display_name from workspaces where id = 'ws_preserved'") .fetch_one(&pool) .await .unwrap() .get::("display_name"); assert_eq!(display_name, "Preserved"); let mut after = Vec::new(); for table in tables { let row = if table == "invocation_logs" { "to_jsonb(t) - 'trace_id'" } else { "to_jsonb(t)" }; let sql = format!("select jsonb_agg({row} order by {row}::text)::text from {table} t"); after.push( sqlx::query_scalar::<_, Option>(sqlx::AssertSqlSafe(sql)) .fetch_one(&pool) .await .unwrap(), ); } assert_eq!(before, after, "brownfield rows must remain byte-equivalent"); } #[tokio::test] async fn legacy_mcp_sessions_survive_consolidation() { let database_url = crank_test_support::postgres_schema_url("test_legacy_mcp").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query( "insert into mcp_transport_sessions ( id, protocol_version, initialized, supports_elicitation, workspace_slug, agent_slug, created_at, updated_at, expires_at ) values ('session_preserved', '2025-11-25', true, false, 'default', 'agent', now(), now(), null)", ) .execute(&pool) .await .unwrap(); remove_v3_schema(&pool).await; sqlx::query("drop table __crank_migrations, __crank_migration_legacy_audit") .execute(&pool) .await .unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); let count = sqlx::query("select count(*)::bigint as count from mcp_transport_sessions where id = 'session_preserved'") .fetch_one(&pool) .await .unwrap() .get::("count"); assert_eq!(count, 1); } #[tokio::test] async fn repeated_apply_does_not_rewrite_audit_timestamps() { let database_url = crank_test_support::postgres_schema_url("test_repeat_apply").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); let before = sqlx::query("select applied_at from __crank_migrations order by version") .fetch_all(&pool) .await .unwrap() .into_iter() .map(|row| row.get::("applied_at")) .collect::>(); MigrationAuthority::apply(&pool).await.unwrap(); let after = sqlx::query("select applied_at from __crank_migrations order by version") .fetch_all(&pool) .await .unwrap() .into_iter() .map(|row| row.get::("applied_at")) .collect::>(); assert_eq!(before, after); } #[tokio::test] async fn future_sequence_fails_closed() { let database_url = crank_test_support::postgres_schema_url("test_future_sequence").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query("update __crank_migrations set version = 4 where version = 3") .execute(&pool) .await .unwrap(); assert_eq!( MigrationAuthority::preflight(&pool) .await .unwrap_err() .code(), "future_version" ); } #[tokio::test] async fn healthy_v2_is_reported_as_migration_required_and_upgrades_to_v3() { let database_url = crank_test_support::postgres_schema_url("test_v2_to_v3_identity").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query("delete from __crank_migrations where version = 3") .execute(&pool) .await .unwrap(); remove_v3_schema(&pool).await; assert_eq!( MigrationAuthority::preflight(&pool).await.unwrap(), MigrationPreflight::MigrationRequired { current: 2, target: 3, } ); MigrationAuthority::apply(&pool).await.unwrap(); assert_eq!( MigrationAuthority::preflight(&pool).await.unwrap(), MigrationPreflight::Current { version: 3 } ); let trace_column: bool = sqlx::query_scalar( "select exists ( select 1 from information_schema.columns where table_schema = current_schema() and table_name = 'invocation_logs' and column_name = 'trace_id' )", ) .fetch_one(&pool) .await .unwrap(); assert!(trace_column); } #[tokio::test] async fn v2_with_partial_v3_objects_fails_before_apply() { let database_url = crank_test_support::postgres_schema_url("test_v2_partial_v3").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query("delete from __crank_migrations where version = 3") .execute(&pool) .await .unwrap(); sqlx::raw_sql( "drop index invocation_logs_workspace_request_id_idx; drop index invocation_logs_workspace_trace_id_idx; alter table invocation_logs drop constraint invocation_logs_trace_id_format_check;", ) .execute(&pool) .await .unwrap(); let error = MigrationAuthority::preflight(&pool).await.unwrap_err(); assert_eq!(error.code(), "partial_sequence"); assert_eq!(error.stage(), "preflight.schema"); } #[tokio::test] async fn v3_rejects_same_named_constraint_or_index_with_wrong_definition() { let database_url = crank_test_support::postgres_schema_url("test_v3_named_drift").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::raw_sql( "alter table invocation_logs drop constraint invocation_logs_trace_id_format_check; alter table invocation_logs add constraint invocation_logs_trace_id_format_check check (true) not valid;", ) .execute(&pool) .await .unwrap(); assert_eq!( MigrationAuthority::preflight(&pool) .await .unwrap_err() .code(), "partial_sequence" ); sqlx::raw_sql( "alter table invocation_logs drop constraint invocation_logs_trace_id_format_check; alter table invocation_logs add constraint invocation_logs_trace_id_format_check check ( trace_id is null or ( trace_id ~ '^[0-9a-f]{32}$' and trace_id <> '00000000000000000000000000000000' ) ) not valid; drop index invocation_logs_workspace_trace_id_idx; create index invocation_logs_workspace_trace_id_idx on invocation_logs(trace_id);", ) .execute(&pool) .await .unwrap(); assert_eq!( MigrationAuthority::preflight(&pool) .await .unwrap_err() .code(), "partial_sequence" ); } #[tokio::test] async fn v3_upgrade_ignores_oversized_legacy_request_ids_in_partial_index() { let database_url = crank_test_support::postgres_schema_url("test_v3_legacy_request_id").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::raw_sql( "insert into operations (id, workspace_id, name, display_name, protocol, status, created_at, updated_at) values ('op_legacy_request', 'ws_default', 'legacy-request', 'Legacy Request', 'rest', 'draft', now(), now());", ) .execute(&pool) .await .unwrap(); sqlx::query( "insert into invocation_logs (id, workspace_id, operation_id, source, level, status, tool_name, message, request_id, duration_ms, request_preview_json, response_preview_json, created_at) values ('legacy_request_log', 'ws_default', 'op_legacy_request', 'admin', 'info', 'success', 'legacy_request', 'safe', $1, 1, '{}'::jsonb, '{}'::jsonb, now())", ) .bind("x".repeat(10_000)) .execute(&pool) .await .unwrap(); sqlx::query("delete from __crank_migrations where version = 3") .execute(&pool) .await .unwrap(); remove_v3_schema(&pool).await; MigrationAuthority::apply(&pool).await.unwrap(); assert_eq!( MigrationAuthority::preflight(&pool).await.unwrap(), MigrationPreflight::Current { version: 3 } ); } async fn remove_v3_schema(pool: &sqlx::PgPool) { sqlx::raw_sql( "drop index if exists invocation_logs_workspace_request_id_idx; alter table invocation_logs drop column if exists trace_id;", ) .execute(pool) .await .unwrap(); } #[tokio::test] async fn partial_sequence_fails_closed() { let database_url = crank_test_support::postgres_schema_url("test_partial_sequence").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query("delete from __crank_migrations where version = 1") .execute(&pool) .await .unwrap(); assert_eq!( MigrationAuthority::preflight(&pool) .await .unwrap_err() .code(), "partial_sequence" ); } #[tokio::test] async fn missing_relation_with_current_ledger_fails_closed() { let database_url = crank_test_support::postgres_schema_url("test_missing_relation").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query("drop table usage_rollups") .execute(&pool) .await .unwrap(); let error = MigrationAuthority::preflight(&pool).await.unwrap_err(); assert_eq!(error.code(), "partial_sequence"); assert_eq!(error.stage(), "preflight.schema"); } #[tokio::test] async fn unregistered_legacy_extension_provenance_is_rejected() { let database_url = crank_test_support::postgres_schema_url("test_legacy_extension").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query("drop table __crank_migrations, __crank_migration_legacy_audit") .execute(&pool) .await .unwrap(); let checksum = "a".repeat(64); sqlx::query( "insert into __crank_ext_migrations (extension_name, version, checksum) values ('known-extension', 1, $1)", ) .bind(&checksum) .execute(&pool) .await .unwrap(); let error = MigrationAuthority::apply(&pool).await.unwrap_err(); assert_eq!(error.code(), "legacy_conflict"); } #[tokio::test] async fn any_owned_relation_without_core_ledger_is_partial() { let database_url = crank_test_support::postgres_schema_url("test_owned_partial").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); sqlx::query("create table users (id text primary key)") .execute(&pool) .await .unwrap(); let error = MigrationAuthority::preflight(&pool).await.unwrap_err(); assert_eq!(error.code(), "partial_sequence"); } #[tokio::test] async fn current_ledger_with_structural_drift_fails_closed() { let database_url = crank_test_support::postgres_schema_url("test_structural_drift").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query("alter table mcp_transport_sessions drop column supports_elicitation") .execute(&pool) .await .unwrap(); let error = MigrationAuthority::preflight(&pool).await.unwrap_err(); assert_eq!(error.code(), "partial_sequence"); assert_eq!(error.stage(), "preflight.schema"); } #[tokio::test] async fn tampered_legacy_audit_fails_closed() { let database_url = crank_test_support::postgres_schema_url("test_tampered_audit").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query( "update __crank_migration_legacy_audit set source_checksum = 'tampered' where source = 'core'", ) .execute(&pool) .await .unwrap(); let error = MigrationAuthority::preflight(&pool).await.unwrap_err(); assert_eq!(error.code(), "legacy_conflict"); } #[tokio::test] async fn failed_consolidation_rolls_back_all_changes() { let _event_trigger_guard = EVENT_TRIGGER_TEST_LOCK.lock().await; let database_url = crank_test_support::postgres_schema_url("test_apply_rollback").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::query( "insert into workspaces (id, slug, display_name, status, settings_json, created_at, updated_at) values ('rollback_preserved', 'rollback-preserved', 'Rollback Preserved', 'active', '{}'::jsonb, now(), now())", ) .execute(&pool) .await .unwrap(); sqlx::query( "drop table __crank_migrations, __crank_migration_legacy_audit, __crank_mcp_migrations, mcp_transport_sessions, __crank_ext_migrations", ) .execute(&pool) .await .unwrap(); let schema: String = sqlx::query_scalar("select current_schema()") .fetch_one(&pool) .await .unwrap(); let failure_trigger = format!( "create function reject_story14_v2() returns event_trigger language plpgsql as $$ begin if current_schema() = '{schema}' and current_query() like '%__crank_migrations%' then raise exception 'injected v2 ddl failure'; end if; end $$; create event trigger reject_story14_v2 on ddl_command_start execute function reject_story14_v2();" ); sqlx::raw_sql(sqlx::AssertSqlSafe(failure_trigger)) .execute(&pool) .await .unwrap(); let error = MigrationAuthority::apply(&pool).await.unwrap_err(); sqlx::raw_sql( "drop event trigger reject_story14_v2; drop function reject_story14_v2();", ) .execute(&pool) .await .unwrap(); assert_eq!(error.code(), "apply_failed"); for relation in [ "__crank_migrations", "__crank_migration_legacy_audit", "__crank_mcp_migrations", "mcp_transport_sessions", "__crank_ext_migrations", ] { let present: bool = sqlx::query_scalar( "select to_regclass(format('%I.%I', current_schema(), $1)) is not null", ) .bind(relation) .fetch_one(&pool) .await .unwrap(); assert!(!present, "{relation} must roll back with failed v2 DDL"); } let preserved: String = sqlx::query_scalar("select display_name from workspaces where id = 'rollback_preserved'") .fetch_one(&pool) .await .unwrap(); assert_eq!(preserved, "Rollback Preserved"); } #[tokio::test] async fn failed_request_trace_identity_migration_rolls_back_all_changes() { let _event_trigger_guard = EVENT_TRIGGER_TEST_LOCK.lock().await; let database_url = crank_test_support::postgres_schema_url("test_trace_identity_rollback").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); sqlx::raw_sql( "insert into operations (id, workspace_id, name, display_name, protocol, status, created_at, updated_at) values ('op_trace_rollback', 'ws_default', 'trace-rollback', 'Trace rollback', 'rest', 'draft', now(), now()); insert into invocation_logs (id, workspace_id, operation_id, source, level, status, tool_name, message, duration_ms, request_preview_json, response_preview_json, created_at) values ('trace_rollback_preserved', 'ws_default', 'op_trace_rollback', 'admin', 'info', 'success', 'trace_rollback', 'safe preserved row', 1, '{}'::jsonb, '{}'::jsonb, now());", ) .execute(&pool) .await .unwrap(); sqlx::query("delete from __crank_migrations where version = 3") .execute(&pool) .await .unwrap(); remove_v3_schema(&pool).await; let schema: String = sqlx::query_scalar("select current_schema()") .fetch_one(&pool) .await .unwrap(); let failure_trigger = format!( "create function reject_story15_v3() returns event_trigger language plpgsql as $$ begin if current_schema() = '{schema}' and current_query() like '%invocation_logs_workspace_trace_id_idx%' then raise exception 'injected v3 ddl failure'; end if; end $$; create event trigger reject_story15_v3 on ddl_command_start execute function reject_story15_v3();" ); sqlx::raw_sql(sqlx::AssertSqlSafe(failure_trigger)) .execute(&pool) .await .unwrap(); let error = MigrationAuthority::apply(&pool).await.unwrap_err(); sqlx::raw_sql( "drop event trigger reject_story15_v3; drop function reject_story15_v3();", ) .execute(&pool) .await .unwrap(); assert_eq!(error.code(), "apply_failed"); assert_eq!(error.version(), Some(3)); let trace_column: bool = sqlx::query_scalar( "select exists ( select 1 from information_schema.columns where table_schema = current_schema() and table_name = 'invocation_logs' and column_name = 'trace_id' )", ) .fetch_one(&pool) .await .unwrap(); assert!(!trace_column, "trace_id column must roll back with v3"); for index in [ "invocation_logs_workspace_request_id_idx", "invocation_logs_workspace_trace_id_idx", ] { let present: bool = sqlx::query_scalar( "select to_regclass(format('%I.%I', current_schema(), $1)) is not null", ) .bind(index) .fetch_one(&pool) .await .unwrap(); assert!(!present, "{index} must roll back with failed v3 DDL"); } let ledger_v3: i64 = sqlx::query_scalar("select count(*) from __crank_migrations where version = 3") .fetch_one(&pool) .await .unwrap(); assert_eq!(ledger_v3, 0, "failed v3 must not be recorded as applied"); let preserved: String = sqlx::query_scalar( "select message from invocation_logs where id = 'trace_rollback_preserved'", ) .fetch_one(&pool) .await .unwrap(); assert_eq!(preserved, "safe preserved row"); }