fix(e2e): cloud auth via cookie, local port, Playwright WebSocket gotcha

E2E harness fixes to get all three modes (demo/cloud/local) passing:

- conftest.py: use ctx.add_cookies() for cloud auth instead of
  ctx.route() or set_extra_http_headers(). Playwright's route() only
  intercepts HTTP; set_extra_http_headers() explicitly excludes
  WebSocket handshakes. Streamlit reads st.context.headers from the
  WebSocket upgrade, so cookies are the only vehicle that reaches it
  without a reverse proxy.

- cloud_session.py: fall back to Cookie header when X-CF-Session is
  absent — supports direct access (E2E tests, dev without Caddy).
  In production Caddy sets X-CF-Session; in tests the cf_session cookie
  is set on the browser context and arrives in the Cookie header.

- modes/cloud.py: add /peregrine base URL path (STREAMLIT_SERVER_BASE_URL_PATH=peregrine)

- modes/local.py: correct port from 8502 → 8501 and add /peregrine path

All three modes now pass smoke + interaction tests clean.
This commit is contained in:
pyr0ball 2026-03-17 20:01:42 -07:00
parent 0758b70306
commit 167fa8d84a
4 changed files with 19 additions and 8 deletions

View file

@ -151,7 +151,12 @@ def resolve_session(app: str = "peregrine") -> None:
if st.session_state.get("user_id"): if st.session_state.get("user_id"):
return return
cookie_header = st.context.headers.get("x-cf-session", "") # Primary: Caddy injects X-CF-Session header in production.
# Fallback: direct access (E2E tests, dev without Caddy) reads the cookie header.
cookie_header = (
st.context.headers.get("x-cf-session", "")
or st.context.headers.get("cookie", "")
)
session_jwt = _extract_session_token(cookie_header) session_jwt = _extract_session_token(cookie_header)
if not session_jwt: if not session_jwt:
_render_auth_wall("Please sign in to access Peregrine.") _render_auth_wall("Please sign in to access Peregrine.")

View file

@ -93,11 +93,17 @@ def mode_contexts(active_modes, playwright) -> dict[str, BrowserContext]:
for mode in active_modes: for mode in active_modes:
ctx = browser.new_context(viewport={"width": 1280, "height": 900}) ctx = browser.new_context(viewport={"width": 1280, "height": 900})
if mode.name == "cloud": if mode.name == "cloud":
def _inject_jwt(route, request): # Cookies are sent on WebSocket upgrade requests; set_extra_http_headers
jwt = _get_jwt() # and ctx.route() are both HTTP-only and miss st.context.headers.
headers = {**request.headers, "x-cf-session": f"cf_session={jwt}"} # cloud_session.py falls back to the Cookie header when X-CF-Session
route.continue_(headers=headers) # is absent (direct access without Caddy).
ctx.route(f"{mode.base_url}/**", _inject_jwt) jwt = _get_jwt()
ctx.add_cookies([{
"name": "cf_session",
"value": jwt,
"domain": "localhost",
"path": "/",
}])
else: else:
mode.auth_setup(ctx) mode.auth_setup(ctx)
contexts[mode.name] = ctx contexts[mode.name] = ctx

View file

@ -68,7 +68,7 @@ def _cloud_auth_setup(context: Any) -> None:
CLOUD = ModeConfig( CLOUD = ModeConfig(
name="cloud", name="cloud",
base_url="http://localhost:8505", base_url="http://localhost:8505/peregrine",
auth_setup=_cloud_auth_setup, auth_setup=_cloud_auth_setup,
expected_failures=[], expected_failures=[],
results_dir=Path("tests/e2e/results/cloud"), results_dir=Path("tests/e2e/results/cloud"),

View file

@ -9,7 +9,7 @@ _BASE_SETTINGS_TABS = [
LOCAL = ModeConfig( LOCAL = ModeConfig(
name="local", name="local",
base_url="http://localhost:8502", base_url="http://localhost:8501/peregrine",
auth_setup=lambda ctx: None, auth_setup=lambda ctx: None,
expected_failures=[], expected_failures=[],
results_dir=Path("tests/e2e/results/local"), results_dir=Path("tests/e2e/results/local"),