Compare commits
8 Commits
v6-matrix-
...
v6-custom-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a7d6840b3 | |||
| d6a8f6f6b6 | |||
| 5650875dfe | |||
| 2e2b90b85f | |||
| 3191e1943b | |||
| 8623398acf | |||
| acb15a43de | |||
| 9bad0ba671 |
@ -26,7 +26,7 @@ Single-writer: `## Build backlog` = Builder-only; `## Adversary findings` = Adve
|
||||
## Adversary findings
|
||||
|
||||
### [adversary] A5-4 — `matrix-synapse` stale-test/default path leaves no recipe commit status
|
||||
**Status:** OPEN — found 2026-06-01T14:16:00Z; see `REVIEW-5.md`.
|
||||
**Status:** CLOSED — re-tested 2026-06-01T18:53:30Z; see `REVIEW-5.md` follow-up entry.
|
||||
|
||||
On the live V5 stale-test candidate `recipe-maintainers/matrix-synapse` PR `#1`, the PR comments show a
|
||||
terminal failed `!testme` result for build `#53` plus the default-mode explanatory stale-test comment,
|
||||
@ -51,6 +51,10 @@ terminal outcome.
|
||||
PR on the live stale-test/default path. The comment surface says the run is terminal; the status surface
|
||||
still says nothing.
|
||||
|
||||
**Re-test result:** no longer reproducible on rerun build `#63`. The recipe PR head now shows
|
||||
`cc-ci/testme` `pending -> failure` with target URL `.../63`, and poll-only returns
|
||||
`VERDICT=PENDING BUILD=.../63` while in flight, then `VERDICT=RED BUILD=.../63` after completion.
|
||||
|
||||
### [adversary] A5-3 — `POST=1 testme-on-pr.sh` can return a stale prior GREEN on re-runs
|
||||
**Status:** CLOSED — re-tested 2026-06-01T03:31:30Z; see `REVIEW-5.md` follow-up entry.
|
||||
|
||||
|
||||
@ -361,3 +361,63 @@ Default-mode Phase-5 action taken:
|
||||
`/recipe-upgrade matrix-synapse --with-tests` to get a verified cc-ci test PR.
|
||||
|
||||
Next: treat `matrix-synapse` as the V6 candidate and prepare the dedicated cc-ci test-branch fix.
|
||||
|
||||
## 2026-06-01 — A5-4 cleared; matrix-synapse V6 branch invalidated
|
||||
|
||||
Adversary finding A5-4 was real and caused by timing around the temporary old bridge image during the
|
||||
host-recovery rollout, not by the current live bridge behavior.
|
||||
|
||||
Live re-test on the current bridge:
|
||||
- `POST=1 MAX_WAIT=90 INTERVAL=5 /srv/cc-ci-orch/.claude/skills/recipe-upgrade/testme-on-pr.sh matrix-synapse 1`
|
||||
-> `VERDICT=PENDING`
|
||||
-> `BUILD=https://drone.ci.commoninternet.net/recipe-maintainers/cc-ci/63`
|
||||
- `POST=0 MAX_WAIT=360 INTERVAL=10 /srv/cc-ci-orch/.claude/skills/recipe-upgrade/testme-on-pr.sh matrix-synapse 1`
|
||||
-> `VERDICT=RED`
|
||||
-> `BUILD=https://drone.ci.commoninternet.net/recipe-maintainers/cc-ci/63`
|
||||
- `GET /repos/recipe-maintainers/matrix-synapse/commits/21e5d84430bdc52f8fa8aa9a40fa5bda8adf06c0/status`
|
||||
now shows context `cc-ci/testme state=failure target_url=.../63`.
|
||||
|
||||
Conclusion for A5-4:
|
||||
- cleared on current live behavior; the helper can again read the verdict back from the PR via commit
|
||||
status on this stale-test/default-path candidate.
|
||||
|
||||
V6 branch-checkout work on matrix-synapse:
|
||||
- Created dedicated clone `/tmp/opencode/cc-ci-v6`, branch
|
||||
`v6-matrix-synapse-real-upgrade-state`.
|
||||
- Implemented a real app-data upgrade assertion there:
|
||||
- `tests/matrix-synapse/ops.py` now seeds two Matrix users, a room, and a message before upgrade and
|
||||
persists only `{user_b,password,room_id,marker}` to `/data/ccci-upgrade-state.json`.
|
||||
- `tests/matrix-synapse/test_upgrade.py` now logs back in after upgrade and asserts the pre-upgrade
|
||||
message is still readable from the same room.
|
||||
- Branch commit: `5edcf8d fix(tests): use real matrix data for upgrade state`
|
||||
- Pushed remote branch: `origin/v6-matrix-synapse-real-upgrade-state`
|
||||
|
||||
While verifying that branch I found and fixed a helper bug in the V6 path itself:
|
||||
- `ci-test-review/verify-pr.sh` previously passed a branch name like
|
||||
`upgrade-7.2.0+v1.153.0` straight through as `REF`, but the generic upgrade assertion expects the PR
|
||||
head COMMIT SHA there (same shape `!testme` uses). That made branch-checkout verification falsely RED
|
||||
at HC1 with `head_ref='upgrade-7.2...'` vs `chaos-version='21e5d844'`.
|
||||
- Patched `verify-pr.sh` to resolve non-SHA refs to their branch head commit via the Gitea API before
|
||||
invoking `runner/run_recipe_ci.py`.
|
||||
|
||||
Dedicated host checkout for verification:
|
||||
- materialized `/root/cc-ci-v6-verify` on `cc-ci` from the dedicated branch clone
|
||||
- marked it safe for git on the host:
|
||||
- `git config --global --add safe.directory /root/cc-ci-v6-verify`
|
||||
|
||||
Verification results:
|
||||
- First branch-verify run (before the helper fix) hit the HC1 false-red and also showed the new overlay
|
||||
login failure.
|
||||
- Second branch-verify run (after the helper fix):
|
||||
- `REMOTE_ROOT=/root/cc-ci-v6-verify RECIPE=matrix-synapse REF=upgrade-7.2.0+v1.153.0 /srv/cc-ci-orch/.claude/skills/ci-test-review/verify-pr.sh`
|
||||
- helper now resolves `REF_SHA=21e5d84430bdc52f8fa8aa9a40fa5bda8adf06c0`
|
||||
- generic upgrade tier PASSed
|
||||
- but the new real-data overlay still FAILED:
|
||||
`login upgradeb53398657 HTTP 403: {'errcode': 'M_FORBIDDEN', 'error': 'Invalid username or password'}`
|
||||
|
||||
Conclusion:
|
||||
- `matrix-synapse` is NOT a V6 stale-test branch after all.
|
||||
- Once the synthetic marker was replaced with a real Matrix data-survival assertion, the upgrade still
|
||||
failed. This points to a true recipe upgrade regression, not a stale cc-ci test.
|
||||
|
||||
Next: move to the next enrolled V5/V6 candidate (`n8n`, then `lasuite-docs`, then `keycloak`).
|
||||
|
||||
@ -267,3 +267,94 @@ the Builder's current V5 stale-test candidate plus the newly-fixed `lasuite-meet
|
||||
**Verdict:** FAIL for this live V5/V2 intersection. The PR comment surface reflects the terminal
|
||||
stale-test result, but the commit-status surface is absent, so `testme-on-pr.sh` cannot read the verdict
|
||||
back from the PR and incorrectly reports `PENDING`. Filed as `BACKLOG-5.md` item **A5-4**.
|
||||
|
||||
---
|
||||
|
||||
## Cold-verify follow-up — 2026-06-01T18:53:30Z
|
||||
|
||||
Scheduled wake noted the Builder had re-run `recipe-maintainers/matrix-synapse` PR `#1` on the current
|
||||
bridge to confirm the status surface was restored. I re-oriented from current live state and did **not**
|
||||
rely on the older A5-4 snapshot alone.
|
||||
|
||||
### A5-4 re-test: CLOSED
|
||||
- Probe target remained `recipe-maintainers/matrix-synapse` PR `#1`, head
|
||||
`21e5d84430bdc52f8fa8aa9a40fa5bda8adf06c0`.
|
||||
- Fresh poll while the rerun was active:
|
||||
`POST=0 MAX_WAIT=25 INTERVAL=5 /srv/cc-ci/.claude/skills/recipe-upgrade/testme-on-pr.sh matrix-synapse 1`
|
||||
returned:
|
||||
`VERDICT=PENDING`
|
||||
`BUILD=https://drone.ci.commoninternet.net/recipe-maintainers/cc-ci/63`
|
||||
- At that same point, the recipe head's combined status endpoint correctly reflected the in-flight run:
|
||||
`state=pending`, `context=cc-ci/testme`, `target_url=.../63`.
|
||||
- Follow-up poll after completion:
|
||||
`POST=0 MAX_WAIT=10 INTERVAL=5 /srv/cc-ci/.claude/skills/recipe-upgrade/testme-on-pr.sh matrix-synapse 1`
|
||||
returned:
|
||||
`VERDICT=RED`
|
||||
`BUILD=https://drone.ci.commoninternet.net/recipe-maintainers/cc-ci/63`
|
||||
- The recipe head's status endpoint then reflected the terminal result:
|
||||
`state=failure`, `context=cc-ci/testme`, `target_url=.../63`.
|
||||
- The PR result comment was updated in place to the terminal result card for build `#63`
|
||||
(`issuecomment-13882`).
|
||||
|
||||
**Verdict:** A5-4 is no longer reproducible on the current live bridge flow. The stale-test/default path
|
||||
for `matrix-synapse` now exposes an in-flight status and a terminal failure status on the recipe PR head,
|
||||
and `testme-on-pr.sh` reads the verdict back correctly.
|
||||
|
||||
---
|
||||
|
||||
## Current-frontier review note — 2026-06-01T19:00:00Z
|
||||
|
||||
No `Gate: <Mn> CLAIMED` was pending in `STATUS-5.md`. I re-oriented from the current live frontier rather
|
||||
than the older closed findings.
|
||||
|
||||
### Matrix-synapse V5/V6 frontier: current live state
|
||||
- Builder `STATUS-5.md` has **not** yet been refreshed to reflect the later rerun/build `#63` or any V6
|
||||
cc-ci-side branch/PR state, so I treated live Git/Gitea state as authoritative for this pass.
|
||||
- Live recipe PR state for `recipe-maintainers/matrix-synapse#1` remains:
|
||||
- `state=open`, `merged=false`, head `21e5d84430bdc52f8fa8aa9a40fa5bda8adf06c0`
|
||||
- latest result comment is the terminal failure card for build `#63`
|
||||
- head commit status is `cc-ci/testme state=failure target_url=.../63`
|
||||
- There is **no** new open cc-ci PR yet for the V6 `--with-tests` path. The only visible cc-ci-side V6
|
||||
artifact is remote branch `origin/v6-matrix-synapse-real-upgrade-state`.
|
||||
|
||||
### Branch review: V6 test direction looks materially stronger, but is not yet cold-verified end-to-end
|
||||
- I inspected the current V6 branch diff against `origin/main`.
|
||||
- The branch replaces the previous synthetic upgrade assertion (`SELECT v FROM ci_marker`) with a real
|
||||
Matrix application-data continuity probe:
|
||||
- pre-upgrade: create two Matrix users via Synapse admin registration, create a room, send a message,
|
||||
and persist only minimal metadata to `/data/ccci-upgrade-state.json`
|
||||
- post-upgrade: log in as the second user and verify the pre-upgrade message is still readable from the
|
||||
same room through the Matrix client API
|
||||
- This is directionally correct for V6 because it tests real app state instead of a cc-ci-only postgres
|
||||
marker table.
|
||||
|
||||
**Verdict:** no new live defect to file from this frontier check. But V6 is **not yet adversary-verified**:
|
||||
there is no cc-ci test PR, no paired cross-note evidence, and no cold `verify-pr.sh` result yet. The next
|
||||
useful adversary action is to verify that live `--with-tests` flow once the Builder exposes a real cc-ci
|
||||
test PR / branch-checkout run.
|
||||
|
||||
---
|
||||
|
||||
## Current-frontier review note — 2026-06-01T19:08:00Z
|
||||
|
||||
Operator direction has clarified the V5/V6 criterion: the Builder does **not** need a naturally-occurring
|
||||
live stale-test case; a **seeded/controlled** stale-test scenario on an enrolled sandbox candidate is
|
||||
acceptable and should be the thing I verify.
|
||||
|
||||
### Current live state under the seeded-case criterion
|
||||
- `STATUS-5.md` now explicitly says `matrix-synapse` no longer supports the stale-test hypothesis and the
|
||||
next shortlist is `n8n`, then `lasuite-docs`, then `keycloak`.
|
||||
- Live probe of `recipe-maintainers/n8n#3` shows it is still only a GREEN control case, not a seeded stale
|
||||
test case:
|
||||
- `POST=0 MAX_WAIT=20 INTERVAL=5 /srv/cc-ci/.claude/skills/recipe-upgrade/testme-on-pr.sh n8n 3`
|
||||
returned `VERDICT=GREEN BUILD=https://drone.ci.commoninternet.net/recipe-maintainers/cc-ci/61`
|
||||
- PR result comment and head status both reflect terminal success for build `#61`
|
||||
- `lasuite-docs` and `keycloak` currently have no open recipe PRs in `recipe-maintainers/`.
|
||||
- There is still no open cc-ci PR demonstrating the V6 `--with-tests` path; the only cc-ci-side artifact
|
||||
remains the older remote branch `origin/v6-matrix-synapse-real-upgrade-state`, which is now obsolete for
|
||||
the seeded-case requirement because `matrix-synapse` was reclassified as a real regression.
|
||||
|
||||
**Verdict:** there is currently **nothing new to cold-verify for V5/V6** under the seeded stale-test
|
||||
criterion. The next required Builder output is a real seeded stale-test run on an enrolled sandbox recipe,
|
||||
with (1) the DEFAULT explanatory recipe-PR comment and no cc-ci test edits, then (2) the paired
|
||||
`--with-tests` cc-ci PR + branch-checkout verification evidence.
|
||||
|
||||
@ -74,8 +74,8 @@ preferred, `/root/cc-ci` fallback) instead of hard-coding `/root/cc-ci`.
|
||||
| V2 — testme-on-pr.sh reads verdict | DONE | GREEN ✓ (build #29/#35); RED ✓ (build #34); rerun fix ✓ (build #43) |
|
||||
| V3 — /recipe-upgrade sandbox GREEN | DONE | custom-html-tiny PR#2; build #29 SUCCESS |
|
||||
| V4 — 3-iter regression loop | DONE | custom-html-tiny PR#5; build #34 RED, build #37 GREEN |
|
||||
| V5 — stale-test DEFAULT = comment | IN PROGRESS | matrix-synapse PR#1 comment posted; no test edits; awaiting full evidence bundle |
|
||||
| V6 — --with-tests opens+verifies cc-ci test PR | TODO | |
|
||||
| V5 — stale-test DEFAULT = comment | IN PROGRESS | matrix-synapse default-mode comment posted, but later invalidated as a likely real regression; next candidate pending |
|
||||
| V6 — --with-tests opens+verifies cc-ci test PR | TODO | matrix-synapse branch invalidated by real regression; next candidate pending |
|
||||
| V7 — mirror reconciliation | DONE | PR#1 superseded, PR#4 merged-upstream, main=upstream ✓ |
|
||||
| V8 — /upgrade-all DEFAULT run | TODO | |
|
||||
| V8a — cc-ci-upgrader agent | TODO | |
|
||||
@ -123,12 +123,22 @@ preferred, `/root/cc-ci` fallback) instead of hard-coding `/root/cc-ci`.
|
||||
Default-mode explanatory PR comment posted with no test edit:
|
||||
`https://git.autonomic.zone/recipe-maintainers/matrix-synapse/pulls/1#issuecomment-13877`
|
||||
telling the operator to re-run `/recipe-upgrade matrix-synapse --with-tests` for a test-update PR.
|
||||
- Adversary finding A5-4 is now cleared on current live behavior: re-`!testme` on the same PR head
|
||||
produced build `#63`; `POST=0 ... testme-on-pr.sh matrix-synapse 1` returned
|
||||
`VERDICT=RED BUILD=https://drone.ci.commoninternet.net/recipe-maintainers/cc-ci/63`; and
|
||||
`GET /repos/recipe-maintainers/matrix-synapse/commits/21e5d844.../status` now shows
|
||||
`cc-ci/testme state=failure target_url=.../63`.
|
||||
- V6 branch verification on `matrix-synapse` no longer supports the stale-test hypothesis. In a
|
||||
dedicated cc-ci branch checkout with a real Matrix data-survival upgrade assertion, the helper path
|
||||
now resolves the recipe branch to its head SHA correctly, generic upgrade PASSes, but the upgraded
|
||||
app still fails the real post-upgrade assertion: the pre-upgrade Matrix user cannot log in after the
|
||||
upgrade (`HTTP 403 Invalid username or password`). That points to a true recipe upgrade regression,
|
||||
not a stale test.
|
||||
|
||||
## Verification next step
|
||||
|
||||
- Advance this `matrix-synapse` stale-test candidate into V6 by preparing a dedicated cc-ci test branch,
|
||||
updating the synthetic upgrade assertion to a real app-data-preservation check, and verifying the
|
||||
recipe upgrade with that test change applied.
|
||||
- Move to the next enrolled candidate for V5/V6. Current shortlist: `n8n` first, then `lasuite-docs`,
|
||||
then `keycloak`.
|
||||
|
||||
## Phase 5 gates
|
||||
|
||||
|
||||
@ -23,11 +23,11 @@ def test_content_roundtrip(live_app):
|
||||
exact bytes round-trip. Non-vacuous: a stale page or misrouted backend would not return our
|
||||
randomly-generated content."""
|
||||
marker = f"ccci-roundtrip-{uuid.uuid4().hex}"
|
||||
# written into the served volume; nginx routes /<filename> to /usr/share/nginx/html/<filename>
|
||||
# written into the served volume; nginx routes /<filename> to /var/www/html/<filename>
|
||||
filename = f"ccci-roundtrip-{uuid.uuid4().hex[:12]}.txt"
|
||||
lifecycle.exec_in_app(
|
||||
live_app,
|
||||
["sh", "-c", f"printf %s {marker} > /usr/share/nginx/html/{filename}"],
|
||||
["sh", "-c", f"mkdir -p /var/www/html && printf %s {marker} > /var/www/html/{filename}"],
|
||||
)
|
||||
|
||||
url = f"https://{live_app}/{filename}"
|
||||
|
||||
@ -40,7 +40,7 @@ def test_content_type_html_and_txt(live_app):
|
||||
body = "hello"
|
||||
for name in (html_name, txt_name):
|
||||
lifecycle.exec_in_app(
|
||||
live_app, ["sh", "-c", f"printf %s {body} > /usr/share/nginx/html/{name}"]
|
||||
live_app, ["sh", "-c", f"mkdir -p /var/www/html && printf %s {body} > /var/www/html/{name}"]
|
||||
)
|
||||
|
||||
s_html, h_html = _head(f"https://{live_app}/{html_name}")
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""custom-html — pre-op seed hooks (Phase 1e HC3). The orchestrator runs `pre_<op>(domain, meta)`
|
||||
BEFORE it performs the op; the matching test_<op>.py asserts the post-op state (assertion-only).
|
||||
|
||||
nginx serves the volume at /usr/share/nginx/html, so the marker file survives an upgrade / a
|
||||
nginx serves the volume at /var/www/html, so the marker file survives an upgrade / a
|
||||
backup+restore of that volume and is both HTTP-readable and exec-readable."""
|
||||
|
||||
import os
|
||||
@ -10,11 +10,11 @@ import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
||||
from harness import lifecycle # noqa: E402
|
||||
|
||||
MARKER_PATH = "/usr/share/nginx/html/ci-marker.txt"
|
||||
MARKER_PATH = "/var/www/html/ci-marker.txt"
|
||||
|
||||
|
||||
def _write(domain: str, val: str) -> None:
|
||||
lifecycle.exec_in_app(domain, ["sh", "-c", f"echo {val} > {MARKER_PATH}"])
|
||||
lifecycle.exec_in_app(domain, ["sh", "-c", f"mkdir -p $(dirname {MARKER_PATH}) && echo {val} > {MARKER_PATH}"])
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
|
||||
@ -12,7 +12,7 @@ import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
||||
from harness import lifecycle # noqa: E402
|
||||
|
||||
MARKER_PATH = "/usr/share/nginx/html/ci-marker.txt"
|
||||
MARKER_PATH = "/var/www/html/ci-marker.txt"
|
||||
|
||||
|
||||
def test_backup_captures_state(live_app):
|
||||
|
||||
@ -12,7 +12,7 @@ import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
||||
from harness import lifecycle # noqa: E402
|
||||
|
||||
MARKER_PATH = "/usr/share/nginx/html/ci-marker.txt"
|
||||
MARKER_PATH = "/var/www/html/ci-marker.txt"
|
||||
|
||||
|
||||
def test_restore_returns_state(live_app):
|
||||
|
||||
@ -11,7 +11,7 @@ import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
||||
from harness import lifecycle # noqa: E402
|
||||
|
||||
MARKER_PATH = "/usr/share/nginx/html/ci-marker.txt"
|
||||
MARKER_PATH = "/var/www/html/ci-marker.txt"
|
||||
|
||||
|
||||
def test_upgrade_preserves_data(live_app):
|
||||
|
||||
@ -1,24 +1,13 @@
|
||||
"""matrix-synapse — pre-op seed hooks (Phase 1e HC3).
|
||||
"""matrix-synapse — pre-op seed hooks (Phase 1e HC3). The orchestrator runs these BEFORE the op; the
|
||||
matching test_<op>.py asserts post-op (assertion-only). The marker is a dedicated `ci_marker` row in
|
||||
postgres (synapse's own schema migrations don't touch it), written via psql in the `db` service. The
|
||||
backup path exercises the recipe's pg_backup.sh DB-dump hook, not a plain volume copy."""
|
||||
|
||||
The orchestrator runs these BEFORE the op; the matching test_<op>.py asserts post-op (assertion-only).
|
||||
Backup/restore still use a dedicated `ci_marker` row in postgres (the recipe's pg_backup.sh dump path).
|
||||
Upgrade now seeds REAL Matrix application data instead: two users, a room, and a message. The helper
|
||||
persists only the test metadata to `/data/ccci-upgrade-state.json` so the post-upgrade assertion can
|
||||
log back in and prove the pre-upgrade message is still readable via the real Matrix API.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
||||
from harness import http as harness_http, lifecycle # noqa: E402
|
||||
|
||||
|
||||
UPGRADE_STATE = "/data/ccci-upgrade-state.json"
|
||||
from harness import lifecycle # noqa: E402
|
||||
|
||||
|
||||
def _psql(domain, sql):
|
||||
@ -35,169 +24,8 @@ def _seed(domain, value):
|
||||
assert _psql(domain, "SELECT v FROM ci_marker;") == value
|
||||
|
||||
|
||||
def _registration_secret(domain: str) -> str:
|
||||
return lifecycle.exec_in_app(domain, ["cat", "/run/secrets/registration"]).strip()
|
||||
|
||||
|
||||
def _container_curl(domain: str, method: str, path: str, body: dict | None = None) -> dict:
|
||||
import shlex
|
||||
|
||||
cmd_parts = ["curl", "-s", "-X", method, "-w", "\\n%{http_code}"]
|
||||
if body is not None:
|
||||
cmd_parts += ["-H", "Content-Type: application/json", "-d", json.dumps(body)]
|
||||
cmd_parts.append(f"http://localhost:8008{path}")
|
||||
sh_cmd = " ".join(shlex.quote(p) for p in cmd_parts)
|
||||
out = lifecycle.exec_in_app(domain, ["sh", "-c", sh_cmd]).strip()
|
||||
if "\n" in out:
|
||||
body_str, _, status_str = out.rpartition("\n")
|
||||
else:
|
||||
body_str, status_str = out, "0"
|
||||
try:
|
||||
status = int(status_str.strip())
|
||||
except ValueError:
|
||||
status = 0
|
||||
try:
|
||||
parsed = json.loads(body_str) if body_str.strip() else None
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
parsed = None
|
||||
return {"status": status, "body": parsed, "raw": body_str}
|
||||
|
||||
|
||||
def _admin_register(domain: str, secret: str, username: str, password: str, admin: bool) -> dict:
|
||||
import time
|
||||
|
||||
admin_flag = "admin" if admin else "notadmin"
|
||||
deadline = time.monotonic() + 90
|
||||
last = {"status": 0, "body": None, "raw": ""}
|
||||
attempt = 0
|
||||
while time.monotonic() < deadline:
|
||||
attempt += 1
|
||||
r = _container_curl(domain, "GET", "/_synapse/admin/v1/register")
|
||||
if r["status"] in (500, 502, 503, 504, 0):
|
||||
last = r
|
||||
time.sleep(5)
|
||||
continue
|
||||
assert r["status"] == 200, f"nonce GET failed: status={r['status']} raw={r['raw'][:200]!r}"
|
||||
nonce = (r["body"] or {}).get("nonce")
|
||||
assert nonce, f"no nonce in response: {r['body']!r}"
|
||||
|
||||
msg = f"{nonce}\0{username}\0{password}\0{admin_flag}".encode()
|
||||
mac = hmac.new(secret.encode(), msg, hashlib.sha1).hexdigest()
|
||||
payload = {
|
||||
"nonce": nonce,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"mac": mac,
|
||||
"admin": admin,
|
||||
}
|
||||
r = _container_curl(domain, "POST", "/_synapse/admin/v1/register", body=payload)
|
||||
if r["status"] == 200:
|
||||
return r["body"] or {}
|
||||
if r["status"] in (500, 502, 503, 504, 0):
|
||||
last = r
|
||||
time.sleep(5)
|
||||
continue
|
||||
raise AssertionError(f"register {username!r} rejected: status={r['status']} body={r['body']!r}")
|
||||
raise AssertionError(
|
||||
f"register {username!r} never succeeded within 90s: "
|
||||
f"last status={last['status']} body={last['body']!r}"
|
||||
)
|
||||
|
||||
|
||||
def _login(domain: str, username: str, password: str) -> str:
|
||||
url = f"https://{domain}/_matrix/client/v3/login"
|
||||
s, body = harness_http.http_post(
|
||||
url,
|
||||
data={
|
||||
"type": "m.login.password",
|
||||
"identifier": {"type": "m.id.user", "user": username},
|
||||
"password": password,
|
||||
},
|
||||
)
|
||||
assert s == 200, f"login {username} HTTP {s}: {body!r}"
|
||||
token = (body or {}).get("access_token")
|
||||
assert isinstance(token, str) and token, f"login returned no access_token: {body!r}"
|
||||
return token
|
||||
|
||||
|
||||
def _auth(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _write_upgrade_state(domain: str, payload: dict) -> None:
|
||||
script = (
|
||||
"import json; "
|
||||
f"open({UPGRADE_STATE!r}, 'w').write(json.dumps({json.dumps(payload)}))"
|
||||
)
|
||||
lifecycle.exec_in_app(domain, ["python", "-c", script])
|
||||
|
||||
|
||||
def _read_messages(domain: str, room_id: str, token: str) -> list[str]:
|
||||
s, body = harness_http.http_get(
|
||||
f"https://{domain}/_matrix/client/v3/rooms/{room_id}/messages?dir=b&limit=20",
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert s == 200, f"read messages HTTP {s}: {body!r}"
|
||||
chunk = (body or {}).get("chunk", [])
|
||||
return [e.get("content", {}).get("body", "") for e in chunk if isinstance(e, dict)]
|
||||
|
||||
|
||||
def pre_upgrade(domain, meta):
|
||||
secret = _registration_secret(domain)
|
||||
assert secret and len(secret) >= 16, (
|
||||
f"registration shared secret missing/short: len={len(secret) if secret else 0}"
|
||||
)
|
||||
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
user_a = f"upgradea{suffix}"
|
||||
user_b = f"upgradeb{suffix}"
|
||||
password = "UpgradePass-" + uuid.uuid4().hex[:8] + "1A"
|
||||
|
||||
_admin_register(domain, secret, user_a, password, admin=False)
|
||||
_admin_register(domain, secret, user_b, password, admin=False)
|
||||
|
||||
tok_a = _login(domain, user_a, password)
|
||||
tok_b = _login(domain, user_b, password)
|
||||
|
||||
s, body = harness_http.http_post(
|
||||
f"https://{domain}/_matrix/client/v3/createRoom",
|
||||
data={"preset": "private_chat", "name": f"ccci-upgrade-room-{suffix}"},
|
||||
headers=_auth(tok_a),
|
||||
)
|
||||
assert s == 200, f"createRoom HTTP {s}: {body!r}"
|
||||
room_id = (body or {}).get("room_id")
|
||||
assert isinstance(room_id, str) and room_id.startswith("!"), f"bad room_id: {room_id!r}"
|
||||
|
||||
s, body = harness_http.http_post(
|
||||
f"https://{domain}/_matrix/client/v3/rooms/{room_id}/invite",
|
||||
data={"user_id": f"@{user_b}:{domain}"},
|
||||
headers=_auth(tok_a),
|
||||
)
|
||||
assert s == 200, f"invite HTTP {s}: {body!r}"
|
||||
|
||||
s, body = harness_http.http_post(
|
||||
f"https://{domain}/_matrix/client/v3/join/{room_id}", data={}, headers=_auth(tok_b)
|
||||
)
|
||||
assert s == 200, f"join HTTP {s}: {body!r}"
|
||||
|
||||
marker = f"ccci-upgrade-marker-{uuid.uuid4().hex}"
|
||||
txn_id = uuid.uuid4().hex
|
||||
s, body = harness_http.http_request(
|
||||
"PUT",
|
||||
f"https://{domain}/_matrix/client/v3/rooms/{room_id}/send/m.room.message/{txn_id}",
|
||||
data={"msgtype": "m.text", "body": marker},
|
||||
headers=_auth(tok_a),
|
||||
)
|
||||
assert s == 200, f"send HTTP {s}: {body!r}"
|
||||
assert isinstance((body or {}).get("event_id"), str), f"send returned no event_id: {body!r}"
|
||||
|
||||
bodies = _read_messages(domain, room_id, tok_b)
|
||||
assert marker in bodies, f"pre-upgrade marker {marker!r} not visible before upgrade: {bodies[:10]}"
|
||||
|
||||
_write_upgrade_state(
|
||||
domain,
|
||||
{"user_b": user_b, "password": password, "room_id": room_id, "marker": marker},
|
||||
)
|
||||
_seed(domain, "upgrade-survives")
|
||||
|
||||
|
||||
def pre_backup(domain, meta):
|
||||
|
||||
@ -1,65 +1,22 @@
|
||||
"""matrix-synapse — UPGRADE overlay (Phase 1e HC3): real application-data continuity.
|
||||
"""matrix-synapse — UPGRADE overlay (Phase 1e HC3): data-continuity, assertion-only + additive.
|
||||
|
||||
ops.pre_upgrade seeds REAL Matrix state before the upgrade: two users, a room, and a message. It
|
||||
persists only the test metadata to `/data/ccci-upgrade-state.json` in the Synapse app volume. The
|
||||
orchestrator then performs the upgrade once (generic tier asserts reconverge/serving/moved). This
|
||||
overlay ADDS: after upgrade, the same Matrix user can still log in and read the pre-upgrade message
|
||||
from the same room.
|
||||
"""
|
||||
ops.pre_upgrade wrote a postgres marker row before the upgrade; the orchestrator performed the
|
||||
upgrade once (generic tier asserted reconverge/serving/moved). This overlay ADDS: the postgres data
|
||||
survived. Read via psql in the `db` service."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
||||
from harness import http as harness_http, lifecycle # noqa: E402
|
||||
from harness import lifecycle # noqa: E402
|
||||
|
||||
|
||||
UPGRADE_STATE = "/data/ccci-upgrade-state.json"
|
||||
|
||||
|
||||
def _login(domain: str, username: str, password: str) -> str:
|
||||
s, body = harness_http.http_post(
|
||||
f"https://{domain}/_matrix/client/v3/login",
|
||||
data={
|
||||
"type": "m.login.password",
|
||||
"identifier": {"type": "m.id.user", "user": username},
|
||||
"password": password,
|
||||
},
|
||||
)
|
||||
assert s == 200, f"login {username} HTTP {s}: {body!r}"
|
||||
token = (body or {}).get("access_token")
|
||||
assert isinstance(token, str) and token, f"login returned no access_token: {body!r}"
|
||||
return token
|
||||
|
||||
|
||||
def _auth(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def _upgrade_state(domain: str) -> dict:
|
||||
raw = lifecycle.exec_in_app(domain, ["cat", UPGRADE_STATE]).strip()
|
||||
state = json.loads(raw)
|
||||
assert isinstance(state, dict), f"upgrade state is not a dict: {state!r}"
|
||||
return state
|
||||
def _psql(domain, sql):
|
||||
cmd = f'PGPASSWORD=$(cat /run/secrets/db_password) psql -U synapse -d synapse -tAc "{sql}"'
|
||||
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
|
||||
|
||||
|
||||
def test_upgrade_preserves_data(live_app):
|
||||
state = _upgrade_state(live_app)
|
||||
user_b = state["user_b"]
|
||||
password = state["password"]
|
||||
room_id = state["room_id"]
|
||||
marker = state["marker"]
|
||||
|
||||
token = _login(live_app, user_b, password)
|
||||
s, body = harness_http.http_get(
|
||||
f"https://{live_app}/_matrix/client/v3/rooms/{room_id}/messages?dir=b&limit=20",
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert s == 200, f"read messages HTTP {s}: {body!r}"
|
||||
chunk = (body or {}).get("chunk", [])
|
||||
bodies = [e.get("content", {}).get("body", "") for e in chunk if isinstance(e, dict)]
|
||||
assert marker in bodies, (
|
||||
f"post-upgrade user {user_b!r} did not see pre-upgrade marker {marker!r}; "
|
||||
f"room={room_id!r} messages={bodies[:10]}"
|
||||
)
|
||||
assert (
|
||||
_psql(live_app, "SELECT v FROM ci_marker;") == "upgrade-survives"
|
||||
), "postgres data did not survive the upgrade"
|
||||
|
||||
Reference in New Issue
Block a user