From b44d75b89c130e84c97fed23094d026ce152dfc3 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Fri, 29 May 2026 15:08:52 +0100 Subject: [PATCH] =?UTF-8?q?fix(2):=20F2-13=20cryptpad=20roundtrip=20read-b?= =?UTF-8?q?ack=20robustness=20=E2=80=94=20poll=20all=20frames=20for=20mark?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../playwright/test_pad_content_roundtrip.py | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/tests/cryptpad/playwright/test_pad_content_roundtrip.py b/tests/cryptpad/playwright/test_pad_content_roundtrip.py index 099a3ed..7218c6b 100644 --- a/tests/cryptpad/playwright/test_pad_content_roundtrip.py +++ b/tests/cryptpad/playwright/test_pad_content_roundtrip.py @@ -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()