feat(chat): save by default + share anonymously — auto-saved chats, guest-facing Share, success toast, action row
This commit is contained in:
@@ -8,10 +8,11 @@ Run in isolation (DB must be up: ``podman compose up -d db``):
|
||||
|
||||
The full user-visible invalidation loop under test:
|
||||
|
||||
* **Save (fresh)** — as admin: ask (the mock answers deterministically),
|
||||
Save via the chat-page pill; ``GET /api/chats`` (admin cookie) reports
|
||||
the row with ``stale: false``; opening the FRESH row at ``/?chat=<id>``
|
||||
shows NO banner;
|
||||
* **Auto-save (fresh, phase 55)** — as admin: ask (the mock answers
|
||||
deterministically) — the conversation auto-saves at the save points
|
||||
(the Save pill is gone); ``GET /api/chats`` (admin cookie) reports
|
||||
the row with ``stale: false``; opening the FRESH row at
|
||||
``/?chat=<id>`` shows NO banner;
|
||||
* **KB change** — the test process (which shares the app's environment)
|
||||
bumps the ``sources_meta`` seed row through ``bump_sources_version``
|
||||
over a short ``SessionLocal()`` — the exact helper BOTH real sync
|
||||
@@ -60,6 +61,7 @@ the click.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
@@ -174,11 +176,25 @@ def _ask(page: Page, question: str) -> None:
|
||||
expect(page.locator("#send-label")).to_have_text("Send")
|
||||
|
||||
|
||||
def _save(page: Page) -> None:
|
||||
"""Press Save and wait for the live-region confirmation (the
|
||||
never-stale contract: the status line is the success feedback)."""
|
||||
page.locator("#save-chat-btn").click()
|
||||
expect(page.locator("#send-status")).to_have_text("Conversation saved.")
|
||||
def _wait_saved_row(
|
||||
app_url: str,
|
||||
cookies: dict[str, str],
|
||||
title: str,
|
||||
messages: int = 2,
|
||||
) -> dict[str, Any]:
|
||||
"""Wait for the auto-saved row (phase 55: auto-saves are SILENT —
|
||||
A2 — so there is no status line to wait on). The upsert is
|
||||
fire-and-forget from the UI's point of view, so poll the admin
|
||||
list until the row with the conversation's auto-title appears with
|
||||
the expected message count."""
|
||||
deadline = time.monotonic() + 15
|
||||
last: dict[str, Any] | None = None
|
||||
while time.monotonic() < deadline:
|
||||
last = _find_row(_chats(app_url, cookies), title)
|
||||
if last is not None and last["message_count"] >= messages:
|
||||
return last
|
||||
time.sleep(0.2)
|
||||
raise AssertionError(f"no auto-saved row for {title!r} (last: {last!r})")
|
||||
|
||||
|
||||
def _admin_cookies(page: Page) -> dict[str, str]:
|
||||
@@ -298,11 +314,10 @@ def test_full_invalidation_loop(
|
||||
q = "How is my Kubernetes cluster set up? (stale-loop)"
|
||||
_ask(page, q)
|
||||
|
||||
# --- Save (fresh): the API agrees, and a fresh open shows NO banner.
|
||||
_save(page)
|
||||
# --- Auto-save (fresh, phase 55 — no Save pill): the API agrees,
|
||||
# and a fresh open shows NO banner.
|
||||
cookies = _admin_cookies(page)
|
||||
row = _find_row(_chats(app_url, cookies), _auto_title(q))
|
||||
assert row is not None, "the saved chat row must exist"
|
||||
row = _wait_saved_row(app_url, cookies, _auto_title(q))
|
||||
assert row["message_count"] == 2
|
||||
assert row["stale"] is False, "a just-saved chat is fresh, not stale"
|
||||
chat_id: str = row["id"]
|
||||
@@ -467,11 +482,9 @@ def test_anonymous_shared_snapshot_has_no_staleness_surface(
|
||||
|
||||
q = "How is my Kubernetes cluster set up? (stale-share)"
|
||||
_ask(page, q)
|
||||
_save(page)
|
||||
|
||||
cookies = _admin_cookies(page)
|
||||
row = _find_row(_chats(app_url, cookies), _auto_title(q))
|
||||
assert row is not None
|
||||
row = _wait_saved_row(app_url, cookies, _auto_title(q)) # auto-saved (phase 55)
|
||||
chat_id: str = row["id"]
|
||||
anon_ctx: BrowserContext | None = None
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user