feat(2): Q4.1 partial — matrix-synapse Phase-2 code (NOT YET cold-verified end-to-end)
Code-only commit. The Phase-2 functional tests + PARITY.md are written and locally consistent, but the e2e cold-verify on cc-ci is BLOCKED by abra deploy timing out (900s) on the matrix-synapse stack. The deploy hits the orchestrator's wait_healthy timeout — synapse + postgres-autoupgrade are too slow on this host (28GB disk, 3.5GB RAM, single node). Even after pruning Docker images (freed disk from 90% → 55% used), the deploy still times out. Root cause appears to be CPU/IO-bound startup on this host rather than disk space. What's landed (code-only): - tests/matrix-synapse/PARITY.md: parity table; the 3 recipe-maintainer shell-script tests (compress_state / test_complexity_limit / test_purge) deferred with technical rationale (operational regressions against persistent state — incompatible with the ephemeral per-run model). Phase-2 health_check added (the corpus has no health_check.py). - tests/matrix-synapse/functional/test_health_check.py: GET /_matrix/client/versions → 200 + JSON. - tests/matrix-synapse/functional/test_federation_version.py: GET /_matrix/federation/v1/version → 200, asserts server.name='Synapse' + non-empty server.version (plan §4.3 prescribed). - tests/matrix-synapse/functional/test_register_and_message.py: plan §4.3 prescribed test — registers two users via the public client API (m.login.dummy UIAA flow), logs in, creates a private_chat room, invites + joins user_b, sends an m.room.message with a uuid marker, reads the room's messages, asserts the marker appears in user_b's view. Non-vacuous full client-API roundtrip. - tests/matrix-synapse/recipe_meta.py: EXTRA_ENV adds ENABLE_REGISTRATION=true (lets the test use public client registration; admin endpoints aren't routed publicly by this recipe) and TIMEOUT=900 (overrides the recipe's default 300s abra-deploy convergence timeout). **Cold-verify status: BLOCKED on cc-ci host capacity for matrix-synapse deploys** — needs operator review (more disk / RAM / a heavier-recipe sequencing strategy). Filed in JOURNAL-2 + PushNotification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
45
tests/matrix-synapse/functional/test_federation_version.py
Normal file
45
tests/matrix-synapse/functional/test_federation_version.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""matrix-synapse — recipe-specific functional test (Phase 2 P3, ≥2 beyond parity).
|
||||
|
||||
Plan §4.3 explicitly names `/_matrix/federation/v1/version` as a matrix-synapse-specific test
|
||||
target ("federation endpoint reachable"). This endpoint is the canonical "is this server
|
||||
discoverable on the Matrix federation" probe — every Synapse exposes it, and federating servers
|
||||
hit it to learn the running implementation + version.
|
||||
|
||||
This test fetches `/_matrix/federation/v1/version` and asserts:
|
||||
1. Status 200.
|
||||
2. JSON body with the documented `server` envelope: `{ "server": { "name": ..., "version": ... } }`.
|
||||
3. `server.name` is the string `"Synapse"` (the running implementation).
|
||||
4. `server.version` is a non-empty string (the running synapse build).
|
||||
|
||||
Non-vacuous: a generic matrix-compatible server with a different implementation name (e.g.
|
||||
Dendrite) would fail the `name == "Synapse"` check. A misconfigured Synapse that hasn't booted
|
||||
its federation subsystem returns 502; a homeserver with federation disabled returns 403/404.
|
||||
|
||||
Runs in the custom tier against the shared post-install live deployment.
|
||||
"""
|
||||
|
||||
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_federation_version_endpoint(live_app):
|
||||
"""GET /_matrix/federation/v1/version → 200, JSON with server.name=Synapse."""
|
||||
url = f"https://{live_app}/_matrix/federation/v1/version"
|
||||
status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=60, interval=3)
|
||||
assert status == 200, f"GET {url} HTTP {status} (expected 200)"
|
||||
assert isinstance(body, dict), f"federation version returned non-dict: {type(body).__name__}"
|
||||
server = body.get("server")
|
||||
assert isinstance(server, dict), (
|
||||
f"federation version response missing 'server' envelope: {body!r}"
|
||||
)
|
||||
name = server.get("name")
|
||||
assert name == "Synapse", f"server.name={name!r}, expected 'Synapse'"
|
||||
version = server.get("version")
|
||||
assert isinstance(version, str) and len(version) > 0, (
|
||||
f"server.version is not a non-empty string: {version!r}"
|
||||
)
|
||||
29
tests/matrix-synapse/functional/test_health_check.py
Normal file
29
tests/matrix-synapse/functional/test_health_check.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""matrix-synapse — health-check parity port (Phase 2 P2).
|
||||
|
||||
SOURCE: there is no `recipe-info/matrix-synapse/tests/health_check.py` in the recipe-maintainer
|
||||
corpus (the corpus's matrix-synapse tests are shell scripts targeting a persistent instance —
|
||||
see DECISIONS.md for the Q4 deeper-port deferral). This file is a Phase-2 health_check ALIGNED
|
||||
with the parity-port convention: HTTP 200 from the client API versions endpoint, which is the
|
||||
recipe's HEALTH_PATH and the canonical "is synapse up" signal.
|
||||
|
||||
Runs in the custom tier against the shared post-install live deployment.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 # noqa: E402
|
||||
|
||||
|
||||
def test_synapse_client_versions_returns_json(live_app):
|
||||
"""Verify /_matrix/client/versions returns 200 + a JSON document with a non-empty versions list."""
|
||||
url = f"https://{live_app}/_matrix/client/versions"
|
||||
status, body = harness_http.retry_http_get(url, expect_status=200, max_wait=60, interval=3)
|
||||
assert status == 200, f"GET {url} HTTP {status} (expected 200)"
|
||||
assert isinstance(body, dict) and isinstance(body.get("versions"), list) and body["versions"], (
|
||||
f"GET {url} did not return Matrix client-versions document: {body!r}"
|
||||
)
|
||||
137
tests/matrix-synapse/functional/test_register_and_message.py
Normal file
137
tests/matrix-synapse/functional/test_register_and_message.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""matrix-synapse — recipe-specific functional test (Phase 2 P3 §4.3 prescribed test).
|
||||
|
||||
Plan §4.3 explicitly: "register two users; one sends a room message, the other reads it" — the
|
||||
canonical create-and-read-back for matrix-synapse.
|
||||
|
||||
Implementation note: matrix-synapse's `/_synapse/admin/v1/*` endpoints are NOT routed publicly by
|
||||
this recipe (Synapse's recommended posture). So the shared-secret admin-register endpoint is not
|
||||
reachable from a CI client. We use the public client-API register endpoint instead, enabled in
|
||||
this run via `recipe_meta.EXTRA_ENV = {"ENABLE_REGISTRATION": "true"}` — safe for ephemeral CI
|
||||
(each run is a fresh DB).
|
||||
|
||||
Flow (real Matrix client API, no mocks):
|
||||
1. Both users register via the public `/_matrix/client/v3/register` with `m.login.dummy` auth
|
||||
(a Matrix-spec "no-auth" registration UIAA stage, available when registration is enabled).
|
||||
2. Both users login via `/_matrix/client/v3/login` (password) to obtain access_tokens.
|
||||
3. user_a creates a room (`POST /_matrix/client/v3/createRoom`); invites user_b.
|
||||
4. user_b joins the room (`POST /_matrix/client/v3/join/<room_id>`).
|
||||
5. user_a PUTs an `m.room.message` with a unique marker body.
|
||||
6. user_b GETs `/_matrix/client/v3/rooms/<room_id>/messages?dir=b` and asserts the marker is in
|
||||
one of the returned events.
|
||||
|
||||
Non-vacuous: every step exercises a different layer (registration UIAA, login API, room
|
||||
create/invite/join, message send/receive). A broken Synapse fails AT the step where it's broken
|
||||
— the test diagnostic identifies which layer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 # noqa: E402
|
||||
|
||||
|
||||
def _register(domain: str, username: str, password: str) -> str:
|
||||
"""Public client-API registration with the m.login.dummy UIAA stage. Returns access_token.
|
||||
|
||||
Matrix UIAA is a two-step protocol when no session is established:
|
||||
1. POST to /register without auth → 401 with 'session' + 'flows' listing supported UIAA stages.
|
||||
2. POST again with `auth: {type: m.login.dummy, session: <session>}` + body fields.
|
||||
For homeservers with ENABLE_REGISTRATION=true and no captcha/email requirement, m.login.dummy
|
||||
is supported."""
|
||||
url = f"https://{domain}/_matrix/client/v3/register"
|
||||
|
||||
# Step 1: trigger UIAA negotiation
|
||||
s, body = harness_http.http_post(url, data={})
|
||||
assert s == 401, f"step1 expected 401 UIAA, got HTTP {s}: {body!r}"
|
||||
body = body or {}
|
||||
session = body.get("session")
|
||||
assert session, f"step1 no UIAA session: {body!r}"
|
||||
flows = body.get("flows") or []
|
||||
dummy_supported = any("m.login.dummy" in (f.get("stages") or []) for f in flows)
|
||||
assert dummy_supported, f"m.login.dummy not in flows: {flows!r}"
|
||||
|
||||
# Step 2: register with m.login.dummy auth
|
||||
s, body = harness_http.http_post(
|
||||
url,
|
||||
data={
|
||||
"auth": {"type": "m.login.dummy", "session": session},
|
||||
"username": username,
|
||||
"password": password,
|
||||
},
|
||||
)
|
||||
assert s == 200, f"step2 register {username} HTTP {s}: {body!r}"
|
||||
token = (body or {}).get("access_token")
|
||||
assert isinstance(token, str) and token, f"register returned no access_token: {body!r}"
|
||||
return token
|
||||
|
||||
|
||||
def _auth(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_register_two_users_send_receive_message(live_app):
|
||||
"""End-to-end: register 2 users via public client API; create + invite + join a room; send
|
||||
and read a message."""
|
||||
domain = live_app
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
user_a = f"alice{suffix}"
|
||||
user_b = f"bob{suffix}"
|
||||
password = "TestPass-" + uuid.uuid4().hex[:8] + "1A"
|
||||
|
||||
# Register both users via the public client API → tokens
|
||||
tok_a = _register(domain, user_a, password)
|
||||
tok_b = _register(domain, user_b, password)
|
||||
|
||||
# user_a creates a room
|
||||
s, body = harness_http.http_post(
|
||||
f"https://{domain}/_matrix/client/v3/createRoom",
|
||||
data={"preset": "private_chat", "name": f"ccci-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}"
|
||||
|
||||
# user_a invites user_b
|
||||
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}"
|
||||
|
||||
# user_b joins
|
||||
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}"
|
||||
|
||||
# user_a sends an m.room.message with a unique marker (PUT, txn_id)
|
||||
marker = f"ccci-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}"
|
||||
event_id = (body or {}).get("event_id")
|
||||
assert isinstance(event_id, str), f"send returned no event_id: {body!r}"
|
||||
|
||||
# user_b reads the room's messages; asserts the marker is present
|
||||
s, body = harness_http.http_get(
|
||||
f"https://{domain}/_matrix/client/v3/rooms/{room_id}/messages?dir=b&limit=20",
|
||||
headers=_auth(tok_b),
|
||||
)
|
||||
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"user_b did not see user_a's marker {marker!r}. event_id={event_id}; "
|
||||
f"messages={bodies[:10]}"
|
||||
)
|
||||
Reference in New Issue
Block a user