feat(chat): share a chat by link — anonymous read-only /shared/<token> page, share/unshare
This commit is contained in:
@@ -16,14 +16,18 @@ mock LLM keeps the SSE check deterministic (no live aipi).
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from e2e.auth_helpers import login
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
CHAT_QUESTION = "How is my Kubernetes cluster set up?"
|
||||
SHARE_URL_RE = re.compile(r"^/shared/[0-9a-f-]{36}$")
|
||||
|
||||
|
||||
def _expected_token() -> str:
|
||||
@@ -44,6 +48,16 @@ def _version_token(url: str) -> str:
|
||||
return url.rsplit("?v=", 1)[1]
|
||||
|
||||
|
||||
def _admin_cookies(page: Page) -> dict[str, str]:
|
||||
"""The signed session cookies the browser holds after a form login
|
||||
— used to call the admin API with plain httpx."""
|
||||
return {
|
||||
c["name"]: c["value"]
|
||||
for c in page.context.cookies()
|
||||
if "name" in c and "value" in c
|
||||
}
|
||||
|
||||
|
||||
def _stream_chat_frames(app_url: str, message: str) -> list[dict[str, Any]]:
|
||||
"""Minimal SSE chat request (same pattern as ``test_chat_rag.py``):
|
||||
POST /api/chat and collect the ``data:`` frames until the stream ends."""
|
||||
@@ -123,6 +137,62 @@ def test_other_pages_share_the_token(page: Page, app_url: str) -> None:
|
||||
assert sources_token == login_token == history_token == token
|
||||
|
||||
|
||||
def test_shared_page_is_no_cache_and_versioned(
|
||||
page: Page, app_url: str, db_ready: None
|
||||
) -> None:
|
||||
"""`/shared/<token>` (phase 51, the dynamic share page): the same
|
||||
contract as the static HTML pages — the document revalidates
|
||||
(no-cache) and the served HTML's asset refs are `?v=`-tagged (the
|
||||
middleware's prefix extension, task 01). The chat is created +
|
||||
shared via the admin API for the test."""
|
||||
token = _expected_token()
|
||||
assert token, "the version token must be non-empty"
|
||||
|
||||
login(page, app_url, next="/")
|
||||
cookies = _admin_cookies(page)
|
||||
r = httpx.post(
|
||||
f"{app_url}/api/chats",
|
||||
json={
|
||||
"messages": [
|
||||
{"who": "user", "text": "cache-busting shared-page probe"},
|
||||
{"who": "brain", "text": "Shared for the cache contract."},
|
||||
],
|
||||
"share": True,
|
||||
},
|
||||
timeout=10,
|
||||
cookies=cookies,
|
||||
)
|
||||
assert r.status_code == 201
|
||||
body = r.json()
|
||||
assert SHARE_URL_RE.fullmatch(body["share_url"]), body["share_url"]
|
||||
try:
|
||||
with page.expect_response(lambda r: "/assets/styles.css" in r.url) as css_info:
|
||||
doc = page.goto(app_url + body["share_url"])
|
||||
|
||||
# The document: always revalidated, like every HTML page.
|
||||
assert doc is not None
|
||||
assert doc.headers["cache-control"] == "no-cache"
|
||||
|
||||
# The CSS request the browser actually makes carries the
|
||||
# process token…
|
||||
assert _version_token(css_info.value.url) == token
|
||||
|
||||
# …and the served HTML references its assets versioned (the
|
||||
# page uses ABSOLUTE /assets refs — required for the nested
|
||||
# /shared/<token> path).
|
||||
html = page.content()
|
||||
assert f'href="/assets/styles.css?v={token}"' in html
|
||||
assert f'src="/assets/brand.js?v={token}"' in html
|
||||
assert f'src="/assets/markdown.js?v={token}"' in html
|
||||
assert f'src="/assets/shared.js?v={token}"' in html
|
||||
|
||||
# No unversioned reference survives the rewrite.
|
||||
assert 'href="/assets/styles.css"' not in html
|
||||
assert 'src="/assets/shared.js"' not in html
|
||||
finally:
|
||||
httpx.delete(f"{app_url}/api/chats/{body['id']}", timeout=10, cookies=cookies)
|
||||
|
||||
|
||||
def test_api_responses_unaffected(page: Page, app_url: str, db_ready: None) -> None:
|
||||
"""`/api/*` passes through untouched: no injected Cache-Control on the
|
||||
health endpoint, and the SSE chat stream still streams to done."""
|
||||
|
||||
Reference in New Issue
Block a user