mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(oauth): sign-in failed on first launch (oauth flow) (#1689)
## 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)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## 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_stack_entry_start -->
[](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2247?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)
<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Ghost Scripter <ghostscripter@zerolend.xyz>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Ghost Scripter
Steven Enamakel
parent
1c5f199cc7
commit
5cbf4adda0
@@ -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<void> {
|
||||
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<void> {
|
||||
* 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<voi
|
||||
while (Date.now() < deadline) {
|
||||
let onPicker = false;
|
||||
try {
|
||||
onPicker = await browser.execute(() => {
|
||||
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<voi
|
||||
const dismissDeadline = Date.now() + 5_000;
|
||||
while (Date.now() < dismissDeadline) {
|
||||
try {
|
||||
const stillThere = await browser.execute(() =>
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user