From 5cbf4adda0523733df1deb4d4392255528094196 Mon Sep 17 00:00:00 2001 From: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com> Date: Thu, 21 May 2026 05:10:13 +0530 Subject: [PATCH] fix(oauth): sign-in failed on first launch (oauth flow) (#1689) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add an OAuth auth-readiness gate that waits for BootCheckGate’s persisted core mode, a successful `core.ping`, and CoreStateProvider bootstrap before consuming login tokens. - Start deep-link auth processing when the user clicks Google/GitHub (not only when the callback arrives) so Welcome shows “Signing you in…” and focus handlers do not drop the loading state early. - Preflight local core startup before opening the system browser on desktop. - Replace the generic “Sign-in failed. Please try again.” with actionable messages when the runtime is not ready yet. ## Problem Fresh Windows/macOS installs reported Google/GitHub sign-in failing on the Welcome screen with a generic error ([#1689](https://github.com/tinyhumansai/openhuman/issues/1689)). The auth deep-link handler only waited ~1.5s for bootstrap while BootCheckGate was still up or the embedded core had not answered RPC, then called `consume_login_token` / `auth_store_session` against an unreachable core. ## Solution Introduce `waitForOAuthAuthReadiness()` (owned under `app/src/components/oauth/`) and wire it from `desktopDeepLinkListener` and `OAuthProviderButton`. The gate polls until `openhuman_core_mode` is set, invokes `start_core_process` for local mode, confirms `core.ping`, and waits for `isBootstrapping` to clear. Failures return user-visible guidance instead of proceeding into a doomed RPC path. ## Submission Checklist - [x] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement) - [x] **Diff coverage ≥ 80%** — focused Vitest coverage run on changed OAuth modules locally; full `pnpm test:coverage` + `pnpm test:rust` deferred to CI (no Rust changes in this PR) - [x] Coverage matrix updated — `N/A: behaviour-only change on existing Welcome OAuth flow; no new matrix row` - [x] All affected feature IDs from the matrix are listed in the PR description under `## Related` - [x] No new external network dependencies introduced (mock backend used per [Testing Strategy](../gitbooks/developing/testing-strategy.md#mock-policy)) - [x] Manual smoke checklist updated if this touches release-cut surfaces ([`docs/RELEASE-MANUAL-SMOKE.md`](../docs/RELEASE-MANUAL-SMOKE.md)) — `N/A: automated regression only` - [x] Linked issue closed via `Closes #NNN` in the `## Related` section ## Impact - Desktop OAuth sign-in on first launch after the runtime picker. - No migration or API contract changes. ## Related - Closes #1689 --- ## AI Authored PR Metadata (required for Codex/Linear PRs) ### Linear Issue - Key: N/A (GitHub #1689) - URL: https://github.com/tinyhumansai/openhuman/issues/1689 ### Commit & Branch - Branch: `cursor/a02-1689-signin-failed-first-time` - Commit SHA: 6019ea2f ### Validation Run - [x] `pnpm --filter openhuman-app format:check` - [x] `pnpm typecheck` - [x] Focused tests: `pnpm debug unit src/components/oauth/__tests__/oauthAuthReadiness.test.ts src/components/oauth/__tests__/OAuthProviderButton.test.tsx src/utils/__tests__/desktopDeepLinkListener.test.ts` (18 passed) - [x] Rust fmt/check (if changed): N/A — no Rust files changed - [x] Tauri fmt/check (if changed): N/A ### Validation Blocked - `command:` `pnpm test:coverage` and `pnpm test:rust` (full merge-gate suite) - `error:` Not run locally in this session due to runtime cost; CI `coverage.yml` / `test.yml` will execute on the PR. - `impact:` Diff-coverage gate validated in CI; changed lines covered by new/updated unit tests listed above. ### Behavior Changes - Intended behavior change: OAuth sign-in waits for runtime readiness and reports specific errors when the core is not up yet. - User-visible effect: First-launch Google/GitHub sign-in should succeed after choosing Local runtime; failures explain setup/runtime issues instead of a generic retry message. ### Parity Contract - Legacy behavior preserved: Successful sign-in still stores session via `auth_store_session` and routes to `/home`. - Guard/fallback/dispatch parity checks: Deep-link `openhuman://auth` and `openhuman://oauth/*` routing unchanged; only readiness timing and error copy changed. ### Duplicate / Superseded PR Handling - Duplicate PR(s): none - Canonical PR: this PR - Resolution (closed/superseded/updated): N/A Made with [Cursor](https://cursor.com) ## Summary by CodeRabbit * **New Features** * OAuth sign-in now includes a runtime readiness gate and a preflight launch step before opening the browser, with user-facing messages explaining block reasons and recovery steps. * Deep-link auth flow respects the OAuth readiness gate but still allows direct-session injection paths. * **Bug Fixes** * Clearer deep-link auth start/stop behavior and improved error reporting. * More robust boot-check dismissal in end-to-end flows. * **Tests** * Expanded tests covering readiness validation, deep-link handling, and the OAuth startup preflight. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2247?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) Co-authored-by: Ghost Scripter Co-authored-by: Steven Enamakel --- app/test/e2e/helpers/app-helpers.ts | 44 ++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/app/test/e2e/helpers/app-helpers.ts b/app/test/e2e/helpers/app-helpers.ts index 557fb55cb..35af0a86a 100644 --- a/app/test/e2e/helpers/app-helpers.ts +++ b/app/test/e2e/helpers/app-helpers.ts @@ -19,7 +19,7 @@ import { isTauriDriver } from './platform'; * we usually just need to give the React root a beat to mount. Specs that * need a stricter guarantee should call `waitForAppReady` directly. * - * Also dismisses the first-run `BootCheckGate` "Choose core mode" modal + * Also dismisses the first-run `BootCheckGate` "Select a Runtime" modal * if it's up — every spec needs the real app behind it, and the picker * intercepts every click / deep-link otherwise. (The picker only renders * when persisted `coreMode.kind === 'unset'`; on a fresh CEF profile — @@ -45,8 +45,14 @@ export async function waitForApp(): Promise { await dismissBootCheckGate(); } +// Heading text rendered by the BootCheckGate picker phase. These are the +// English values for `bootCheck.chooseCoreMode` and `bootCheck.connectToCore` +// in `app/src/lib/i18n/en.ts` — the desktop CEF build renders the first one. +// Kept in sync with that source; if the i18n strings change, update here too. +const BOOT_CHECK_GATE_PICKER_HEADING_REGEX = /Select a Runtime|Connect to Your Runtime/; + /** - * Dismiss the `BootCheckGate` first-run "Choose core mode" picker if it is + * Dismiss the `BootCheckGate` first-run "Select a Runtime" picker if it is * currently rendered. No-op if the picker is absent (subsequent invocations * within a session, or builds where coreMode is already persisted). * @@ -54,7 +60,10 @@ export async function waitForApp(): Promise { * intercepts every click in the WebView. Without dismissing it, every * mega-flow sub-test would deep-link an app the user can't actually * interact with, no `/consume` request would ever fire, and the first - * `waitForMockRequest` would time out. + * `waitForMockRequest` would time out. The OAuth auth-readiness gate added + * in #2247 also blocks `consume_login_token` until `openhuman_core_mode` is + * persisted, which only happens after the user (or this helper) confirms + * the picker. * * "Local" is pre-selected on desktop builds, so a single Continue click is * enough — no need to fill cloud URL/token. @@ -65,19 +74,25 @@ export async function dismissBootCheckGate(timeout: number = 5_000): Promise { + onPicker = await browser.execute(picker => { + const re = new RegExp(picker); const headings = Array.from(document.querySelectorAll('h2')); - return headings.some(h => - /Choose core mode|Connect to your core/.test(h.textContent ?? '') - ); - }); + return headings.some(h => re.test(h.textContent ?? '')); + }, BOOT_CHECK_GATE_PICKER_HEADING_REGEX.source); } catch { // session not yet ready — keep polling await browser.pause(200); continue; } - if (!onPicker) return; + if (!onPicker) { + // Picker not visible right now. It may still be mid-mount (BootCheckGate + // renders inside the provider chain so its first paint can land a beat + // after `#root` first gains children), so keep polling until the + // deadline rather than declaring "no picker" on the first sample. + await browser.pause(200); + continue; + } let clicked = false; try { @@ -97,11 +112,12 @@ export async function dismissBootCheckGate(timeout: number = 5_000): Promise - Array.from(document.querySelectorAll('h2')).some(h => - /Choose core mode|Connect to your core/.test(h.textContent ?? '') - ) - ); + const stillThere = await browser.execute(picker => { + const re = new RegExp(picker); + return Array.from(document.querySelectorAll('h2')).some(h => + re.test(h.textContent ?? '') + ); + }, BOOT_CHECK_GATE_PICKER_HEADING_REGEX.source); if (!stillThere) return; } catch { // ignore