Skip to content

fix(auth): close web and desktop logout contracts - #1487

Merged
DeliciousBuding merged 1 commit into
masterfrom
fix/frontend-logout-contract
Aug 1, 2026
Merged

fix(auth): close web and desktop logout contracts#1487
DeliciousBuding merged 1 commit into
masterfrom
fix/frontend-logout-contract

Conversation

@DeliciousBuding

@DeliciousBuding DeliciousBuding commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • wire the shared workbench logout action to the real Web/Desktop auth singleton
  • clear QueryClient/private UI state and return to the appropriate login surface
  • make ProfilePopover portal to document.body so the logout action is not clipped or intercepted by the conversation sidebar
  • make renderer OIDC tests fail closed on a reserved Hub origin and align callback/profile fixtures with the current auth contract
  • add a focused portal regression test

Evidence

  • Desktop App unit: 10 passed
  • Web App unit: 6 passed
  • Shared ProfilePopover unit: 1 passed
  • Desktop OIDC renderer logout: 1 passed
  • Web OIDC renderer suite: 19 passed, 1 approved-real test skipped by its explicit approval gate
  • Desktop/Web typechecks passed
  • git diff --check passed

Known baseline debt

  • Shared package-wide tsc --noEmit remains red on pre-existing Storybook/test typing errors outside this slice; the new ProfilePopover test passes independently.
  • Renderer evidence is stubbed Hub + real browser UI, not packaged Tauri or approved-real production login.

Summary by CodeRabbit

  • Bug Fixes

    • Improved logout handling across desktop and web apps, including clearing session data and returning to the sign-in screen.
    • Fixed account menus so they remain visible when opened inside overflow-limited areas.
    • Improved authentication callback handling for apps hosted under a configured base path.
  • Tests

    • Expanded coverage for sign-in, logout, session clearing, account menus, and authentication state transitions.

Copilot AI review requested due to automatic review settings August 1, 2026 13:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@DeliciousBuding, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9eaed9bd-135a-448f-8cc4-b3d28e34d1d4

📥 Commits

Reviewing files that changed from the base of the PR and between 8740663 and d5c79fb.

📒 Files selected for processing (11)
  • app/desktop/playwright.config.ts
  • app/desktop/src/App.tsx
  • app/desktop/src/__e2e__/oidc-login.spec.ts
  • app/desktop/src/__tests__/App.v4.test.tsx
  • app/shared/src/workbench/floating/ProfilePopover.test.tsx
  • app/shared/src/workbench/floating/ProfilePopover.tsx
  • app/web/playwright.config.ts
  • app/web/src/App.test.tsx
  • app/web/src/App.tsx
  • app/web/src/__e2e__/oidc-login.spec.ts
  • app/web/src/api/hubAuth.ts
📝 Walkthrough

Walkthrough

Desktop and web authentication now centralize logout cleanup, align OIDC callback handling with the application base path, configure isolated E2E endpoints, portal the profile menu, and expand unit and end-to-end coverage.

Changes

Authentication and OIDC session flow

Layer / File(s) Summary
Runtime authentication and logout
app/desktop/src/App.tsx, app/web/src/App.tsx, app/desktop/src/__tests__/App.v4.test.tsx, app/web/src/App.test.tsx
Desktop and web logout handlers clear authentication state, query data, and workbench state before showing unauthenticated UI. Tests cover authenticated sessions and cleanup behavior.
Portaled profile menu
app/shared/src/workbench/floating/ProfilePopover.tsx, app/shared/src/workbench/floating/ProfilePopover.test.tsx
ProfilePopover renders through a shared dialog shell and portals browser output to document.body. Tests verify placement and logout callbacks.
Callback and E2E server configuration
app/desktop/playwright.config.ts, app/web/playwright.config.ts, app/web/src/api/hubAuth.ts
Playwright environments preserve process variables while setting isolated Hub endpoints. OIDC callback defaults now include the application base path.
OIDC flow and logout coverage
app/desktop/src/__e2e__/oidc-login.spec.ts, app/web/src/__e2e__/oidc-login.spec.ts
OIDC mocks use awaited wildcard routes and configurable callback paths. Tests verify token persistence, callback errors, profile interaction, logout requests, and cleared storage.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AgentHubWorkbench
  participant useAuth
  participant queryClient
  participant SessionStorage
  User->>AgentHubWorkbench: Select logout
  AgentHubWorkbench->>useAuth: logout()
  AgentHubWorkbench->>queryClient: Clear private queries
  AgentHubWorkbench->>SessionStorage: Remove session and workbench state
  AgentHubWorkbench-->>User: Show login UI
