mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Automate owner deletion preparation (#7830)
## Why Owner deletion admission stops at `submitted`. Manual inventory and approval still stand between owner intent and the existing deletion executor, preventing self-serve deletion. ## What The existing `buzz-admin deletions drain` process prepares and automatically approves operator-attested owner requests. Operator-origin requests retain manual approval. ## How The drain prioritizes approved work, then claims one owner submission with the existing generation lease. It freezes inventory and records digest-bound `owner_automatic` approval in one transaction before entering the unchanged executor. Before approval, the transaction rechecks archived state and current ownership. It locks the deletion advisory fence, community, owner memberships, and request in that order. A regression test pins privileged abort during a live preparation lease. What got simpler: one drain still owns preparation and execution. There is no second worker, queue, lifecycle, or retry authority. The rebased branch contains only this feature and its review fixes. ## Risk Automatic approval permits irreversible deletion without a human grace period. Operator attestation is not a cryptographic owner signature. Provenance, locked authority checks, generation fences, and digest-bound approval constrain the operation. This remains work in progress. Broad test failures and incomplete mobile checks are disclosed below; publication is not a merge-ready verdict. ## Testing Published head: `e059ae9056b447ca3b5d0c2893da86550ef16f5c`. All seven commit trees match tested source `5605f27eaece1e809f746cc1839b55c7ceef8d80`; only attribution metadata changed. Source-SHA results below are not new-head execution receipts. At source commit `5605f27eaece1e809f746cc1839b55c7ceef8d80`, isolated PostgreSQL tests showed stale archive and ownership fixtures fail closed after the fix. Before the fix both reached automatic approval. Removing abort's generation increment or stage transition made the new live-lease regression fail; restored production passed. `just test` failed in two ACP native-git fixtures and one agent body-timeout test. The failing source files match the pinned base, but there is no exact-base execution comparison proving those failures unrelated. `just test-unit` also failed in the ACP fixtures, leaving 18 tests unrun. `just ci` stopped in Flutter tooling package resolution; separately run non-mobile lanes do not constitute a full `just ci` pass. No live tenant deletion or human testing was performed. A dedicated populated `0052` → `0053` approval-backfill fixture remains a coverage gap. Final-head hosted CI and security review are required. ## Bigger picture Rebased directly onto main `12670bd0f037c66a682272bb81c46c3f254fad74`; prerequisite #7818/#7827 are already merged. Main owns `0052_channel_artifacts`, so this feature's automatic-approval migration is now `0053`. Client/KGoose and quota work are separate PRs and are not part of this diff. Generated with Codex --------- Signed-off-by: Codex <noreply@openai.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -705,9 +705,12 @@ mod postgres_tests {
|
||||
let mut migrations: Vec<_> = MIGRATOR.iter().collect();
|
||||
migrations.sort_by_key(|migration| migration.version);
|
||||
|
||||
assert_eq!(migrations.len(), 52);
|
||||
assert_eq!(migrations.len(), 53);
|
||||
assert_eq!(migrations[48].version, 49);
|
||||
assert_eq!(migrations[49].version, 50);
|
||||
assert_eq!(migrations[50].version, 51);
|
||||
assert_eq!(migrations[51].version, 52);
|
||||
assert_eq!(migrations[52].version, 53);
|
||||
assert!(migrations[48]
|
||||
.sql
|
||||
.as_str()
|
||||
@@ -716,6 +719,11 @@ mod postgres_tests {
|
||||
.sql
|
||||
.as_str()
|
||||
.contains("community_deletion_owner_provenance"));
|
||||
assert!(migrations[52].sql.as_str().contains("approval_origin"));
|
||||
assert!(migrations[52]
|
||||
.sql
|
||||
.as_str()
|
||||
.contains("community_deletion_requests_owner_preparable"));
|
||||
assert_eq!(migrations[0].version, 1);
|
||||
assert_eq!(&*migrations[0].description, "initial schema");
|
||||
assert!(migrations[0]
|
||||
@@ -1722,6 +1730,34 @@ mod postgres_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owner_deletion_auto_approval_migration_matches_desired_schema() {
|
||||
let migration = MIGRATOR
|
||||
.iter()
|
||||
.find(|migration| migration.version == 53)
|
||||
.expect("embedded migration 0053")
|
||||
.sql
|
||||
.as_ref()
|
||||
.to_ascii_lowercase();
|
||||
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.and_then(std::path::Path::parent)
|
||||
.expect("workspace root");
|
||||
let schema = std::fs::read_to_string(workspace_root.join("schema/schema.sql"))
|
||||
.expect("read schema/schema.sql")
|
||||
.to_ascii_lowercase();
|
||||
|
||||
for sql in [&migration, &schema] {
|
||||
assert!(sql.contains("approval_origin text not null default 'operator'"));
|
||||
assert!(sql.contains("approval_origin in ('operator', 'owner_automatic')"));
|
||||
assert!(sql.contains("'submitted', 'approved', 'fenced'"));
|
||||
assert!(sql.contains("community_deletion_requests_owner_preparable"));
|
||||
assert!(sql.contains("request_origin = 'owner'"));
|
||||
assert!(sql.contains("stage = 'submitted'"));
|
||||
}
|
||||
assert!(migration.contains("set local lock_timeout = '5s'"));
|
||||
}
|
||||
|
||||
/// Structural parity between migration 0029's deletion surface and the
|
||||
/// desired-state bootstrap schema (`schema/schema.sql`).
|
||||
///
|
||||
@@ -1866,13 +1902,50 @@ mod postgres_tests {
|
||||
.tables
|
||||
.get(table)
|
||||
.unwrap_or_else(|| panic!("schema.sql is missing deletion table {table}"));
|
||||
if table != "community_deletion_requests" {
|
||||
if table != "community_deletion_requests" && table != "community_deletion_approvals" {
|
||||
assert_eq!(
|
||||
in_schema, definition,
|
||||
"schema.sql definition of {table} drifted from migration 0029"
|
||||
);
|
||||
}
|
||||
}
|
||||
let migration_approval_table = migration
|
||||
.tables
|
||||
.get("community_deletion_approvals")
|
||||
.expect("0029 approval table");
|
||||
let schema_approval_table = schema
|
||||
.tables
|
||||
.get("community_deletion_approvals")
|
||||
.expect("schema.sql approval table");
|
||||
for invariant in [
|
||||
"inventory_digest bytea not null check (length(inventory_digest) = 32)",
|
||||
"foreign key (request_id, community_id, inventory_digest) references community_deletion_requests(id, community_id, inventory_digest) on delete restrict",
|
||||
] {
|
||||
assert!(
|
||||
migration_approval_table.contains(invariant),
|
||||
"0029 deletion approvals are missing {invariant}"
|
||||
);
|
||||
assert!(
|
||||
schema_approval_table.contains(invariant),
|
||||
"schema.sql deletion approvals are missing {invariant}"
|
||||
);
|
||||
}
|
||||
let migration_request_table = migration
|
||||
.tables
|
||||
.get("community_deletion_requests")
|
||||
.expect("0029 deletion request table");
|
||||
for request_table in [
|
||||
migration_request_table,
|
||||
schema
|
||||
.tables
|
||||
.get("community_deletion_requests")
|
||||
.expect("schema.sql deletion request table"),
|
||||
] {
|
||||
assert!(
|
||||
request_table.contains("unique (id, community_id, inventory_digest)"),
|
||||
"deletion requests must expose the exact composite approval target"
|
||||
);
|
||||
}
|
||||
for (function, definition) in &migration.functions {
|
||||
let in_schema = schema
|
||||
.functions
|
||||
|
||||
Reference in New Issue
Block a user