feat: harden community production foundation through story 1.5
This commit is contained in:
@@ -1,34 +1,46 @@
|
||||
use crank_registry::PostgresRegistry;
|
||||
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 core_migration_is_versioned_and_safe_under_concurrent_startup() {
|
||||
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!(
|
||||
PostgresRegistry::connect(&database_url),
|
||||
PostgresRegistry::connect(&database_url),
|
||||
MigrationAuthority::apply(&pool),
|
||||
MigrationAuthority::apply(&pool),
|
||||
);
|
||||
let first = first.expect("first service startup must apply the migration");
|
||||
second.expect("second service startup must observe the applied migration");
|
||||
first.expect("first controlled runner must apply the sequence");
|
||||
second.expect("second controlled runner must observe the applied sequence");
|
||||
|
||||
let rows = sqlx::query(
|
||||
"select version, description, checksum from __crank_core_migrations order by version",
|
||||
)
|
||||
.fetch_all(first.pool())
|
||||
.await
|
||||
.expect("migration ledger must be readable");
|
||||
let first = PostgresRegistry::connect(&database_url)
|
||||
.await
|
||||
.expect("service startup must verify the migrated schema");
|
||||
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0].get::<i32, _>("version"), 1);
|
||||
assert_eq!(
|
||||
rows[0].get::<String, _>("description"),
|
||||
"community baseline"
|
||||
);
|
||||
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::<i64, _>("version"), 1);
|
||||
assert_eq!(rows[0].get::<String, _>("name"), "community-baseline-v1");
|
||||
assert_eq!(
|
||||
rows[0].get::<String, _>("checksum"),
|
||||
"crank-community-baseline-v1"
|
||||
);
|
||||
assert_eq!(rows[1].get::<i64, _>("version"), 2);
|
||||
assert_eq!(rows[1].get::<String, _>("name"), "legacy-consolidation-v2");
|
||||
assert_eq!(rows[1].get::<String, _>("checksum").len(), 64);
|
||||
assert_eq!(rows[2].get::<i64, _>("version"), 3);
|
||||
assert_eq!(
|
||||
rows[2].get::<String, _>("name"),
|
||||
"request-trace-identity-v3"
|
||||
);
|
||||
assert_eq!(rows[2].get::<String, _>("checksum").len(), 64);
|
||||
|
||||
let approval_columns = sqlx::query(
|
||||
"select column_name
|
||||
@@ -41,4 +53,657 @@ async fn core_migration_is_versioned_and_safe_under_concurrent_startup() {
|
||||
.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::<Option<String>, _>("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::<String, _>("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<String>>(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::<String, _>("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<String>>(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::<i64, _>("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::<time::OffsetDateTime, _>("applied_at"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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::<time::OffsetDateTime, _>("applied_at"))
|
||||
.collect::<Vec<_>>();
|
||||
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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user