Files
crank/crates/crank-registry/tests/integration/migrations/legacy_adoption.rs
T
bsodfather bbb9394911
CI / Rust Checks (push) Successful in 14m21s
CI / UI Checks (push) Successful in 6s
CI / Community Image Smoke (push) Successful in 6m14s
CI / Frontend E2E (push) Successful in 7m10s
CI / Deploy (push) Successful in 1m12s
fix(migrations): normalize legacy column order
2026-09-02 01:01:58 +03:00

440 lines
17 KiB
Rust

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_ledgerless_historical_column_layout_upgrades_without_data_loss() {
let database_url =
crank_test_support::postgres_schema_url("test_ledgerless_historical_layout").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
ledgerless_v1(&pool).await;
sqlx::raw_sql(
"alter table platform_api_keys
drop column key_kind,
drop column expires_at,
drop column allowed_origins_json;
alter table platform_api_keys
add column key_kind text not null default 'mcp_client',
add column expires_at timestamptz null,
add column allowed_origins_json jsonb not null default '[]'::jsonb;
drop index approval_requests_pending_fingerprint_idx;
alter table approval_requests
drop column execution_started_at,
drop column execution_attempts,
drop column request_fingerprint,
add column confirmation_title text not null default '',
add column confirmation_body text not null default '';
alter table approval_requests
alter column confirmation_title drop default,
alter column confirmation_body drop default,
drop column confirmation_title,
drop column confirmation_body,
add column execution_started_at timestamptz null,
add column execution_attempts integer not null default 0,
add column request_fingerprint text null;
create unique index approval_requests_pending_fingerprint_idx
on approval_requests(agent_id, operation_id, operation_version, request_fingerprint)
where status = 'pending' and request_fingerprint is not null;
insert into operations
(id, workspace_id, name, display_name, protocol, status, created_at, updated_at)
values ('op_historical_layout', 'ws_default', 'historical-layout', 'Historical layout', 'rest', 'draft', now(), now());
insert into agents
(id, workspace_id, slug, display_name, description, status, created_at, updated_at)
values ('agent_historical_layout', 'ws_default', 'historical-layout', 'Historical layout', '', 'draft', now(), now());
insert into platform_api_keys
(id, workspace_id, agent_id, name, prefix, secret_hash, key_kind, scopes_json, status,
created_at, expires_at, allowed_origins_json)
values ('key_historical_layout', 'ws_default', 'agent_historical_layout', 'Historical layout',
'cp_', 'hash', 'admin', '[]'::jsonb, 'active', now(),
'2030-01-02 03:04:05+00'::timestamptz, '[\"https://example.test\"]'::jsonb);
insert into approval_requests
(id, workspace_id, agent_id, operation_id, operation_version, status, risk_level,
request_payload_json, created_at, expires_at, execution_started_at,
execution_attempts, request_fingerprint)
values ('approval_historical_layout', 'ws_default', 'agent_historical_layout', 'op_historical_layout', 1,
'pending', 'high', '{\"layout\":\"historical\"}'::jsonb, now(), now() + interval '1 hour',
'2029-02-03 04:05:06+00'::timestamptz, 3, 'historical-fingerprint');",
)
.execute(&pool)
.await
.unwrap();
let historical_ordinals: Vec<(String, i32)> = sqlx::query_as(
"select table_name || '.' || column_name, ordinal_position
from information_schema.columns
where table_schema = current_schema()
and ((table_name = 'platform_api_keys'
and column_name in ('key_kind', 'expires_at', 'allowed_origins_json'))
or (table_name = 'approval_requests'
and column_name in ('execution_started_at', 'execution_attempts', 'request_fingerprint')))
order by table_name, ordinal_position",
)
.fetch_all(&pool)
.await
.unwrap();
assert_eq!(
historical_ordinals,
vec![
("approval_requests.execution_started_at".to_owned(), 22),
("approval_requests.execution_attempts".to_owned(), 23),
("approval_requests.request_fingerprint".to_owned(), 24),
("platform_api_keys.key_kind".to_owned(), 15),
("platform_api_keys.expires_at".to_owned(), 16),
("platform_api_keys.allowed_origins_json".to_owned(), 17),
],
"fixture must reproduce the published in-place column layout",
);
assert_eq!(
MigrationAuthority::preflight(&pool).await.unwrap(),
MigrationPreflight::MigrationRequired {
current: 0,
target: 13,
}
);
MigrationAuthority::apply(&pool).await.unwrap();
let key_count: i64 = sqlx::query_scalar(
"select count(*) from platform_api_keys
where id = 'key_historical_layout'
and key_kind = 'admin'
and expires_at = '2030-01-02 03:04:05+00'::timestamptz
and allowed_origins_json = '[\"https://example.test\"]'::jsonb",
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
key_count, 1,
"platform key row must survive the layout upgrade"
);
let approval_count: i64 = sqlx::query_scalar(
"select count(*) from approval_requests
where id = 'approval_historical_layout'
and request_payload_json = '{\"layout\":\"historical\"}'::jsonb
and execution_started_at = '2029-02-03 04:05:06+00'::timestamptz
and execution_attempts = 3
and request_fingerprint = 'historical-fingerprint'",
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
approval_count, 1,
"approval row must survive the layout upgrade"
);
assert_eq!(
MigrationAuthority::preflight(&pool).await.unwrap(),
MigrationPreflight::Current { version: 13 }
);
}
#[tokio::test]
async fn published_initial_mcp_layout_is_accepted() {
let database_url = crank_test_support::postgres_schema_url("test_ledgerless_initial_mcp").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);",
)
.execute(&pool)
.await
.unwrap();
assert_eq!(
MigrationAuthority::preflight(&pool).await.unwrap(),
MigrationPreflight::MigrationRequired {
current: 0,
target: 13,
}
);
MigrationAuthority::apply(&pool).await.unwrap();
let supports_elicitation: bool = sqlx::query_scalar(
"select exists (
select 1 from information_schema.columns
where table_schema = current_schema()
and table_name = 'mcp_transport_sessions'
and column_name = 'supports_elicitation'
)",
)
.fetch_one(&pool)
.await
.unwrap();
assert!(supports_elicitation);
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_default_drift_is_rejected_without_writes() {
let database_url = crank_test_support::postgres_schema_url("test_ledgerless_default").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
ledgerless_v1(&pool).await;
sqlx::query("alter table workspaces alter column status set default 'active'")
.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_definition").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
ledgerless_v1(&pool).await;
sqlx::query(
"alter table workspaces
add constraint workspaces_status_nonempty check (status <> '')",
)
.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_rls_drift_is_rejected_without_writes() {
let database_url = crank_test_support::postgres_schema_url("test_ledgerless_rls").await;
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
ledgerless_v1(&pool).await;
sqlx::query("alter table workspaces enable row level security")
.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_nullability_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");
}