fix(2): F2-13 cryptpad roundtrip read-back robustness — poll all frames for marker

Adversary cold-verify of F2-9 FAILED: the read-back's CKEditor-frame-attach wait timed out on a fresh
cold context (flaky, not 3x-reliable). Fix: read-back now polls EVERY frame's body text for the marker
(don't require the specific ckeditor-inner frame to attach — that's the flaky part) with a generous
~240s deadline + periodic reloads to unstick cold loads. The marker appearing in a fresh context still
proves server-side E2E-encrypted persistence (only URL+fragment key carried over). Also bumped the
session-1 post-type sync wait 9s→12s. F2-13 Adversary-owned; will validate cold before it closes F2-9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 15:08:52 +01:00
parent 1cbb1ccd73
commit b44d75b89c

View File

@ -85,6 +85,33 @@ def _ckeditor_frame(page, deadline_polls=90, reload_at=22, reload_url=None):
return None
def _poll_any_frame_for_text(page, needle, deadline_polls=120, reload_at=(20, 45, 75, 100), reload_url=None):
"""Robust read-back (F2-13): poll EVERY frame's body text for `needle`, returning True as soon as
it appears. The fresh cold-cache read-back context's deeply-nested CKEditor frame is slow/flaky to
*attach* by URL (the prior `_ckeditor_frame` wait timed out on the Adversary's cold run), but the
decrypted pad content lands in whichever frame renders it — so we don't require a specific frame,
we just watch all of them for the marker. Reload periodically to unstick a stalled cold load.
Generous deadline (~deadline_polls*2s) since the fresh context recompiles LESS + re-fetches +
decrypts. Returns False if the marker never appears (genuine non-persistence)."""
for i in range(deadline_polls):
for f in page.frames:
try:
if needle in (f.locator("body").inner_text(timeout=2000) or ""):
return True
except Exception: # noqa: BLE001 — frame not ready / detached; keep polling
pass
if reload_url and i in reload_at:
try:
harness_browser.goto_with_retry(
page, reload_url, accept_statuses=(200,), goto_timeout_ms=60_000,
wait_until="load", deadline_seconds=120,
)
except Exception: # noqa: BLE001 — best-effort unstick
pass
page.wait_for_timeout(2000)
return False
def _dismiss_store_modal(page):
"""Anonymous pads prompt 'store in CryptDrive?'. Dismiss it (DON'T STORE) so it can't intercept
editor clicks. Non-fatal if absent."""
@ -120,22 +147,23 @@ def test_cryptpad_pad_content_survives_fresh_session(live_app):
body.click()
page.wait_for_timeout(1000)
body.type(marker, delay=40)
page.wait_for_timeout(9000) # let CryptPad encrypt + sync the update to the server
page.wait_for_timeout(12000) # let CryptPad encrypt + sync the update to the server
assert marker in ck.locator("body").inner_text(), (
"marker not present in the editor after typing — type did not land"
)
ctx1.close()
# --- session 2: FRESH context (no shared storage) reads the pad back by URL ---
# --- session 2: FRESH context (no shared storage/localStorage) reads the pad back by URL.
# Robust read-back (F2-13): poll ALL frames for the marker (don't require the CKEditor
# frame to attach — that's the flaky part on a cold fresh context). The marker appearing
# in a brand-new session proves the content was persisted server-side (encrypted) and
# decrypted from only the URL+fragment key.
ctx2 = browser.new_context(ignore_https_errors=True)
page2, _ = _open_pad(ctx2, pad_url)
ck2 = _ckeditor_frame(page2, reload_url=pad_url)
assert ck2 is not None, "CKEditor content frame never attached on read-back"
page2.wait_for_timeout(6000) # let the pad load + decrypt
readback = ck2.locator("body").inner_text()
assert marker in readback, (
found = _poll_any_frame_for_text(page2, marker, reload_url=pad_url)
assert found, (
f"marker {marker!r} did NOT survive into a fresh session — content not persisted/"
f"decrypted. Read-back body excerpt: {readback[:200]!r}"
"decrypted (polled all frames + reloads to a generous deadline)"
)
finally:
browser.close()