Loading

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: completing web and desktop logout authentication behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/frontend-logout-contract

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/desktop/src/App.tsx (1)

1-1: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Run local logout cleanup even if logout() rejects.

Both handleLogout implementations await the shared auth singleton's logout() before running local cleanup. The upstream logout() contract only swallows errors from its network POST call; clearStoredHubAccessToken()/clearStoredHubRefreshToken() remain unguarded. If either rejects, all local cleanup is skipped and the user is stranded on the authenticated view with no path back to login without a full reload.

  • app/desktop/src/App.tsx#L91-97: wrap queryClient.clear(), the WORKBENCH_DATA_MODE_STORAGE_KEY removal, and setEntryMode('entry') in a finally block around await logout().
  • app/web/src/App.tsx#L184-194: wrap sessionQueryClient.clear(), the local selection/error state resets, and setShowAuthModal(true) in a finally block around await logout().
🔧 Proposed fix for app/desktop/src/App.tsx
   const handleLogout = useCallback(async (): Promise<void> => {
-    await logout();
-    queryClient.clear();
-    window.localStorage.removeItem(WORKBENCH_DATA_MODE_STORAGE_KEY);
-    setEntryMode('entry');
+    try {
+      await logout();
+    } finally {
+      queryClient.clear();
+      window.localStorage.removeItem(WORKBENCH_DATA_MODE_STORAGE_KEY);
+      setEntryMode('entry');
+    }
   }, [logout, queryClient]);
