Browse tool cannot authenticate with Rails + Devise + Turbo apps
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
### Summary
The gstack browse tool (headless Playwright) cannot complete a login flow on Rails 8 + Devise + Turbo applications, making `/qa`, `/design-review`, and other browser-based skills unable to test any authenticated pages.
### Environment
- gstack browse (headless Playwright, launched mode)
- Rails 8.1 + Turbo 8 + Devise 5.0.3
- macOS Darwin 25.3.0
- App running on localhost:3000
### Problem
There are two distinct issues that compound to make authentication impossible:
**Issue 1: Devise + Turbo status code mismatch (app-side, fixable)**
Devise returns HTTP 200 on failed authentication. Turbo expects either a redirect (303) or an error status (422) for form re-renders. When `$B click` submits the login form via Turbo, a failed login produces:
```
Error: Form responses must redirect to another location
at ie.requestSucceededWithResponse (turbo.min.js:5:15107)
```
**Fix:** Set `config.responder.error_status = :unprocessable_entity` and `config.responder.redirect_status = :see_other` in `config/initializers/devise.rb`. After this fix, curl confirms correct behavior: 422 on bad password, 303 redirect on good password.
**Issue 2: Browse tool loses page context after form-based navigation (browse-side, not fixable by app)**
Even after fixing the Devise status codes, the browse tool cannot complete the login flow because:
1. `$B fill @e2 "email"` + `$B fill @e3 "pass"` + `$B click @e5` — The click triggers a 303 redirect. After the redirect completes, subsequent `$B` commands show `[browse] Starting server...` and `about:blank`. The page context is lost.
2. `$B js "form.submit()"` with `data-turbo="false"` — The standard form POST triggers a full page navigation. Same result: subsequent commands get `about:blank`.
3. `$B chain 'snapshot -i' 'fill @e2 ...' 'click @e5' 'wait 3000' 'url'` — The `fill` fails because refs from snapshot don't propagate into chain commands.
4. `$B js "fetch('/users/sign_in', {...})"` — The async fetch fires but subsequent `$B` commands restart the server connection before the result is available.
5. `$B fill "#user_email" "email"` (CSS selectors instead of refs) — Times out: `Operation timed out: fill: Timeout 5000ms exceeded.`
6. `$B focus "#user_email"` — Returns `focus requires headed mode. Run $B connect first.`
### What I observed
Each `$B` command appears to reconnect to the Playwright daemon. While the browser process (PID visible via `$B status`) stays alive, page context doesn't survive across commands when a navigation event occurs between them. The `[browse] Starting server...` message appears on most commands, suggesting the connection is re-established each time.
Cookies DO persist in the daemon (verified with `$B cookies`), but only the `__profilin` cookie — the session cookie set by the redirect response is never captured because the page context is lost before the redirect completes.
### Reproduction
Any Rails + Devise app with Turbo enabled:
```bash
B=~/.claude/skills/gstack/browse/dist/browse
$B goto "http://localhost:3000/users/sign_in"
$B snapshot -i # shows @e2 email, @e3 password, @e5 submit
$B fill @e2 "user@example.com"
$B fill @e3 "password"
$B click @e5 # triggers 303 redirect
# After this point:
$B url # returns about:blank
$B snapshot -i # returns "(no accessible elements found)"
```
### Workaround
Created a development-only bypass route that skips the login form entirely:
```ruby
# GET /dev_login?email=admin@example.com&redirect=/target/page
get "dev_login", to: "dev_login#create" if Rails.env.development?
```
The browse tool can then authenticate with a single `$B goto` that sets the session cookie and redirects — no form, no Turbo, no multi-command state to lose.
### Expected behavior
The browse tool should be able to:
1. Fill a login form across multiple commands
2. Submit the form (click or press Enter)
3. Follow the redirect
4. Continue browsing on the authenticated page with the session cookie intact
### Suggested fixes
1. **Page context persistence after navigation:** When a `click` or `press Enter` triggers a page navigation (including redirects), the browse daemon should follow the navigation and present the final page to subsequent commands, not reset to `about:blank`.
2. **Chain ref persistence:** Refs captured by `snapshot -i` inside a `chain` should be available to subsequent commands in the same chain.
3. **`fill` with CSS selectors:** `$B fill "#user_email" "value"` should work the same as `$B fill @e2 "value"`. Currently times out on Rails apps.
4. **Login helper command:** Consider a built-in `$B login ` that handles the entire flow atomically in one Playwright context, including following redirects.
Contributor guide
Assessment
This issue has not been assessed yet.