Skip to content

Commit 16a0731

Browse files
henrypark133claude
andauthored
test(e2e): add Playwright persistence happy-path test (#2475)
* test(e2e): add Playwright persistence happy-path test Add `test_message_persists_across_page_reload` which validates the full persistence round-trip: send a message via the chat UI, reload the page (clearing all client-side state), switch back to the thread, and verify both user message and assistant response are restored from the database. Cross-checks via the history API that exactly one user turn exists with a completed response. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(e2e): address PR review comments - Replace fixed `wait_for_timeout(2000)` with polling via history API until the turn reaches `Completed` state (avoids CI flakiness) - Use `SEL["auth_screen"]` instead of hardcoded `"#auth-screen"` selector (follows project convention from helpers.py) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8973d1b commit 16a0731

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""E2E tests for user message persistence.
2+
3+
Verifies that user messages and assistant responses survive a full page
4+
reload — the round-trip from the database.
5+
"""
6+
7+
import asyncio
8+
import os
9+
import sys
10+
11+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
12+
from helpers import (
13+
AUTH_TOKEN,
14+
SEL,
15+
api_get,
16+
api_post,
17+
send_chat_and_wait_for_terminal_message,
18+
)
19+
20+
21+
async def _wait_for_completed_turn(
22+
base_url: str,
23+
thread_id: str,
24+
*,
25+
timeout: float = 20.0,
26+
) -> list:
27+
"""Poll chat history until a completed turn appears."""
28+
deadline = asyncio.get_running_loop().time() + timeout
29+
while asyncio.get_running_loop().time() < deadline:
30+
resp = await api_get(base_url, f"/api/chat/history?thread_id={thread_id}")
31+
assert resp.status_code == 200, resp.text
32+
turns = resp.json()["turns"]
33+
if any(t.get("state") == "Completed" for t in turns):
34+
return turns
35+
await asyncio.sleep(0.5)
36+
raise AssertionError(
37+
f"Timed out waiting for completed turn in thread {thread_id}"
38+
)
39+
40+
41+
async def test_message_persists_across_page_reload(page, ironclaw_server):
42+
"""Happy-path: send a message, reload the page, both user message and
43+
assistant response survive the full round-trip from the database."""
44+
# Create an isolated thread
45+
resp = await api_post(ironclaw_server, "/api/chat/thread/new")
46+
assert resp.status_code == 200, resp.text
47+
thread_id = resp.json()["id"]
48+
49+
# Switch the page to this thread
50+
await page.evaluate("(id) => switchThread(id)", thread_id)
51+
await page.wait_for_function(
52+
"(id) => currentThreadId === id",
53+
arg=thread_id,
54+
timeout=10000,
55+
)
56+
57+
# Send a message and wait for the assistant response
58+
result = await send_chat_and_wait_for_terminal_message(page, "What is 2+2?")
59+
assert result["role"] == "assistant"
60+
assert "4" in result["text"], result
61+
62+
# Poll history API until the turn is completed (avoids flaky fixed sleep)
63+
await _wait_for_completed_turn(ironclaw_server, thread_id)
64+
65+
# Reload the page — clears all client-side state (JS vars, SSE, DOM)
66+
await page.goto(
67+
f"{ironclaw_server}/?token={AUTH_TOKEN}",
68+
timeout=15000,
69+
)
70+
await page.wait_for_selector(SEL["auth_screen"], state="hidden", timeout=10000)
71+
await page.wait_for_function(
72+
"() => typeof sseHasConnectedBefore !== 'undefined' && sseHasConnectedBefore === true",
73+
timeout=10000,
74+
)
75+
76+
# Switch back to the original thread
77+
await page.evaluate("(id) => switchThread(id)", thread_id)
78+
await page.wait_for_function(
79+
"(id) => currentThreadId === id",
80+
arg=thread_id,
81+
timeout=10000,
82+
)
83+
84+
# Verify user message survived the reload
85+
await page.locator(SEL["message_user"]).filter(
86+
has_text="What is 2+2?"
87+
).wait_for(state="visible", timeout=15000)
88+
89+
# Verify assistant response survived the reload
90+
await page.locator(SEL["message_assistant"]).filter(
91+
has_text="4"
92+
).wait_for(state="visible", timeout=15000)
93+
94+
# Cross-check via API: exactly 1 user turn with a response
95+
resp = await api_get(
96+
ironclaw_server,
97+
f"/api/chat/history?thread_id={thread_id}",
98+
)
99+
assert resp.status_code == 200, resp.text
100+
turns = resp.json()["turns"]
101+
user_turns = [t for t in turns if t.get("user_input")]
102+
assert len(user_turns) == 1, (
103+
f"Expected exactly 1 user turn, got {len(user_turns)}: {user_turns}"
104+
)
105+
assert "2+2" in user_turns[0]["user_input"] or "2 + 2" in user_turns[0]["user_input"]
106+
assert user_turns[0].get("response") and "4" in user_turns[0]["response"]
107+
assert user_turns[0]["state"] == "Completed", user_turns[0]["state"]

0 commit comments

Comments
 (0)