BUZZ-301: Add dummy user deletion workflow

Signed-off-by: Ravneet Arora <rarora@squareup.com>
This commit is contained in:
Ravneet Arora
2026-10-20 13:38:09 -04:00
parent 0b53fc699d
commit 0f0ca8957f
35 changed files with 2971 additions and 201 deletions

View File

@@ -232,3 +232,64 @@ BEGIN
RAISE EXCEPTION 'replica_heartbeat must contain its singleton row after pgschema apply';
END IF;
END $$;
-- Entity deletion is operator-global, even when a stage targets a community.
INSERT INTO _operator_global_tables (table_name, reason) VALUES
('entity_deletion_requests', 'deployment entity deletion workflow and synthetic demo state'),
('entity_deletion_stage_history', 'deployment entity deletion workflow and synthetic demo state'),
('entity_deletion_approvals', 'deployment entity deletion workflow and synthetic demo state'),
('entity_deletion_executor_heartbeats', 'deployment entity deletion workflow and synthetic demo state')
ON CONFLICT (table_name) DO NOTHING;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint
WHERE conrelid = 'entity_deletion_requests'::regclass
AND conname = 'entity_deletion_request_shape') THEN
ALTER TABLE entity_deletion_requests ADD CONSTRAINT entity_deletion_request_shape CHECK (
((lease_owner IS NULL) = (lease_until IS NULL))
AND ((blocked_at IS NULL) = (blocked_reason IS NULL))
AND ((status = 'demo_completed') = (completed_at IS NOT NULL))
AND (status <> 'demo_completed' OR lease_owner IS NULL)
AND ((status = 'submitted') = (inventory_manifest IS NULL))
AND ((inventory_manifest IS NULL) = (inventory_digest IS NULL))
AND ((inventory_manifest IS NULL) = (inventory_frozen_at IS NULL))
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint
WHERE conrelid = 'entity_deletion_requests'::regclass
AND conname = 'entity_deletion_request_shape' AND convalidated) THEN
RAISE EXCEPTION 'entity_deletion_request_shape must be validated';
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint
WHERE conrelid = 'entity_deletion_stage_history'::regclass
AND conname = 'entity_deletion_stage_shape') THEN
ALTER TABLE entity_deletion_stage_history ADD CONSTRAINT entity_deletion_stage_shape CHECK (
(is_current = (status <> 'completed'))
AND ((status = 'completed') = (finished_at IS NOT NULL))
AND ((status = 'pending') = (started_at IS NULL))
AND (finished_at IS NULL OR finished_at >= started_at)
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint
WHERE conrelid = 'entity_deletion_stage_history'::regclass
AND conname = 'entity_deletion_stage_shape' AND convalidated) THEN
RAISE EXCEPTION 'entity_deletion_stage_shape must be validated';
END IF;
END $$;
DO $$
BEGIN
IF (SELECT count(*) FROM _operator_global_tables WHERE table_name IN (
'entity_deletion_requests',
'entity_deletion_stage_history',
'entity_deletion_approvals',
'entity_deletion_executor_heartbeats'
)) <> 4 THEN
RAISE EXCEPTION 'entity deletion tables must be operator-global';
END IF;
END $$;

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Exercise the actual user-deletion CLI against an explicit disposable database."""
import argparse
import json
import os
import subprocess
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--binary", type=Path, required=True)
args = parser.parse_args()
database_url = os.environ.get("BUZZ_USER_DELETION_TEST_DATABASE_URL")
if not database_url:
parser.error("set BUZZ_USER_DELETION_TEST_DATABASE_URL to an isolated, migrated database")
environment = dict(os.environ, DATABASE_URL=database_url)
binary = str(args.binary.resolve())
def command(*parts, expected=0):
result = subprocess.run(
[binary, "user-deletions", *parts],
env=environment,
capture_output=True,
text=True,
timeout=90,
check=False,
)
if result.returncode != expected:
raise AssertionError(
f"{parts}: expected exit {expected}, got {result.returncode}\n"
f"{result.stdout[-8000:]}\n{result.stderr[-8000:]}"
)
if expected != 0:
return None
return json.loads(result.stdout)
def prepare():
target = "demo:cli-" + uuid.uuid4().hex
request = command("submit", "--target", target, "--requested-by", "operator-a")
request_id = request["id"]
repeated = command("submit", "--target", target, "--requested-by", "operator-a")
assert repeated["id"] == request_id
command("run", request_id, expected=5)
inventory = command("inventory", request_id)
assert len(inventory["inventory_manifest"]["communities"]) == 3
assert all(c["relationship"] == "participant"
for c in inventory["inventory_manifest"]["communities"])
command("approve", request_id, "--approved-by", "operator-a", expected=5)
command("approve", request_id, "--approved-by", "operator-b")
return request_id
def finished(request_id):
result = command("inspect", request_id)
assert result["request"]["status"] == "demo_completed"
assert result["request"]["lease_owner"] is None
history = result["stage_history"]
assert len(history) == 15
assert all(not s["is_current"] and s["status"] == "completed" for s in history)
assert all(s["metadata_json"]["simulated"] for s in history)
assert [s["stage"] for s in history[:4]] == [
"gate_user", "gate_community", "gate_community", "gate_community"
]
for start, community in [(4, 1), (7, 2), (10, 3)]:
assert all(s["target_community_id"] == str(uuid.UUID(int=community))
for s in history[start:start + 3])
return result
stopped_id = prepare()
command("run", stopped_id, "--stop-after", "7")
stopped = command("inspect", stopped_id)
assert stopped["request"]["lease_owner"] is None
assert len(stopped["stage_history"]) == 8
assert stopped["stage_history"][-1]["status"] == "pending"
command("run", stopped_id)
completed = finished(stopped_id)
assert completed["stage_history"][:7] == stopped["stage_history"][:7]
print(f"PASS CLI stop/resume: {stopped_id}", flush=True)
failed_id = prepare()
command("run", failed_id, "--fail-at", "9", expected=1)
failed = command("inspect", failed_id)
assert len(failed["stage_history"]) == 9
assert failed["stage_history"][-1]["status"] == "failed"
assert failed["request"]["lease_owner"] is None
retry_at = datetime.fromisoformat(failed["request"]["next_attempt_at"].replace("Z", "+00:00"))
delay = max(0, (retry_at - datetime.now(timezone.utc)).total_seconds()) + 0.2
assert delay < 35, "unexpected retry delay"
time.sleep(delay)
command("run", failed_id)
recovered = finished(failed_id)
assert recovered["stage_history"][:8] == failed["stage_history"][:8]
assert recovered["stage_history"][8]["id"] == failed["stage_history"][8]["id"]
assert recovered["request"]["lease_generation"] == 2
print(f"PASS CLI failure in B/retry: {failed_id}", flush=True)
interrupted_id = prepare()
process = subprocess.Popen(
[binary, "user-deletions", "run", interrupted_id, "--step-delay-ms", "30000"],
env=environment, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
)
try:
# Cold database connection setup has its own retry budget. Once the
# stage starts, require renewal within two heartbeat intervals.
deadline = time.monotonic() + 45
initial = None
while time.monotonic() < deadline:
if process.poll() is not None:
stdout, stderr = process.communicate(timeout=5)
raise AssertionError(
f"worker exited before heartbeat: {process.returncode}\n"
f"{stdout[-8000:]}\n{stderr[-8000:]}"
)
current = command("inspect", interrupted_id)
if current["stage_history"][0]["status"] == "running":
if initial is None:
initial = current
deadline = time.monotonic() + 20
elif current["request"]["lease_until"] != initial["request"]["lease_until"]:
break
time.sleep(0.25)
else:
raise AssertionError("worker did not renew its lease while a dummy action ran")
command("run", interrupted_id, expected=5)
process.terminate()
stdout, stderr = process.communicate(timeout=15)
assert process.returncode == 0, (stdout[-8000:], stderr[-8000:])
finally:
if process.poll() is None:
process.kill()
process.communicate(timeout=10)
interrupted = command("inspect", interrupted_id)
assert interrupted["request"]["lease_owner"] is None
assert interrupted["stage_history"][0]["is_current"]
command("run", interrupted_id)
resumed = finished(interrupted_id)
assert resumed["request"]["lease_generation"] == 2
assert resumed["stage_history"][0]["id"] == interrupted["stage_history"][0]["id"]
assert resumed["stage_history"][0]["started_at"] == interrupted["stage_history"][0]["started_at"]
print(f"PASS CLI heartbeat/SIGTERM/resume: {interrupted_id}", flush=True)
print("PASS: all actions were simulated; no owned-community path was exercised.")
if __name__ == "__main__":
main()