From ca7acf3d525bf526d61eaaaaad6609d2ed9a7894 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Fri, 29 May 2026 20:38:25 +0100 Subject: [PATCH] =?UTF-8?q?feat(2):=20Q4.6=20discourse=20=E2=80=94=20recip?= =?UTF-8?q?e=5Fmeta=20+=20postgres=20P4=20overlays=20+=20health=20(WIP,=20?= =?UTF-8?q?=C2=A74.3=20create-topic=20next)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit discourse (forum: postgres+redis+sidekiq). HEALTH_PATH=/srv/status (slow Rails boot, DEPLOY_TIMEOUT=1800). P4 via postgres ci_marker (db service, pg_dump backupbot — matrix-synapse pattern). Health functional test. §4.3 create-a-topic + PARITY.md to follow after smoke discovers the admin/API bootstrap path. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../discourse/functional/test_health_check.py | 20 ++++++++ tests/discourse/ops.py | 47 +++++++++++++++++++ tests/discourse/recipe_meta.py | 11 +++++ tests/discourse/test_backup.py | 26 ++++++++++ tests/discourse/test_restore.py | 26 ++++++++++ 5 files changed, 130 insertions(+) create mode 100644 tests/discourse/functional/test_health_check.py create mode 100644 tests/discourse/ops.py create mode 100644 tests/discourse/recipe_meta.py create mode 100644 tests/discourse/test_backup.py create mode 100644 tests/discourse/test_restore.py diff --git a/tests/discourse/functional/test_health_check.py b/tests/discourse/functional/test_health_check.py new file mode 100644 index 0000000..50c1a60 --- /dev/null +++ b/tests/discourse/functional/test_health_check.py @@ -0,0 +1,20 @@ +"""discourse — health/readiness functional test (Phase 2). + +SOURCE: no recipe-maintainer corpus exists for discourse (P2 vacuous — see PARITY.md). This is a +Phase-2 health check aligned with the parity-port convention: /srv/status returns 200 only once the +Rails app is actually serving (the recipe's HEALTH_PATH and the canonical "is discourse up" signal). +""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) +from harness import http as harness_http # noqa: E402 + + +def test_discourse_srv_status_ok(live_app): + url = f"https://{live_app}/srv/status" + status, _ = harness_http.retry_http_get(url, expect_status=200, max_wait=120, interval=5) + assert status == 200, f"GET {url} HTTP {status} (expected 200 — discourse not serving)" diff --git a/tests/discourse/ops.py b/tests/discourse/ops.py new file mode 100644 index 0000000..73aa05d --- /dev/null +++ b/tests/discourse/ops.py @@ -0,0 +1,47 @@ +"""discourse — pre-op seed hooks (Phase 1e HC3 / Phase 2 P4 backup data-integrity). + +Discourse persists in postgres (`db` service, user/db=discourse). The recipe's backupbot pre-hook +pg_dumps that DB, and restore reloads the dump — so real backup data-integrity = seed a `ci_marker` +row into the discourse DB, back up, mutate, restore, and prove the row survived (the same DB the +recipe actually backs up). Mirrors the matrix-synapse postgres pattern. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) +from harness import lifecycle # noqa: E402 + + +def _psql(domain, sql): + cmd = ( + 'PGPASSWORD=$(cat /run/secrets/db_password) ' + f'psql -U discourse -d discourse -tAc "{sql}"' + ) + return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() + + +def _seed(domain, value): + _psql( + domain, + "CREATE TABLE IF NOT EXISTS ci_marker(v text); DELETE FROM ci_marker; " + f"INSERT INTO ci_marker VALUES('{value}');", + ) + got = _psql(domain, "SELECT v FROM ci_marker;") + assert got == value, f"seed did not commit (read back {got!r}, expected {value!r})" + + +def pre_upgrade(domain, meta): + _seed(domain, "upgrade-survives") + + +def pre_backup(domain, meta): + _seed(domain, "original") + + +def pre_restore(domain, meta): + # diverge from the backup so a successful restore is observable + _psql(domain, "DROP TABLE IF EXISTS ci_marker;") + assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in ("", "NULL"), ( + "drop did not take" + ) diff --git a/tests/discourse/recipe_meta.py b/tests/discourse/recipe_meta.py new file mode 100644 index 0000000..d39b46d --- /dev/null +++ b/tests/discourse/recipe_meta.py @@ -0,0 +1,11 @@ +# Per-recipe harness config for discourse (Phase 2 Q4.6 — forum; postgres + redis + sidekiq). +# +# Discourse (bitnami/discourse) is a slow-booting Rails app: the recipe healthcheck polls +# /srv/status with a 5-minute start_period, and a cold first boot (DB migrate + asset precompile) +# regularly takes 8-15 min, so the deploy/HTTP timeouts are generous. /srv/status returns 200 only +# once the app is actually serving (the canonical "is discourse up" signal — NOT "/", which may +# redirect to setup). +HEALTH_PATH = "/srv/status" +HEALTH_OK = (200,) +DEPLOY_TIMEOUT = 1800 +HTTP_TIMEOUT = 1200 diff --git a/tests/discourse/test_backup.py b/tests/discourse/test_backup.py new file mode 100644 index 0000000..1955078 --- /dev/null +++ b/tests/discourse/test_backup.py @@ -0,0 +1,26 @@ +"""discourse — BACKUP overlay (Phase 1e HC3 / Phase 2 P4): assertion-only + additive. + +ops.pre_backup seeded ci_marker='original' into the discourse postgres DB (which the recipe's +backupbot pre-hook pg_dumps). This overlay ADDS: the seeded row is intact at backup time. The +backup→restore divergence (dropping the table) is in ops.pre_restore. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) +from harness import lifecycle # noqa: E402 + + +def _psql(domain, sql): + cmd = ( + 'PGPASSWORD=$(cat /run/secrets/db_password) ' + f'psql -U discourse -d discourse -tAc "{sql}"' + ) + return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() + + +def test_backup_captures_state(live_app): + assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", ( + "the seeded discourse postgres state was not present at backup time" + ) diff --git a/tests/discourse/test_restore.py b/tests/discourse/test_restore.py new file mode 100644 index 0000000..7137028 --- /dev/null +++ b/tests/discourse/test_restore.py @@ -0,0 +1,26 @@ +"""discourse — RESTORE overlay (Phase 1e HC3 / Phase 2 P4): data-integrity, assertion-only. + +ops.pre_restore dropped the ci_marker table (diverge); the recipe's restore reloads the pg_dump. +This overlay ADDS: the restored DB carries the pre-mutation 'original' marker — proving the seeded +data survived backup→restore, not just that the service came back up. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) +from harness import lifecycle # noqa: E402 + + +def _psql(domain, sql): + cmd = ( + 'PGPASSWORD=$(cat /run/secrets/db_password) ' + f'psql -U discourse -d discourse -tAc "{sql}"' + ) + return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip() + + +def test_restore_returns_state(live_app): + assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", ( + "restore did not return the pre-mutation discourse postgres state (data-integrity failure)" + )