237 lines
8.6 KiB
Rust
237 lines
8.6 KiB
Rust
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");
|
|
}
|