982 lines
40 KiB
Rust
982 lines
40 KiB
Rust
use crank_registry::{MigrationAuthority, MigrationPreflight, PostgresRegistry};
|
|
use sqlx::Row;
|
|
|
|
mod artifact_metadata;
|
|
mod rollback;
|
|
|
|
static EVENT_TRIGGER_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
|
|
|
fn legacy_preservation_row_expr(table: &str) -> &'static str {
|
|
match table {
|
|
"invocation_logs" => {
|
|
"to_jsonb(t) - array['trace_id','operation_version','execution_stage','execution_error_code','retryability','outcome_certainty','platform_api_key_id']"
|
|
}
|
|
"operation_versions" => {
|
|
"to_jsonb(t) - array['name','display_name','category','protocol','security_level','snapshot_provenance','snapshot_observed_at','published_at','published_by']"
|
|
}
|
|
"approval_requests" => "to_jsonb(t) - array['request_id','trace_id']",
|
|
"agents" => "to_jsonb(t) - array['catalog_revision']",
|
|
_ => "to_jsonb(t)",
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
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!(
|
|
MigrationAuthority::apply(&pool),
|
|
MigrationAuthority::apply(&pool),
|
|
);
|
|
first.expect("first controlled runner must apply the sequence");
|
|
second.expect("second controlled runner must observe the applied sequence");
|
|
let first = PostgresRegistry::connect(&database_url)
|
|
.await
|
|
.expect("service startup must verify the migrated schema");
|
|
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");
|
|
let expected = [
|
|
(1, "community-baseline-v1"),
|
|
(2, "legacy-consolidation-v2"),
|
|
(3, "request-trace-identity-v3"),
|
|
(4, "operation-lifecycle-v4"),
|
|
(5, "execution-outcome-v5"),
|
|
(6, "platform-key-name-reuse-v6"),
|
|
(7, "master-key-identity-v7"),
|
|
(8, "admin-auth-lifecycle-v8"),
|
|
(9, "agent-catalog-lifecycle-v9"),
|
|
(10, "approval-side-effects-v10"),
|
|
(11, "onboarding-product-events-v11"),
|
|
(12, "artifact-metadata-v12"),
|
|
(13, "artifact-cleanup-indexes-v13"),
|
|
];
|
|
assert_eq!(rows.len(), expected.len());
|
|
for (row, (version, name)) in rows.iter().zip(expected) {
|
|
assert_eq!(row.get::<i64, _>("version"), version);
|
|
assert_eq!(row.get::<String, _>("name"), name);
|
|
let checksum = row.get::<String, _>("checksum");
|
|
assert!(checksum == "crank-community-baseline-v1" || checksum.len() == 64);
|
|
}
|
|
let approval_columns = sqlx::query(
|
|
"select column_name
|
|
from information_schema.columns
|
|
where table_schema = current_schema()
|
|
and table_name = 'approval_requests'
|
|
and column_name in ('execution_started_at', 'execution_attempts', 'request_fingerprint')",
|
|
)
|
|
.fetch_all(first.pool())
|
|
.await
|
|
.expect("approval schema must be readable");
|
|
assert_eq!(approval_columns.len(), 3);
|
|
let onboarding_relations = sqlx::query(
|
|
"select table_name
|
|
from information_schema.tables
|
|
where table_schema = current_schema()
|
|
and table_name in ('product_events', 'product_event_daily_rollups', 'onboarding_selections')
|
|
order by table_name",
|
|
)
|
|
.fetch_all(first.pool())
|
|
.await
|
|
.expect("V11 onboarding relations must be readable");
|
|
assert_eq!(onboarding_relations.len(), 3);
|
|
let key_scope_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 = 'platform_api_key_id'
|
|
)",
|
|
)
|
|
.fetch_one(first.pool())
|
|
.await
|
|
.expect("V11 key provenance column must be readable");
|
|
assert!(key_scope_column);
|
|
let artifact_relations = sqlx::query(
|
|
"select table_name
|
|
from information_schema.tables
|
|
where table_schema = current_schema()
|
|
and table_name in ('artifact_blobs', 'artifact_sources')
|
|
order by table_name",
|
|
)
|
|
.fetch_all(first.pool())
|
|
.await
|
|
.expect("V12 artifact metadata relations must be readable");
|
|
assert_eq!(artifact_relations.len(), 2);
|
|
assert_eq!(
|
|
MigrationAuthority::preflight(first.pool()).await.unwrap(),
|
|
MigrationPreflight::Current { version: 13 }
|
|
);
|
|
}
|
|
#[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();
|
|
remove_v4_schema(&pool).await;
|
|
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 = legacy_preservation_row_expr(table);
|
|
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: 13,
|
|
}
|
|
);
|
|
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 = legacy_preservation_row_expr(table);
|
|
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_v4_schema(&pool).await;
|
|
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();
|
|
let current: i64 = sqlx::query_scalar("select max(version) from __crank_migrations")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
sqlx::query("update __crank_migrations set version = $1 where version = $2")
|
|
.bind(current + 1)
|
|
.bind(current)
|
|
.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();
|
|
remove_v4_schema(&pool).await;
|
|
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: 13,
|
|
}
|
|
);
|
|
MigrationAuthority::apply(&pool).await.unwrap();
|
|
assert_eq!(
|
|
MigrationAuthority::preflight(&pool).await.unwrap(),
|
|
MigrationPreflight::Current { version: 13 }
|
|
);
|
|
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 healthy_v3_upgrades_to_v4_with_honest_legacy_snapshot_provenance() {
|
|
let database_url = crank_test_support::postgres_schema_url("test_v3_to_v4_lifecycle").await;
|
|
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
|
|
MigrationAuthority::apply(&pool).await.unwrap();
|
|
remove_v4_schema(&pool).await;
|
|
sqlx::raw_sql(
|
|
"insert into operations
|
|
(id, workspace_id, name, display_name, category, protocol, security_level, status,
|
|
current_draft_version, latest_published_version, created_at, updated_at, published_at)
|
|
values ('op_v3_cutover', 'ws_default', 'v3_cutover', 'V3 Cutover', 'general', 'rest',
|
|
'standard', 'published', 1, 1, now(), 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_v3_cutover', 1, 'published', '{}'::jsonb, '{}'::jsonb, '{}'::jsonb,
|
|
'{}'::jsonb, '{}'::jsonb, '{}'::jsonb, '{}'::jsonb, now());
|
|
insert into published_operations(operation_id, version, published_at, published_by)
|
|
values ('op_v3_cutover', 1, now(), 'legacy-owner');",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
MigrationAuthority::preflight(&pool).await.unwrap(),
|
|
MigrationPreflight::MigrationRequired {
|
|
current: 3,
|
|
target: 13,
|
|
}
|
|
);
|
|
MigrationAuthority::apply(&pool).await.unwrap();
|
|
let row = sqlx::query(
|
|
"select name, display_name, snapshot_provenance, snapshot_observed_at,
|
|
published_at, published_by
|
|
from operation_versions where operation_id = 'op_v3_cutover' and version = 1",
|
|
)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(row.get::<String, _>("name"), "v3_cutover");
|
|
assert_eq!(row.get::<String, _>("display_name"), "V3 Cutover");
|
|
assert_eq!(
|
|
row.get::<String, _>("snapshot_provenance"),
|
|
"legacy_observed"
|
|
);
|
|
assert!(
|
|
row.try_get::<time::OffsetDateTime, _>("snapshot_observed_at")
|
|
.is_ok()
|
|
);
|
|
assert!(
|
|
row.try_get::<time::OffsetDateTime, _>("published_at")
|
|
.is_ok()
|
|
);
|
|
assert_eq!(
|
|
row.try_get::<Option<String>, _>("published_by").unwrap(),
|
|
Some("legacy-owner".to_owned())
|
|
);
|
|
}
|
|
#[tokio::test]
|
|
async fn healthy_v4_upgrades_to_v5_without_fabricating_legacy_outcomes() {
|
|
let database_url = crank_test_support::postgres_schema_url("test_v4_to_v5_outcome").await;
|
|
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
|
|
MigrationAuthority::apply(&pool).await.unwrap();
|
|
remove_v5_schema(&pool).await;
|
|
sqlx::query(
|
|
"insert into operations
|
|
(id, workspace_id, name, display_name, protocol, status, created_at, updated_at)
|
|
values ('op_v4_history', 'ws_default', 'v4_history', 'V4 History', '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,
|
|
duration_ms, request_preview_json, response_preview_json, created_at)
|
|
values ('legacy_v4_log', 'ws_default', 'op_v4_history', 'admin', 'info',
|
|
'success', 'legacy', 'safe', 1, '{}'::jsonb, '{}'::jsonb, now())",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
MigrationAuthority::preflight(&pool).await.unwrap(),
|
|
MigrationPreflight::MigrationRequired {
|
|
current: 4,
|
|
target: 13,
|
|
}
|
|
);
|
|
MigrationAuthority::apply(&pool).await.unwrap();
|
|
let row = sqlx::query(
|
|
"select operation_version, execution_stage, execution_error_code,
|
|
retryability, outcome_certainty
|
|
from invocation_logs where id = 'legacy_v4_log'",
|
|
)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
for column in [
|
|
"operation_version",
|
|
"execution_stage",
|
|
"execution_error_code",
|
|
"retryability",
|
|
"outcome_certainty",
|
|
] {
|
|
assert!(
|
|
row.try_get::<Option<String>, _>(column)
|
|
.is_ok_and(|value| value.is_none())
|
|
|| row
|
|
.try_get::<Option<i32>, _>(column)
|
|
.is_ok_and(|value| value.is_none())
|
|
);
|
|
}
|
|
}
|
|
#[tokio::test]
|
|
async fn failed_operation_lifecycle_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_operation_lifecycle_rollback").await;
|
|
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
|
|
MigrationAuthority::apply(&pool).await.unwrap();
|
|
remove_v4_schema(&pool).await;
|
|
let schema: String = sqlx::query_scalar("select current_schema()")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
let failure_trigger = format!(
|
|
"create function reject_story17_v4() returns event_trigger language plpgsql as $$
|
|
begin
|
|
if current_schema() = '{schema}' and current_query() like '%operation_versions_immutable_guard%' then
|
|
raise exception 'injected v4 ddl failure';
|
|
end if;
|
|
end $$;
|
|
create event trigger reject_story17_v4 on ddl_command_start
|
|
execute function reject_story17_v4();"
|
|
);
|
|
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_story17_v4;
|
|
drop function reject_story17_v4();",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(error.code(), "apply_failed");
|
|
assert_eq!(error.version(), Some(4));
|
|
let lifecycle_column: bool = sqlx::query_scalar(
|
|
"select exists (
|
|
select 1 from information_schema.columns
|
|
where table_schema = current_schema()
|
|
and table_name = 'operation_versions'
|
|
and column_name = 'snapshot_provenance'
|
|
)",
|
|
)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert!(!lifecycle_column);
|
|
let ledger_v4: i64 =
|
|
sqlx::query_scalar("select count(*) from __crank_migrations where version = 4")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(ledger_v4, 0);
|
|
}
|
|
#[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();
|
|
remove_v4_schema(&pool).await;
|
|
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 v5_rejects_same_named_execution_code_constraint_with_wrong_definition() {
|
|
let database_url =
|
|
crank_test_support::postgres_schema_url("test_v5_execution_code_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_execution_error_code_check;
|
|
alter table invocation_logs
|
|
add constraint invocation_logs_execution_error_code_check check (true) not valid;",
|
|
)
|
|
.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 v11_rejects_same_named_product_event_contract_drift() {
|
|
let database_url =
|
|
crank_test_support::postgres_schema_url("test_v11_product_event_named_drift").await;
|
|
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
|
|
MigrationAuthority::apply(&pool).await.unwrap();
|
|
|
|
sqlx::raw_sql(
|
|
"alter table product_events drop constraint product_events_name_check;
|
|
alter table product_events add constraint product_events_name_check check (true) not valid;",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
let constraint_error = MigrationAuthority::preflight(&pool).await.unwrap_err();
|
|
assert_eq!(constraint_error.code(), "partial_sequence");
|
|
assert_eq!(constraint_error.stage(), "preflight.schema");
|
|
|
|
sqlx::raw_sql(
|
|
"alter table product_events drop constraint product_events_name_check;
|
|
alter table product_events add constraint product_events_name_check check (event_name in (
|
|
'onboarding_eligible', 'onboarding_started', 'onboarding_resumed',
|
|
'onboarding_dismissed', 'onboarding_abandoned', 'onboarding_completed'
|
|
));
|
|
drop index product_events_workspace_occurred_idx;
|
|
create index product_events_workspace_occurred_idx
|
|
on product_events(occurred_at, workspace_id, id);",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
let index_error = MigrationAuthority::preflight(&pool).await.unwrap_err();
|
|
assert_eq!(index_error.code(), "partial_sequence");
|
|
assert_eq!(index_error.stage(), "preflight.schema");
|
|
|
|
sqlx::raw_sql(
|
|
"drop index product_events_workspace_occurred_idx;
|
|
create index product_events_workspace_occurred_idx
|
|
on product_events(workspace_id, occurred_at, id);
|
|
create or replace function crank_reject_product_event_mutation()
|
|
returns trigger language plpgsql as $$
|
|
begin
|
|
raise exception 'ProductEvent is append-only' using errcode = '23514';
|
|
end;
|
|
$$;",
|
|
)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
let function_error = MigrationAuthority::preflight(&pool).await.unwrap_err();
|
|
assert_eq!(function_error.code(), "partial_sequence");
|
|
assert_eq!(function_error.stage(), "preflight.schema");
|
|
}
|
|
#[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();
|
|
remove_v4_schema(&pool).await;
|
|
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: 13 }
|
|
);
|
|
}
|
|
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();
|
|
}
|
|
async fn remove_v4_schema(pool: &sqlx::PgPool) {
|
|
remove_v5_schema(pool).await;
|
|
sqlx::raw_sql(
|
|
"drop trigger if exists operation_versions_immutable_guard on operation_versions;
|
|
drop function if exists crank_guard_operation_version_immutable();
|
|
drop trigger if exists published_operations_monotonic_guard on published_operations;
|
|
drop function if exists crank_guard_published_operation_pointer();
|
|
drop trigger if exists operations_latest_pointer_monotonic_guard on operations;
|
|
drop function if exists crank_guard_operation_latest_pointer();
|
|
alter table operation_versions
|
|
drop constraint if exists operation_versions_snapshot_provenance_check,
|
|
drop column if exists name,
|
|
drop column if exists display_name,
|
|
drop column if exists category,
|
|
drop column if exists protocol,
|
|
drop column if exists security_level,
|
|
drop column if exists snapshot_provenance,
|
|
drop column if exists snapshot_observed_at,
|
|
drop column if exists published_at,
|
|
drop column if exists published_by;
|
|
delete from __crank_migrations where version = 4;",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
async fn remove_v5_schema(pool: &sqlx::PgPool) {
|
|
remove_v6_schema(pool).await;
|
|
sqlx::raw_sql(
|
|
"drop index if exists invocation_logs_workspace_operation_version_idx;
|
|
alter table invocation_logs
|
|
drop column if exists operation_version,
|
|
drop column if exists execution_stage,
|
|
drop column if exists execution_error_code,
|
|
drop column if exists retryability,
|
|
drop column if exists outcome_certainty;
|
|
delete from __crank_migrations where version = 5;",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
async fn remove_v6_schema(pool: &sqlx::PgPool) {
|
|
remove_v7_schema(pool).await;
|
|
sqlx::raw_sql(
|
|
"drop index if exists platform_api_keys_workspace_name_active_idx;
|
|
create unique index if not exists platform_api_keys_workspace_name_idx on platform_api_keys(workspace_id, name);
|
|
delete from __crank_migrations where version = 6;",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
async fn remove_v7_schema(pool: &sqlx::PgPool) {
|
|
remove_v8_schema(pool).await;
|
|
sqlx::raw_sql(
|
|
"alter table secret_versions drop constraint if exists secret_versions_target_all_or_none_check,
|
|
drop constraint if exists secret_versions_target_epoch_check,
|
|
drop constraint if exists secret_versions_master_key_epoch_check,
|
|
drop column if exists target_master_key_epoch, drop column if exists target_key_version,
|
|
drop column if exists target_ciphertext, drop column if exists master_key_epoch;
|
|
drop table if exists master_key_rotations;
|
|
drop table if exists master_key_identities;
|
|
delete from __crank_migrations where version = 7;",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
async fn remove_v8_schema(pool: &sqlx::PgPool) {
|
|
remove_v9_schema(pool).await;
|
|
sqlx::raw_sql(
|
|
"drop table if exists admin_security_audit_events;
|
|
drop table if exists admin_login_backoff;
|
|
drop table if exists admin_bootstrap_contracts;
|
|
alter table user_sessions
|
|
drop constraint if exists user_sessions_csrf_hash_check,
|
|
drop column if exists csrf_hash,
|
|
drop column if exists revoked_at;
|
|
delete from __crank_migrations where version = 8;",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
async fn remove_v9_schema(pool: &sqlx::PgPool) {
|
|
remove_v10_schema(pool).await;
|
|
sqlx::raw_sql(
|
|
"drop trigger if exists published_agents_monotonic_guard on published_agents;
|
|
drop function if exists crank_reject_published_agent_pointer_rewind();
|
|
drop trigger if exists agent_operation_bindings_immutable_guard on agent_operation_bindings;
|
|
drop function if exists crank_reject_published_agent_binding_mutation();
|
|
drop trigger if exists agent_versions_immutable_guard on agent_versions;
|
|
drop function if exists crank_reject_published_agent_version_mutation();
|
|
alter table published_agents
|
|
drop constraint if exists published_agents_catalog_revision_check,
|
|
drop column if exists catalog_revision;
|
|
alter table agents
|
|
drop constraint if exists agents_catalog_revision_check,
|
|
drop column if exists catalog_revision;
|
|
delete from __crank_migrations where version = 9;",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
async fn remove_v10_schema(pool: &sqlx::PgPool) {
|
|
remove_v11_schema(pool).await;
|
|
sqlx::raw_sql(
|
|
"drop index if exists approval_requests_pending_scope_fingerprint_idx;
|
|
drop index if exists approval_requests_workspace_request_trace_idx;
|
|
alter table approval_requests drop constraint if exists approval_requests_request_id_check;
|
|
alter table approval_requests drop constraint if exists approval_requests_trace_id_check;
|
|
alter table approval_requests drop column if exists request_id;
|
|
alter table approval_requests drop column if exists trace_id;
|
|
create unique index if not exists 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;
|
|
delete from __crank_migrations where version = 10;",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
async fn remove_v11_schema(pool: &sqlx::PgPool) {
|
|
remove_v12_schema(pool).await;
|
|
sqlx::raw_sql(
|
|
"drop table if exists onboarding_selections;
|
|
drop trigger if exists product_events_append_only_guard on product_events;
|
|
drop function if exists crank_reject_product_event_mutation();
|
|
drop table if exists product_event_daily_rollups;
|
|
drop table if exists product_events;
|
|
drop index if exists invocation_logs_workspace_agent_key_success_idx;
|
|
alter table invocation_logs drop constraint if exists invocation_logs_platform_key_scope_fk;
|
|
alter table invocation_logs drop column if exists platform_api_key_id;
|
|
alter table platform_api_keys drop constraint if exists platform_api_keys_workspace_agent_id_unique;
|
|
delete from __crank_migrations where version = 11;",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
async fn remove_v12_schema(pool: &sqlx::PgPool) {
|
|
sqlx::raw_sql(
|
|
"drop index if exists import_jobs_openapi_source_idx;
|
|
drop index if exists import_jobs_expires_at_idx;
|
|
drop index if exists artifact_sources_openapi_dangling_idx;
|
|
drop index if exists artifact_sources_blob_lifecycle_detached_idx;
|
|
drop index if exists artifact_blobs_expired_claim_idx;
|
|
delete from __crank_migrations where version = 13;
|
|
drop table if exists artifact_sources;
|
|
drop table if exists artifact_blobs;
|
|
delete from __crank_migrations where version = 12;",
|
|
)
|
|
.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");
|
|
assert_eq!(error.stage(), "preflight.core_missing");
|
|
}
|
|
#[tokio::test]
|
|
async fn empty_core_ledger_is_reported_separately() {
|
|
let database_url = crank_test_support::postgres_schema_url("test_empty_core_ledger").await;
|
|
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
|
|
MigrationAuthority::apply(&pool).await.unwrap();
|
|
sqlx::query("delete from __crank_core_migrations")
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
let error = MigrationAuthority::preflight(&pool).await.unwrap_err();
|
|
assert_eq!(error.code(), "partial_sequence");
|
|
assert_eq!(error.stage(), "preflight.core_cardinality");
|
|
}
|
|
#[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");
|
|
}
|