🔧 Proposed fix for app/web/src/App.tsx
   const handleLogout = useCallback(async (): Promise<void> => {
-    await logout();
-    sessionQueryClient.clear();
-    setSelectedConversationId(undefined);
-    setSelectedProjectId(undefined);
-    setAgentActionError(undefined);
-    setSavingAgentId(undefined);
-    setDeletingAgentId(undefined);
-    setShowAuthModal(true);
+    try {
+      await logout();
+    } finally {
+      sessionQueryClient.clear();
+      setSelectedConversationId(undefined);
+      setSelectedProjectId(undefined);
+      setAgentActionError(undefined);
+      setSavingAgentId(undefined);
+      setDeletingAgentId(undefined);
+      setShowAuthModal(true);
+    }
   }, [logout, sessionQueryClient, setShowAuthModal]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/desktop/src/App.tsx` at line 1, Update both handleLogout implementations
in the desktop and web App components so await logout() is wrapped in a
try/finally, with all existing local cleanup moved into finally: query-client
clearing, storage removal or local state resets, and restoring the entry/auth
modal state. Preserve the shared logout call while ensuring cleanup runs even
when it rejects.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/desktop/src/__e2e__/oidc-login.spec.ts`:
- Around line 327-330: In the OIDC login test, update the profile-menu button
locator and logout button locator to accept only the expected translated text
produced by the mocked fixture, removing raw i18n keys such as user.fallbackName
and user.logout from both regular expressions.

In `@app/desktop/src/App.tsx`:
- Around line 91-97: Update handleLogout in App.tsx so cleanup always runs even
when logout() rejects: execute queryClient.clear(), remove
WORKBENCH_DATA_MODE_STORAGE_KEY, and setEntryMode('entry') in a finally path,
while preserving the logout attempt and ensuring the rejection does not prevent
the login gate transition.

In `@app/shared/src/workbench/floating/ProfilePopover.tsx`:
- Around line 167-184: Add Playwright coverage for opening ProfilePopover from a
scrollable, overflow-hidden conversation sidebar, asserting the portal-rendered
menu is visible without clipping and receives pointer events. Run the behavioral
and Visual QA checks at the 1440x810 gate viewport in both light and dark modes,
and include evidence for each mode.

In `@app/web/src/__e2e__/oidc-login.spec.ts`:
- Line 534: Update the logout-button assertions in the OIDC login E2E specs,
including the test around the visible getByRole call and its desktop
counterpart, to match only the supported translated labels and exclude the raw
user.logout i18n key. Preserve the existing localized-language coverage without
allowing unresolved translation keys to satisfy the assertion.

In `@app/web/src/App.tsx`:
- Around line 184-194: Update handleLogout in App.tsx so
sessionQueryClient.clear(), local state resets, and setShowAuthModal(true)
execute even when logout() rejects by placing cleanup in a finally path;
preserve the existing logout attempt and callback dependencies.

---

Outside diff comments:
In `@app/desktop/src/App.tsx`:
- Line 1: Update both handleLogout implementations in the desktop and web App
components so await logout() is wrapped in a try/finally, with all existing
local cleanup moved into finally: query-client clearing, storage removal or
local state resets, and restoring the entry/auth modal state. Preserve the
shared logout call while ensuring cleanup runs even when it rejects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a7cd229f-23d8-466f-a976-0485815762a4

📥 Commits

Reviewing files that changed from the base of the PR and between 1d2feff and 8740663.

📒 Files selected for processing (11)
  • app/desktop/playwright.config.ts
  • app/desktop/src/App.tsx
  • app/desktop/src/__e2e__/oidc-login.spec.ts
  • app/desktop/src/__tests__/App.v4.test.tsx
  • app/shared/src/workbench/floating/ProfilePopover.test.tsx
  • app/shared/src/workbench/floating/ProfilePopover.tsx
  • app/web/playwright.config.ts
  • app/web/src/App.test.tsx
  • app/web/src/App.tsx
  • app/web/src/__e2e__/oidc-login.spec.ts
  • app/web/src/api/hubAuth.ts

Comment on lines +327 to +330
await page.getByRole('button', { name: /^(Test User|testuser|User|user\.fallbackName)$/ }).click();
await expect(page.getByRole('dialog').first()).toBeVisible();
await page.screenshot({ path: testInfo.outputPath('profile-menu-open.png'), fullPage: true });
await page.getByRole('button', { name: /^(退出登录|Log out|user\.logout)$/ }).click();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Permissive button-name regex accepts raw i18n keys, weakening test protection.

The regex /^(Test User|testuser|User|user\.fallbackName)$/ and /^(退出登录|Log out|user\.logout)$/ both include the raw i18n key text (user.fallbackName, user.logout) as an accepted match alongside the real translated strings. If a translation fails to resolve and the UI falls back to displaying the raw key, these assertions still pass. The test then stops verifying which text actually renders, so it can no longer catch a missing-translation regression. Narrow each assertion to the specific text the mocked fixture (display_name: 'Test User') is expected to produce.

As per coding guidelines, **/*.{test,spec}.{ts,tsx,js,jsx}: "禁止无保护力测试:不要复制实现 switch、测试常量字符串、硬断错误文案,或 mock 被测函数本身;mock 应模拟外部系统。"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/desktop/src/__e2e__/oidc-login.spec.ts` around lines 327 - 330, In the
OIDC login test, update the profile-menu button locator and logout button
locator to accept only the expected translated text produced by the mocked
fixture, removing raw i18n keys such as user.fallbackName and user.logout from
both regular expressions.

Source: Coding guidelines

Comment thread app/desktop/src/App.tsx
Comment on lines +91 to +97
const handleLogout = useCallback(async (): Promise<void> => {
await logout();
queryClient.clear();
window.localStorage.removeItem(WORKBENCH_DATA_MODE_STORAGE_KEY);
setEntryMode('entry');
}, [logout, queryClient]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard cleanup against logout() rejecting.

handleLogout awaits logout() before running cleanup. If logout() rejects, queryClient.clear(), the storage-key removal, and setEntryMode('entry') never run. The user stays on the workbench view with a torn-down session and no way back to the login gate without a full reload.

(See consolidated comment for the shared fix with app/web/src/App.tsx.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/desktop/src/App.tsx` around lines 91 - 97, Update handleLogout in App.tsx
so cleanup always runs even when logout() rejects: execute queryClient.clear(),
remove WORKBENCH_DATA_MODE_STORAGE_KEY, and setEntryMode('entry') in a finally
path, while preserving the logout attempt and ensuring the rejection does not
prevent the login gate transition.

Comment thread app/shared/src/workbench/floating/ProfilePopover.tsx
await page.getByRole('button', { name: 'Web User' }).click();
await expect(page.getByRole('dialog').first()).toBeVisible();
await page.screenshot({ path: testInfo.outputPath('profile-menu-open.png'), fullPage: true });
await page.getByRole('button', { name: /^(退出登录|Log out|user\.logout)$/ }).click();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Permissive logout-button regex accepts the raw i18n key.

/^(退出登录|Log out|user\.logout)$/ accepts the literal i18n key user.logout as a valid match, alongside the two translated strings. If translation resolution breaks and the UI falls back to the raw key, this assertion still passes, so it stops verifying which text renders. The same pattern occurs in app/desktop/src/__e2e__/oidc-login.spec.ts at line 330.

As per coding guidelines, **/*.{test,spec}.{ts,tsx,js,jsx}: "禁止无保护力测试:不要复制实现 switch、测试常量字符串、硬断错误文案,或 mock 被测函数本身;mock 应模拟外部系统。"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/web/src/__e2e__/oidc-login.spec.ts` at line 534, Update the logout-button
assertions in the OIDC login E2E specs, including the test around the visible
getByRole call and its desktop counterpart, to match only the supported
translated labels and exclude the raw user.logout i18n key. Preserve the
existing localized-language coverage without allowing unresolved translation
keys to satisfy the assertion.

Source: Coding guidelines

Comment thread app/web/src/App.tsx
Comment on lines +184 to +194
const handleLogout = useCallback(async (): Promise<void> => {
await logout();
sessionQueryClient.clear();
setSelectedConversationId(undefined);
setSelectedProjectId(undefined);
setAgentActionError(undefined);
setSavingAgentId(undefined);
setDeletingAgentId(undefined);
setShowAuthModal(true);
}, [logout, sessionQueryClient, setShowAuthModal]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard cleanup against logout() rejecting.

handleLogout awaits logout() before clearing sessionQueryClient and resetting local state. If logout() rejects, none of the cleanup runs, including setShowAuthModal(true). The user stays on the previous authenticated workbench view with no path back to the login modal.

(See consolidated comment for the shared fix with app/desktop/src/App.tsx.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/web/src/App.tsx` around lines 184 - 194, Update handleLogout in App.tsx
so sessionQueryClient.clear(), local state resets, and setShowAuthModal(true)
execute even when logout() rejects by placing cleanup in a finally path;
preserve the existing logout attempt and callback dependencies.

@DeliciousBuding
DeliciousBuding force-pushed the fix/frontend-logout-contract branch from 8740663 to d5c79fb Compare August 1, 2026 17:32
@DeliciousBuding
DeliciousBuding merged commit bbb0cdd into master Aug 1, 2026
21 checks passed
@DeliciousBuding
DeliciousBuding deleted the fix/frontend-logout-contract branch August 1, 2026 17:56
DeliciousBuding added a commit that referenced this pull request Aug 11, 2026
Co-authored-by: Codex <codex@vectorcontrol.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants