use super::*; #[tokio::test] async fn failed_artifact_metadata_migration_rolls_back_schema_and_ledger() { let _event_trigger_guard = EVENT_TRIGGER_TEST_LOCK.lock().await; let database_url = crank_test_support::postgres_schema_url("test_artifact_metadata_rollback").await; let pool = sqlx::PgPool::connect(&database_url).await.unwrap(); MigrationAuthority::apply(&pool).await.unwrap(); remove_v12_schema(&pool).await; let schema: String = sqlx::query_scalar("select current_schema()") .fetch_one(&pool) .await .unwrap(); let failure_trigger = format!( "create function reject_story21b_v12() returns event_trigger language plpgsql as $$ begin if current_schema() = '{schema}' and current_query() like '%artifact_sources%' then raise exception 'injected v12 ddl failure'; end if; end $$; create event trigger reject_story21b_v12 on ddl_command_start execute function reject_story21b_v12();" ); 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_story21b_v12; drop function reject_story21b_v12();", ) .execute(&pool) .await .unwrap(); assert_eq!(error.code(), "apply_failed"); assert_eq!(error.version(), Some(12)); for relation in [ "artifact_blobs", "artifact_blobs_pkey", "artifact_blobs_artifact_ref_key", "artifact_sources", "artifact_sources_pkey", "artifact_sources_workspace_created_idx", ] { 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 V12 DDL"); } let ledger_v12: i64 = sqlx::query_scalar("select count(*) from __crank_migrations where version = 12") .fetch_one(&pool) .await .unwrap(); assert_eq!(ledger_v12, 0); assert_eq!( MigrationAuthority::preflight(&pool).await.unwrap(), MigrationPreflight::MigrationRequired { current: 11, target: 13, } ); } #[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(); remove_v4_schema(&pool).await; 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"); }