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 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): def _dismiss_store_modal(page):
"""Anonymous pads prompt 'store in CryptDrive?'. Dismiss it (DON'T STORE) so it can't intercept """Anonymous pads prompt 'store in CryptDrive?'. Dismiss it (DON'T STORE) so it can't intercept
editor clicks. Non-fatal if absent.""" editor clicks. Non-fatal if absent."""
@ -120,22 +147,23 @@ def test_cryptpad_pad_content_survives_fresh_session(live_app):
body.click() body.click()
page.wait_for_timeout(1000) page.wait_for_timeout(1000)
body.type(marker, delay=40) 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(), ( assert marker in ck.locator("body").inner_text(), (
"marker not present in the editor after typing — type did not land" "marker not present in the editor after typing — type did not land"
) )
ctx1.close() 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) ctx2 = browser.new_context(ignore_https_errors=True)
page2, _ = _open_pad(ctx2, pad_url) page2, _ = _open_pad(ctx2, pad_url)
ck2 = _ckeditor_frame(page2, reload_url=pad_url) found = _poll_any_frame_for_text(page2, marker, reload_url=pad_url)
assert ck2 is not None, "CKEditor content frame never attached on read-back" assert found, (
page2.wait_for_timeout(6000) # let the pad load + decrypt
readback = ck2.locator("body").inner_text()
assert marker in readback, (
f"marker {marker!r} did NOT survive into a fresh session — content not persisted/" 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: finally:
browser.close() browser.close()