diff --git a/tests/bluesky-pds/_p4.py b/tests/bluesky-pds/_p4.py new file mode 100644 index 0000000..58063e1 --- /dev/null +++ b/tests/bluesky-pds/_p4.py @@ -0,0 +1,75 @@ +"""Shared bluesky-pds P4 data-integrity helpers. + +The marker is a DETERMINISTIC atproto account (recipe-aware data living in the PDS's sqlite store +under /pds), NOT a loose file — so the restore assertion genuinely catches a restore that fails to +bring the account back (e.g. if the running PDS holds its sqlite open and never reloads the restored +files, the same data-loss class cc-ci caught in immich/mattermost). The handle is derived from the +per-run domain so ops.py (seed/wipe) and the test_.py overlays agree without passing state. +""" + +from __future__ import annotations + +import os +import re +import shlex +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) +from harness import http as harness_http, lifecycle # noqa: E402 + +PDS_HOST_LOCAL = "http://localhost:3000" +_PW = "ccci-P4-marker-pw-2026" + + +def _handle(domain: str) -> str: + return f"ccci-p4.{domain}" + + +def _email(domain: str) -> str: + return f"ccci-p4@{domain}" + + +def _goat_admin(domain: str, args: str) -> str: + """`goat pds admin ` inside the PDS `app` container (admin bypasses invite requirement).""" + cmd = ( + f"goat pds admin {args} " + f'--admin-password "$(cat /run/secrets/pds_admin_password)" ' + f"--pds-host {PDS_HOST_LOCAL} 2>&1" + ) + return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], timeout=120) + + +def account_did(domain: str) -> str | None: + """The marker account's DID if it exists (public XRPC describeRepo by handle), else None.""" + st, body = harness_http.http_get( + f"https://{domain}/xrpc/com.atproto.repo.describeRepo?repo={_handle(domain)}" + ) + if st == 200 and isinstance(body, dict): + return body.get("did") + return None + + +def account_exists(domain: str) -> bool: + return account_did(domain) is not None + + +def create_account(domain: str) -> str: + """Idempotently create the deterministic marker account; return its DID.""" + did = account_did(domain) + if did: + return did + out = _goat_admin( + domain, + f"account create --handle {shlex.quote(_handle(domain))} " + f"--email {shlex.quote(_email(domain))} --password {shlex.quote(_PW)}", + ) + m = re.search(r"did:plc:[a-z0-9]+", out) + assert m, f"goat account create produced no DID. Output:\n{out[:400]!r}" + return m.group(0) + + +def delete_account(domain: str) -> None: + """Delete the marker account (so a successful restore is observable). No-op if already gone.""" + did = account_did(domain) + if did: + _goat_admin(domain, f"account delete {did}") diff --git a/tests/bluesky-pds/ops.py b/tests/bluesky-pds/ops.py new file mode 100644 index 0000000..e2bafd9 --- /dev/null +++ b/tests/bluesky-pds/ops.py @@ -0,0 +1,22 @@ +"""bluesky-pds — pre-op seed hooks (Phase 1e HC3). The P4 marker is a deterministic atproto account +(recipe-aware data in the PDS sqlite under /pds, the backed-up volume) — see _p4.py. pre_restore +deletes the account so a successful restore is observable (non-vacuous).""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) +import _p4 # noqa: E402 + + +def pre_upgrade(domain, meta): + _p4.create_account(domain) + + +def pre_backup(domain, meta): + _p4.create_account(domain) + + +def pre_restore(domain, meta): + _p4.delete_account(domain) + assert not _p4.account_exists(domain), "marker account delete did not take (pre_restore)" diff --git a/tests/bluesky-pds/test_backup.py b/tests/bluesky-pds/test_backup.py new file mode 100644 index 0000000..660f710 --- /dev/null +++ b/tests/bluesky-pds/test_backup.py @@ -0,0 +1,13 @@ +"""bluesky-pds — BACKUP overlay (Phase 1e HC3): assertion-only + additive. +ops.pre_backup created the deterministic marker account before the backup op; this overlay asserts it +is present at backup time (so the backup archive captures it).""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) +import _p4 # noqa: E402 + + +def test_backup_captures_state(live_app): + assert _p4.account_exists(live_app), "seeded marker account not present at backup time" diff --git a/tests/bluesky-pds/test_restore.py b/tests/bluesky-pds/test_restore.py new file mode 100644 index 0000000..7bfa04f --- /dev/null +++ b/tests/bluesky-pds/test_restore.py @@ -0,0 +1,16 @@ +"""bluesky-pds — RESTORE overlay (Phase 1e HC3): data-integrity, assertion-only + additive. +ops.pre_restore deleted the marker account (diverge); the orchestrator restored once. This overlay +asserts the account is back — i.e. the PDS data volume (atproto sqlite) genuinely round-tripped +through backup→restore (not just service-up).""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) +import _p4 # noqa: E402 + + +def test_restore_returns_state(live_app): + assert _p4.account_exists(live_app), ( + "restore did not bring back the seeded marker account (PDS data did not survive restore)" + ) diff --git a/tests/bluesky-pds/test_upgrade.py b/tests/bluesky-pds/test_upgrade.py new file mode 100644 index 0000000..6624d1c --- /dev/null +++ b/tests/bluesky-pds/test_upgrade.py @@ -0,0 +1,13 @@ +"""bluesky-pds — UPGRADE overlay (Phase 1e HC3): data-continuity, assertion-only + additive. +ops.pre_upgrade created the deterministic marker account before the upgrade; this overlay asserts it +survived the prev→PR-head chaos crossover.""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) +import _p4 # noqa: E402 + + +def test_upgrade_preserves_data(live_app): + assert _p4.account_exists(live_app), "marker account did not survive the upgrade"