fix(openapi): harden story 2.1 production lifecycle
This commit is contained in:
@@ -329,6 +329,16 @@ async fn source_relations_are_scoped_replayable_pageable_and_detachable() {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(retry, detached);
|
||||
let unconditional_retry = registry
|
||||
.detach_artifact_source(DetachArtifactSourceRequest {
|
||||
workspace_id: &workspace_a,
|
||||
source_id: &source_id,
|
||||
expected_updated_at: None,
|
||||
detached_at: timestamp("2026-08-26T10:05:00Z"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(unconditional_retry, detached);
|
||||
assert!(matches!(
|
||||
registry
|
||||
.detach_artifact_source(DetachArtifactSourceRequest {
|
||||
@@ -350,7 +360,7 @@ async fn source_relations_are_scoped_replayable_pageable_and_detachable() {
|
||||
);
|
||||
assert!(matches!(
|
||||
registry
|
||||
.read_artifact_source(&store, &workspace_a, &source_id)
|
||||
.read_artifact_source(std::sync::Arc::new(store.clone()), &workspace_a, &source_id)
|
||||
.await,
|
||||
Err(RegistryError::SourceUnavailable)
|
||||
));
|
||||
@@ -439,7 +449,7 @@ async fn verified_read_returns_only_digest_and_size_verified_bytes() {
|
||||
.await
|
||||
.unwrap();
|
||||
let verified = registry
|
||||
.read_artifact_source(&store, &workspace_id, &valid_id)
|
||||
.read_artifact_source(std::sync::Arc::new(store.clone()), &workspace_id, &valid_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(verified.bytes, bytes);
|
||||
@@ -473,7 +483,11 @@ async fn verified_read_returns_only_digest_and_size_verified_bytes() {
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
registry
|
||||
.read_artifact_source(&store, &workspace_id, &missing_id)
|
||||
.read_artifact_source(
|
||||
std::sync::Arc::new(store.clone()),
|
||||
&workspace_id,
|
||||
&missing_id
|
||||
)
|
||||
.await,
|
||||
Err(RegistryError::SourceUnavailable)
|
||||
));
|
||||
@@ -504,7 +518,11 @@ async fn verified_read_returns_only_digest_and_size_verified_bytes() {
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
registry
|
||||
.read_artifact_source(&store, &workspace_id, &wrong_size_id)
|
||||
.read_artifact_source(
|
||||
std::sync::Arc::new(store.clone()),
|
||||
&workspace_id,
|
||||
&wrong_size_id
|
||||
)
|
||||
.await,
|
||||
Err(RegistryError::SourceIntegrity)
|
||||
));
|
||||
@@ -529,7 +547,11 @@ async fn verified_read_returns_only_digest_and_size_verified_bytes() {
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
registry
|
||||
.read_artifact_source(&store, &workspace_id, &unavailable_id)
|
||||
.read_artifact_source(
|
||||
std::sync::Arc::new(store.clone()),
|
||||
&workspace_id,
|
||||
&unavailable_id
|
||||
)
|
||||
.await,
|
||||
Err(RegistryError::SourceUnavailable)
|
||||
));
|
||||
@@ -543,7 +565,7 @@ async fn verified_read_returns_only_digest_and_size_verified_bytes() {
|
||||
fs::write(&tampered, vec![b'x'; bytes.len()]).unwrap();
|
||||
assert!(matches!(
|
||||
registry
|
||||
.read_artifact_source(&store, &workspace_id, &valid_id)
|
||||
.read_artifact_source(std::sync::Arc::new(store.clone()), &workspace_id, &valid_id)
|
||||
.await,
|
||||
Err(RegistryError::SourceIntegrity)
|
||||
));
|
||||
@@ -749,12 +771,16 @@ async fn reconciliation_claims_are_fenced_global_and_allow_verified_revival() {
|
||||
.unwrap(),
|
||||
ArtifactClaimOutcome::ActiveReference
|
||||
));
|
||||
let detached_at: OffsetDateTime = sqlx::query_scalar("select clock_timestamp()")
|
||||
.fetch_one(registry.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.detach_artifact_source(DetachArtifactSourceRequest {
|
||||
workspace_id: &workspace_b,
|
||||
source_id: &active_id,
|
||||
expected_updated_at: Some(active.updated_at),
|
||||
detached_at: timestamp("2026-08-27T10:02:00Z"),
|
||||
detached_at,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -51,6 +51,7 @@ async fn controlled_authority_is_versioned_and_safe_under_concurrent_apply() {
|
||||
(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) {
|
||||
@@ -106,7 +107,7 @@ async fn controlled_authority_is_versioned_and_safe_under_concurrent_apply() {
|
||||
assert_eq!(artifact_relations.len(), 2);
|
||||
assert_eq!(
|
||||
MigrationAuthority::preflight(first.pool()).await.unwrap(),
|
||||
MigrationPreflight::Current { version: 12 }
|
||||
MigrationPreflight::Current { version: 13 }
|
||||
);
|
||||
}
|
||||
#[tokio::test]
|
||||
@@ -229,7 +230,7 @@ async fn legacy_core_baseline_is_consolidated_without_data_loss() {
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::MigrationRequired {
|
||||
current: 1,
|
||||
target: 12,
|
||||
target: 13,
|
||||
}
|
||||
);
|
||||
MigrationAuthority::apply(&pool).await.unwrap();
|
||||
@@ -308,7 +309,13 @@ 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 = 13 where version = 12")
|
||||
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();
|
||||
@@ -336,13 +343,13 @@ async fn healthy_v2_is_reported_as_migration_required_and_upgrades_to_v3() {
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::MigrationRequired {
|
||||
current: 2,
|
||||
target: 12,
|
||||
target: 13,
|
||||
}
|
||||
);
|
||||
MigrationAuthority::apply(&pool).await.unwrap();
|
||||
assert_eq!(
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::Current { version: 12 }
|
||||
MigrationPreflight::Current { version: 13 }
|
||||
);
|
||||
let trace_column: bool = sqlx::query_scalar(
|
||||
"select exists (
|
||||
@@ -385,7 +392,7 @@ async fn healthy_v3_upgrades_to_v4_with_honest_legacy_snapshot_provenance() {
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::MigrationRequired {
|
||||
current: 3,
|
||||
target: 12,
|
||||
target: 13,
|
||||
}
|
||||
);
|
||||
MigrationAuthority::apply(&pool).await.unwrap();
|
||||
@@ -445,7 +452,7 @@ async fn healthy_v4_upgrades_to_v5_without_fabricating_legacy_outcomes() {
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::MigrationRequired {
|
||||
current: 4,
|
||||
target: 12,
|
||||
target: 13,
|
||||
}
|
||||
);
|
||||
MigrationAuthority::apply(&pool).await.unwrap();
|
||||
@@ -699,7 +706,7 @@ async fn v3_upgrade_ignores_oversized_legacy_request_ids_in_partial_index() {
|
||||
MigrationAuthority::apply(&pool).await.unwrap();
|
||||
assert_eq!(
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::Current { version: 12 }
|
||||
MigrationPreflight::Current { version: 13 }
|
||||
);
|
||||
}
|
||||
async fn remove_v3_schema(pool: &sqlx::PgPool) {
|
||||
@@ -855,7 +862,13 @@ async fn remove_v11_schema(pool: &sqlx::PgPool) {
|
||||
}
|
||||
async fn remove_v12_schema(pool: &sqlx::PgPool) {
|
||||
sqlx::raw_sql(
|
||||
"drop table if exists artifact_sources;
|
||||
"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;",
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn healthy_v11_upgrades_to_v12_without_rewriting_prior_ledger() {
|
||||
async fn healthy_v11_upgrades_to_v13_without_rewriting_prior_ledger() {
|
||||
let database_url = crank_test_support::postgres_schema_url("test_v11_to_v12_artifacts").await;
|
||||
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
|
||||
MigrationAuthority::apply(&pool).await.unwrap();
|
||||
@@ -27,7 +27,7 @@ async fn healthy_v11_upgrades_to_v12_without_rewriting_prior_ledger() {
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::MigrationRequired {
|
||||
current: 11,
|
||||
target: 12,
|
||||
target: 13,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -51,22 +51,22 @@ async fn healthy_v11_upgrades_to_v12_without_rewriting_prior_ledger() {
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(after, prior);
|
||||
|
||||
let v12_applied_at: time::OffsetDateTime =
|
||||
sqlx::query_scalar("select applied_at from __crank_migrations where version = 12")
|
||||
let v13_applied_at: time::OffsetDateTime =
|
||||
sqlx::query_scalar("select applied_at from __crank_migrations where version = 13")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
MigrationAuthority::apply(&pool).await.unwrap();
|
||||
let replayed_at: time::OffsetDateTime =
|
||||
sqlx::query_scalar("select applied_at from __crank_migrations where version = 12")
|
||||
sqlx::query_scalar("select applied_at from __crank_migrations where version = 13")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(replayed_at, v12_applied_at);
|
||||
assert_eq!(replayed_at, v13_applied_at);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn v12_exact_guard_rejects_column_constraint_index_and_relation_drift() {
|
||||
async fn v12_guard_still_rejects_column_constraint_index_and_relation_drift() {
|
||||
let database_url = crank_test_support::postgres_schema_url("test_v12_artifact_drift").await;
|
||||
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
|
||||
MigrationAuthority::apply(&pool).await.unwrap();
|
||||
@@ -158,7 +158,7 @@ async fn v12_exact_guard_rejects_column_constraint_index_and_relation_drift() {
|
||||
sqlx::raw_sql(restore).execute(&pool).await.unwrap();
|
||||
assert_eq!(
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::Current { version: 12 },
|
||||
MigrationPreflight::Current { version: 13 },
|
||||
"restore: {restore}"
|
||||
);
|
||||
}
|
||||
@@ -175,3 +175,44 @@ async fn v12_exact_guard_rejects_column_constraint_index_and_relation_drift() {
|
||||
assert_eq!(error.code(), "partial_sequence");
|
||||
assert_eq!(error.version(), Some(12));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn v13_guard_rejects_wrong_cleanup_index_order_and_predicate() {
|
||||
let database_url =
|
||||
crank_test_support::postgres_schema_url("test_v13_cleanup_index_drift").await;
|
||||
let pool = sqlx::PgPool::connect(&database_url).await.unwrap();
|
||||
MigrationAuthority::apply(&pool).await.unwrap();
|
||||
|
||||
for (drift, restore) in [
|
||||
(
|
||||
"drop index artifact_sources_blob_lifecycle_detached_idx;
|
||||
create index artifact_sources_blob_lifecycle_detached_idx
|
||||
on artifact_sources(lifecycle, blob_digest, detached_at);",
|
||||
"drop index artifact_sources_blob_lifecycle_detached_idx;
|
||||
create index artifact_sources_blob_lifecycle_detached_idx
|
||||
on artifact_sources(blob_digest, lifecycle, detached_at);",
|
||||
),
|
||||
(
|
||||
"drop index artifact_sources_openapi_dangling_idx;
|
||||
create index artifact_sources_openapi_dangling_idx
|
||||
on artifact_sources(created_at, workspace_id, source_id)
|
||||
where lifecycle = 'active';",
|
||||
"drop index artifact_sources_openapi_dangling_idx;
|
||||
create index artifact_sources_openapi_dangling_idx
|
||||
on artifact_sources(created_at, workspace_id, source_id)
|
||||
where lifecycle = 'active' and left(source_id, 12) = 'src_openapi_';",
|
||||
),
|
||||
] {
|
||||
sqlx::raw_sql(drift).execute(&pool).await.unwrap();
|
||||
let error = MigrationAuthority::preflight(&pool).await.unwrap_err();
|
||||
assert_eq!(error.code(), "partial_sequence", "drift: {drift}");
|
||||
assert_eq!(error.version(), Some(13), "drift: {drift}");
|
||||
|
||||
sqlx::raw_sql(restore).execute(&pool).await.unwrap();
|
||||
assert_eq!(
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::Current { version: 13 },
|
||||
"restore: {restore}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ async fn failed_artifact_metadata_migration_rolls_back_schema_and_ledger() {
|
||||
MigrationAuthority::preflight(&pool).await.unwrap(),
|
||||
MigrationPreflight::MigrationRequired {
|
||||
current: 11,
|
||||
target: 12,
|
||||
target: 13,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user