feat(chat): share a chat by link — anonymous read-only /shared/<token> page, share/unshare

This commit is contained in:
2026-08-30 01:34:44 -04:00
parent ece93a7c8f
commit 114b115034
28 changed files with 3442 additions and 54 deletions
+70
View File
@@ -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."""