use super::*; async fn ledgerless_v1(pool: &sqlx::PgPool) { MigrationAuthority::apply(pool).await.unwrap(); remove_v4_schema(pool).await; remove_v3_schema(pool).await; sqlx::raw_sql( "drop table __crank_migrations, __crank_migration_legacy_audit; drop table __crank_mcp_migrations; drop index mcp_transport_sessions_expires_at_idx; drop table __crank_ext_migrations; create table __crank_ext_migrations ( extension_name text not null, version integer not null, applied_at timestamptz not null default now(), primary key (extension_name, version) ); drop table __crank_core_migrations;", ) .execute(pool) .await .unwrap(); } #[tokio::test] async fn ledgerless_baseline_with_optional_index_drift_is_rejected_without_writes() { let database_url = crank_test_support::postgres_schema_url("test_ledgerless_index").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); ledgerless_v1(&pool).await; sqlx::raw_sql( "drop index mcp_transport_sessions_workspace_agent_idx; create index mcp_transport_sessions_workspace_agent_idx on mcp_transport_sessions(id);", ) .execute(&pool) .await .unwrap(); let error = MigrationAuthority::apply(&pool).await.unwrap_err(); assert_eq!(error.code(), "partial_sequence"); assert_eq!(error.stage(), "preflight.legacy_fingerprint"); let core_exists: bool = sqlx::query_scalar( "select to_regclass(format('%I.%I', current_schema(), '__crank_core_migrations')) is not null", ) .fetch_one(&pool) .await .unwrap(); assert!(!core_exists, "rejected adoption must remain read-only"); } #[tokio::test] async fn ledgerless_baseline_with_disabled_integrity_triggers_is_rejected_without_writes() { let database_url = crank_test_support::postgres_schema_url("test_ledgerless_triggers").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); ledgerless_v1(&pool).await; sqlx::query("alter table memberships disable trigger all") .execute(&pool) .await .unwrap(); let error = MigrationAuthority::apply(&pool).await.unwrap_err(); assert_eq!(error.code(), "partial_sequence"); assert_eq!(error.stage(), "preflight.legacy_fingerprint"); let core_exists: bool = sqlx::query_scalar( "select to_regclass(format('%I.%I', current_schema(), '__crank_core_migrations')) is not null", ) .fetch_one(&pool) .await .unwrap(); assert!(!core_exists, "rejected adoption must remain read-only"); } #[tokio::test] async fn published_ledgerless_baseline_upgrades_without_data_loss() { let database_url = crank_test_support::postgres_schema_url("test_ledgerless_baseline").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); ledgerless_v1(&pool).await; sqlx::query( "insert into operations (id, workspace_id, name, display_name, protocol, status, created_at, updated_at) values ('op_ledgerless', 'ws_default', 'ledgerless', 'Ledgerless', 'rest', 'draft', now(), now())", ) .execute(&pool) .await .unwrap(); assert_eq!( MigrationAuthority::preflight(&pool).await.unwrap(), MigrationPreflight::MigrationRequired { current: 0, target: 13, } ); MigrationAuthority::apply(&pool).await.unwrap(); let operation_count: i64 = sqlx::query_scalar("select count(*) from operations where id = 'op_ledgerless'") .fetch_one(&pool) .await .unwrap(); assert_eq!(operation_count, 1); assert_eq!( MigrationAuthority::preflight(&pool).await.unwrap(), MigrationPreflight::Current { version: 13 } ); } #[tokio::test] async fn published_in_place_mcp_upgrade_layout_is_accepted() { let database_url = crank_test_support::postgres_schema_url("test_ledgerless_mcp_upgrade").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); ledgerless_v1(&pool).await; sqlx::raw_sql( "drop table mcp_transport_sessions; create table mcp_transport_sessions ( id text primary key, protocol_version text not null, initialized boolean not null default false, workspace_slug text not null, agent_slug text not null, created_at timestamptz not null, updated_at timestamptz not null, expires_at timestamptz null ); create index mcp_transport_sessions_workspace_agent_idx on mcp_transport_sessions(workspace_slug, agent_slug, updated_at desc); alter table mcp_transport_sessions add column supports_elicitation boolean not null default false;", ) .execute(&pool) .await .unwrap(); assert_eq!( MigrationAuthority::preflight(&pool).await.unwrap(), MigrationPreflight::MigrationRequired { current: 0, target: 13, } ); MigrationAuthority::apply(&pool).await.unwrap(); assert_eq!( MigrationAuthority::preflight(&pool).await.unwrap(), MigrationPreflight::Current { version: 13 } ); } #[tokio::test] async fn ledgerless_baseline_with_future_drift_is_rejected_without_writes() { let database_url = crank_test_support::postgres_schema_url("test_ledgerless_drift").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); ledgerless_v1(&pool).await; sqlx::query("alter table invocation_logs add column trace_id text") .execute(&pool) .await .unwrap(); let error = MigrationAuthority::apply(&pool).await.unwrap_err(); assert_eq!(error.code(), "partial_sequence"); assert_eq!(error.stage(), "preflight.legacy_fingerprint"); let core_exists: bool = sqlx::query_scalar( "select to_regclass(format('%I.%I', current_schema(), '__crank_core_migrations')) is not null", ) .fetch_one(&pool) .await .unwrap(); assert!(!core_exists, "rejected adoption must remain read-only"); } #[tokio::test] async fn ledgerless_baseline_with_constraint_drift_is_rejected_without_writes() { let database_url = crank_test_support::postgres_schema_url("test_ledgerless_constraint").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); ledgerless_v1(&pool).await; sqlx::query("alter table workspaces alter column status drop not null") .execute(&pool) .await .unwrap(); let error = MigrationAuthority::apply(&pool).await.unwrap_err(); assert_eq!(error.code(), "partial_sequence"); assert_eq!(error.stage(), "preflight.legacy_fingerprint"); let core_exists: bool = sqlx::query_scalar( "select to_regclass(format('%I.%I', current_schema(), '__crank_core_migrations')) is not null", ) .fetch_one(&pool) .await .unwrap(); assert!(!core_exists, "rejected adoption must remain read-only"); }