CI(ui): jump straight to /change-password to avoid /login auto-redirect race

The /login route auto-redirects to /change-password as soon as
/api/auth/status returns requires_password_change=true. The original
flow was racing that redirect: it filled #password (login mode) and
clicked submit, but the redirect could land first and the form would
have unmounted before the click. Going straight to /change-password
also matches what main._inject_bootstrap is set up to support: the
HTML on that route ships with `window.__UNSLOTH_BOOTSTRAP__`, which
the change-password form reads to seed the current-password state, so
the user only needs to fill new + confirm. Renumbered screenshots to
match the new step order.
This commit is contained in:
Daniel Han 2026-05-06 12:41:57 +00:00
commit 2d9bf084f0

View file

@ -159,22 +159,21 @@ jobs:
def shoot(name):
page.screenshot(path = str(ART / f"{name}.png"), full_page = True)
# ── 1. Login with bootstrap password ─────────────────
page.goto(BASE)
# Auth form has #password, no username field (single-user
# bootstrap), and a single submit button.
page.locator("#password").wait_for(state = "visible")
page.fill("#password", PW)
shoot("01-login-filled")
page.locator('button[type="submit"]').click()
# ── 2. Forced change-password screen ────────────────
# /api/auth/login returns must_change_password=true on
# bootstrap; the frontend redirects to the change form.
# ── 1. Bootstrap-driven change-password flow ─────────
# /api/auth/status returns requires_password_change=true
# on a fresh install, so /login auto-redirects to
# /change-password. The HTML for that route is served
# with `<script>window.__UNSLOTH_BOOTSTRAP__={...}</script>`
# injected by main._inject_bootstrap, so the form's
# current-password field is pre-seeded -- the user only
# has to fill the new password twice and submit. We jump
# straight to /change-password to avoid the /login flash
# racing with the auto-redirect.
page.goto(f"{BASE}/change-password")
page.locator("#new-password").wait_for(state = "visible", timeout = 30_000)
page.fill("#new-password", NEW)
page.fill("#confirm-password", NEW)
shoot("02-change-password-filled")
shoot("01-change-password-filled")
page.locator('button[type="submit"]').click()
# ── 3. Chat surface loads ────────────────────────────
@ -182,7 +181,7 @@ jobs:
# it's visible, the auth+UI bootstrap is complete.
composer = page.locator('textarea[aria-label="Message input"]')
composer.wait_for(state = "visible", timeout = 60_000)
shoot("03-chat-loaded")
shoot("02-chat-loaded")
# ── 4. Send a message and wait for a response ────────
composer.fill("Reply with the single word: hello")
@ -206,7 +205,7 @@ jobs:
}""",
timeout = 120_000,
)
shoot("04-assistant-replied")
shoot("03-assistant-replied")
# ── 5. Reload, confirm history persists ──────────────
page.reload()
@ -225,7 +224,7 @@ jobs:
}""",
timeout = 30_000,
)
shoot("05-history-after-reload")
shoot("04-history-after-reload")
# ── 6. Open the configuration / settings sheet ───────
# The "Open configuration" button is in chat-page.tsx
@ -234,7 +233,7 @@ jobs:
cfg = page.locator('button[aria-label="Open configuration"]').first
if cfg.count() > 0:
cfg.click()
shoot("06-settings-open")
shoot("05-settings-open")
# Close button is in chat-settings-sheet.tsx:857.
close = page.locator('button[aria-label="Close configuration"]').first
if close.count() > 0: