Skip to content

fix(profiles): use unified account selection in terminal rate limit handler#1848

Open
VDT-91 wants to merge 5 commits intoAndyMik90:developfrom
VDT-91:terminal-unified-swap
Open

fix(profiles): use unified account selection in terminal rate limit handler#1848
VDT-91 wants to merge 5 commits intoAndyMik90:developfrom
VDT-91:terminal-unified-swap

Conversation

@VDT-91
Copy link
Collaborator

@VDT-91 VDT-91 commented Feb 15, 2026

Problem

When all OAuth profiles are rate-limited, terminals running Claude Code cannot automatically fall back to API profiles (GLM, etc.). Users hit a hard stop even though they have valid API credentials configured.

Solution

Add unified account selection to the terminal's rate limit handler and proactive swap logic:

  • Reactive swap: When rate limit is detected, handleRateLimit() now calls getBestAvailableUnifiedAccount() which considers BOTH OAuth and API profiles. If an API profile is available, it's suggested (and auto-switched if enabled).
  • Proactive swap: Before Claude invocation, ensureBestProfileActive() checks if a better profile (OAuth or API) is available and swaps globally.
  • Manual swap guard: Proactive swap is skipped when a user explicitly selects a profile, respecting their choice.

Changes

  • claude-integration-handler.ts: Added ensureBestProfileActive() helper, rewrote handleRateLimit() to use unified account selection
  • types.ts: Added suggestedAccountType field to RateLimitEvent IPC payload
  • Tests: 90 tests pass (82 terminal + 8 profile-scorer verification tests)
  • Refactored: Extracted sendRateLimitIpc() and switchToUnifiedAccount() helpers to eliminate duplication

Testing

  • Unit tests: 90/90 pass
  • TypeScript: Clean
  • Biome lint: Clean
  • Manual verification: Debug logs show [ClaudeIntegration] Auto-switching to account: GLM API (type: api) when OAuth is rate-limited

⚠️ Platform Note

This fix addresses the core logic for unified account selection. We believe this resolves the Windows OAuth/API rate limit swap issues, but we don't have 100% confirmation that it fixes all edge cases on Windows. The logic is platform-agnostic and uses the same cross-platform abstractions as the rest of the codebase.

Community testing on Windows would be appreciated to confirm the fix works end-to-end.

Related

This is the terminal-focused portion of the unified account selection work, building on the existing getBestAvailableUnifiedAccount() infrastructure in profile-scorer.ts and claude-profile-manager.ts.

Summary by CodeRabbit

  • New Features

    • Automatic fallback to API profiles when OAuth profiles are rate-limited
    • Proactive profile optimization before Claude operations (background profile swap when none explicitly selected)
    • Rate-limit events now include suggested account type and better auto-switch behavior
  • Tests

    • Added comprehensive tests covering rate-limit handling, unified account selection, proactive swaps, and manual vs. automatic selection paths

VDT-91 and others added 4 commits February 15, 2026 20:02
…andler

Replaces getBestAvailableProfile (OAuth-only) with
getBestAvailableUnifiedAccount (OAuth + API) so terminals can swap to
API profiles like GLM when OAuth is rate-limited.

- Add ensureBestProfileActive() helper for proactive swap before
  invoking Claude (awaited in async path, fire-and-forget in sync)
- Rewrite handleRateLimit() to use unified account selection
- API profile swaps use setActiveAPIProfile(), OAuth uses existing
  switchProfileCallback
- Add suggestedAccountType to RateLimitEvent type
- Add 6 new tests for unified swap behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-scorer

Adds 8 tests for getBestAvailableUnifiedAccount() covering:
- API profile selection when all OAuth profiles are rate-limited
- API profile selection when OAuth is at 100% capacity
- Priority order respect for manual user swaps
- Exclusion of specified accounts
- Null return when no profiles exist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a profileId is explicitly provided (e.g. via Rate Limit Modal or
manual switch), skip ensureBestProfileActive() to avoid silently
overriding the user's choice with an auto-selected "better" profile.

Adds test verifying getBestAvailableUnifiedAccount is not called when
profileId is explicitly provided to invokeClaudeAsync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rengthen tests

- Extract sendRateLimitIpc() to eliminate duplicated IPC payload construction
- Extract switchToUnifiedAccount() to deduplicate API-vs-OAuth dispatch logic
- Fix ID format mismatch: use toOAuthUnifiedId() so exclude filter works correctly
- Add oauthToken to test factory so OAuth profiles are properly authenticated
- Remove inconsistent vi.doMock in first handleRateLimit test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @VDT-91, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the terminal's Claude integration by introducing a robust unified account selection mechanism. It addresses the problem of users being unable to automatically switch to available API profiles when their OAuth profiles are rate-limited, leading to a smoother and more resilient experience. The changes include both reactive switching upon rate limit detection and proactive optimization before invocation, ensuring that users always utilize the best available credentials without manual intervention, unless explicitly overridden.

Highlights

  • Unified Account Selection for Rate Limits: Implemented unified account selection in the terminal's rate limit handler, allowing automatic fallback to API profiles (e.g., GLM) when all OAuth profiles are rate-limited. This resolves issues where users hit a hard stop despite having valid API credentials.
  • Proactive Profile Swapping: Introduced a proactive swap mechanism (ensureBestProfileActive) that checks for a better available profile (OAuth or API) before invoking Claude. This ensures the most optimal profile is active, but respects explicit user selections by skipping the swap if a profile is manually chosen.
  • Refactored Rate Limit Handling: Extracted common logic into new helper functions sendRateLimitIpc() and switchToUnifiedAccount() to centralize IPC payload construction and streamline the profile switching process for both API and OAuth accounts, reducing duplication.
  • Enhanced IPC Communication: Added a suggestedAccountType field to the RateLimitEvent IPC payload, providing more detailed information about the type of suggested profile (OAuth or API) to the frontend.
  • Comprehensive Testing: Added new unit tests for getBestAvailableUnifiedAccount to verify its core functionality in selecting API profiles when OAuth profiles are rate-limited or at capacity. Also added tests for the claude-integration-handler to cover the new unified rate limit handling and proactive swap logic, including the manual selection guard.
Changelog
  • apps/frontend/src/main/claude-profile/tests/unified-account-selection.test.ts
    • Added new test file to verify getBestAvailableUnifiedAccount functionality.
  • apps/frontend/src/main/terminal/tests/claude-integration-handler.test.ts
    • Mocked setActiveAPIProfile for testing API profile auto-switching.
    • Added new test suite for handleRateLimit covering unified account selection and IPC events.
    • Added tests for auto-switching to API and OAuth profiles based on availability.
    • Added tests for scenarios where auto-switching is disabled or no best account is found.
    • Added new test suite for proactive swap, ensuring it respects manual profile selections.
  • apps/frontend/src/main/terminal/claude-integration-handler.ts
    • Imported toOAuthUnifiedId utility.
    • Added sendRateLimitIpc helper function to centralize IPC event payload construction.
    • Added switchToUnifiedAccount helper function to abstract global profile switching for both API and OAuth types.
    • Added ensureBestProfileActive helper function for proactive profile swapping before Claude invocation.
    • Modified handleRateLimit to use unified account selection via getBestAvailableUnifiedAccount.
    • Updated handleRateLimit to use sendRateLimitIpc and switchToUnifiedAccount for reactive profile switching.
    • Integrated ensureBestProfileActive into invokeClaude and invokeClaudeAsync to perform proactive swaps, with a guard for explicit profile selections.
  • apps/frontend/src/main/terminal/types.ts
    • Added suggestedAccountType field to the RateLimitEvent interface.
Activity
  • No human activity (comments, reviews) was provided in the context for this pull request.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Integrates unified-account selection into rate-limit handling and Claude invocation flows, adds proactive profile-swap helpers, extends rate-limit IPC payload with account type, and introduces tests covering unified account selection and rate-limit auto-switch behavior.

Changes

Cohort / File(s) Summary
Unified Account Selection Tests
apps/frontend/src/main/claude-profile/__tests__/unified-account-selection.test.ts
New tests for getBestAvailableUnifiedAccount covering OAuth vs API fallbacks, capacity/exclusion, priority ordering, and explicit null results.
Rate-Limit Handler Tests
apps/frontend/src/main/terminal/__tests__/claude-integration-handler.test.ts
Adds tests asserting IPC payloads, setActiveAPIProfile invocation for API auto-switch, non-invocation for OAuth, autoSwitch toggle behavior, proactive-swap with explicit profileId, and fallback scenarios.
Integration Handler Implementation
apps/frontend/src/main/terminal/claude-integration-handler.ts
Adds sendRateLimitIpc, switchToUnifiedAccount, ensureBestProfileActive; wires getBestAvailableUnifiedAccount into handleRateLimit and invocation flows; persists API swaps via new setActiveAPIProfile; distinguishes API vs OAuth swap paths and error-handles proactively.
Event Type Extension
apps/frontend/src/main/terminal/types.ts
Adds optional `suggestedAccountType?: 'oauth'
Credential Utils Comments
apps/frontend/src/main/claude-profile/credential-utils.ts
Added CodeQL suppression comments in hashing helpers; no functional changes.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant Handler as IntegrationHandler
    participant Selector as AccountSelector
    participant ProfileMgr as ProfileManager
    participant IPC

    Client->>Handler: invokeClaude() or handleRateLimit()
    alt No explicit profileId provided
        Handler->>Handler: ensureBestProfileActive()
        Handler->>Selector: getBestAvailableUnifiedAccount(unifiedId)
        Selector-->>Handler: BestAccount (API or OAuth)
        alt Account is API-type
            Handler->>ProfileMgr: setActiveAPIProfile(id)
            ProfileMgr-->>Handler: persisted
        else Account is OAuth-type
            Handler->>Handler: switchProfileCallback(id)
            Handler-->>Handler: persisted
        end
    end
    Handler->>IPC: sendRateLimitIpc({suggestedProfileId, suggestedAccountType, autoSwitchEnabled})
    IPC-->>Client: RateLimitEvent
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related Issues

  • Issue #1798: Matches wiring unified-account selection into rate-limit and proactive-swap flows (getBestAvailableUnifiedAccount, ensureBestProfileActive, setActiveAPIProfile).

Possibly Related PRs

  • PR #1496: Overlaps on multi-profile swapping, rate-limit detection, and profile-selection logic consumed here.
  • PR #1794: Introduces unified-account utilities (getBestAvailableUnifiedAccount, unified IDs) that this PR consumes and tests.
  • PR #652: Changes around OAuth recognition/auth checks that affect profile-scorer behavior used by unified selection.

Suggested Labels

bug, area/frontend, size/M

Suggested Reviewers

  • AndyMik90

Poem

🐰 A hop, a hop, a clever tweak,
Profiles swap when limits peak.
API or OAuth, choose the best,
I nudge the switch and then I rest.
Hooray — the rabbit aced the test! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 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: implementing unified account selection in the terminal rate limit handler to enable fallback to API profiles.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into develop

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request introduces unified account selection for rate limit handling and proactive profile swapping in the terminal. This is a significant improvement, allowing fallback to API profiles when OAuth profiles are rate-limited. The changes are well-tested with new unit tests covering the unified account selection logic and proactive swap behavior. The code is generally clean and follows good practices, with clear separation of concerns into helper functions. I've identified a few areas for minor improvement related to error handling and logging consistency.

Comment on lines +93 to +97
// Mock setActiveAPIProfile for handleRateLimit API profile auto-switch
const mockSetActiveAPIProfile = vi.fn().mockResolvedValue({});
vi.mock('../../services/profile/profile-manager', () => ({
setActiveAPIProfile: mockSetActiveAPIProfile,
}));
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

It's good practice to group related vi.mock calls. Consider moving this mock into the profile-manager mock block if setActiveAPIProfile is part of that module, or create a separate mock file for profile-manager if it's a larger module with many exports.

Comment on lines +473 to +475
} catch (err) {
debugError('[ClaudeIntegration] Proactive swap check failed (continuing with current profile):', err);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The comment /* handled internally */ is a bit vague. While the error is caught, it's still good to know if ensureBestProfileActive failed. Consider logging the error here as well, perhaps with a debugError to provide more context during debugging, even if it's not critical enough to halt execution.

    if (!profileId) {
      ensureBestProfileActive().catch(err => debugError('[ClaudeIntegration] Proactive swap failed in invokeClaude (sync):', err));
    }

Comment on lines +528 to +531
console.warn('[ClaudeIntegration] API profile auto-switch completed:', bestAccount.name);
}).catch(err => {
console.error('[ClaudeIntegration] API profile auto-switch failed:', err);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The error message for API profile auto-switch failure is generic. It would be more helpful to include bestAccount.name and bestAccount.id in the error log to quickly identify which API profile failed to activate.

        }).catch(err => {
          console.error('[ClaudeIntegration] API profile auto-switch failed for', bestAccount.name, '(', bestAccount.id, '):', err);
        });

Comment on lines +536 to +539
}).catch(err => {
console.error('[ClaudeIntegration] OAuth profile auto-switch failed:', err);
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Similar to the API profile, it would be beneficial to include bestAccount.name and bestAccount.id in the error log for OAuth profile auto-switch failures. This provides more specific debugging information.

        }).catch(err => {
          console.error('[ClaudeIntegration] OAuth profile auto-switch failed for', bestAccount.name, '(', bestAccount.id, '):', err);
        });

Comment on lines +1179 to +1182
// Proactive swap only when no explicit profile was requested (i.e. not a manual switch).
// When profileId is set, the user explicitly chose a profile — respect that choice.
if (!profileId) {
ensureBestProfileActive().catch(() => {/* handled internally */});
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The comment /* handled internally */ is a bit vague. While the error is caught, it's still good to know if ensureBestProfileActive failed. Consider logging the error here as well, perhaps with a debugError to provide more context during debugging, even if it's not critical enough to halt execution.

    if (!profileId) {
      ensureBestProfileActive().catch(err => debugError('[ClaudeIntegration] Proactive swap failed in invokeClaude (async):', err));
    }

@sentry
Copy link

sentry bot commented Feb 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In
`@apps/frontend/src/main/claude-profile/__tests__/unified-account-selection.test.ts`:
- Around line 12-33: The test factories createOAuthProfile and createAPIProfile
omit required timestamp fields and rely on `as` casts, so update these factories
to include sensible defaults for the missing fields (e.g., set
ClaudeProfile.createdAt to a new Date() and APIProfile.createdAt/updatedAt to
Date.now() or fixed numbers) while preserving the ability to override via the
overrides param; ensure the added fields match the types in ClaudeProfile and
APIProfile so getBestAvailableUnifiedAccount won't encounter undefined
timestamps at runtime.
- Around line 77-95: The test for getBestAvailableUnifiedAccount is too
weak—after calling getBestAvailableUnifiedAccount with healthyOAuth (id
'oauth-1') and glmProfile, add assertions that the chosen account is the OAuth
profile (e.g., assert result!.type is the OAuth type and/or result!.id ===
'oauth-1') so the test fails if scoring ever prefers the API profile; update the
test in unified-account-selection.test.ts to explicitly check the selected
account's type/identifier in addition to isAvailable.

In `@apps/frontend/src/main/terminal/claude-integration-handler.ts`:
- Around line 408-429: The bestAccount parameter in sendRateLimitIpc is declared
with type: string but is assigned to RateLimitEvent.suggestedAccountType which
expects 'oauth' | 'api', so tighten the parameter type to match RateLimitEvent
(e.g., change bestAccount?: { id: string; name: string; type: 'oauth' | 'api' }
| null or reference the RateLimitEvent type for suggestedAccountType) and
remove/avoid relying on the broad "as RateLimitEvent" mask; update any callers
to pass the narrowed union type if needed so the type system enforces only
'oauth'|'api' values for bestAccount.type instead of an arbitrary string.
- Around line 437-447: The function switchToUnifiedAccount currently accepts
account: { id: string; type: string } and treats any non-'api' value as OAuth;
change the account type to a discriminated union (e.g., { id: string; type:
'api' } | { id: string; type: 'oauth' }) or an enum so callers are constrained,
then replace the current if/else with explicit branches for 'api' (call
setActiveAPIProfile(account.id)) and 'oauth' (call
profileManager.setActiveProfile(account.id)), and add an exhaustive default that
throws an error for unknown types so unexpected values cannot silently be
treated as OAuth; update the signature of switchToUnifiedAccount and any callers
to match the tighter type.
- Around line 1179-1183: The fire-and-forget call to ensureBestProfileActive()
in the synchronous invokeClaude path causes a race because
profileManager.getActiveProfile() is read before the async swap can complete;
remove the proactive ensureBestProfileActive().catch(...) invocation from the
sync path (leave the awaited call in the async path that already uses await
ensureBestProfileActive()), and add a brief comment explaining that proactive
swaps are handled only in the async/await path so the sync path will not mutate
active profile mid-invocation; do not change invokeClaude's return type.
- Around line 515-545: Replace the duplicated inline API-switch logic in the API
branch (the dynamic import and setActiveAPIProfile calls inside the
bestAccount.type === 'api' block) with a call to the existing helper
switchToUnifiedAccount to reuse its encapsulated import/setActiveAPIProfile
behavior; call switchToUnifiedAccount(bestAccount) (or bestAccount.id if the
helper expects an id), chain the same then() and catch() handlers to preserve
the success/error console messages, and remove the now-redundant import →
setActiveAPIProfile block.

Comment on lines +12 to +33
function createOAuthProfile(overrides: Partial<ClaudeProfile> = {}): ClaudeProfile {
return {
id: 'profile-1',
name: 'Account 1',
isDefault: false,
configDir: '/tmp/config',
oauthToken: 'fake-token-for-testing',
usage: { weeklyUsagePercent: 50, sessionUsagePercent: 50 },
rateLimitEvents: [],
...overrides,
} as ClaudeProfile;
}

function createAPIProfile(overrides: Partial<APIProfile> = {}): APIProfile {
return {
id: 'glm-1',
name: 'GLM API',
baseUrl: 'https://api.z.ai/api/anthropic',
apiKey: 'sk-glm-key',
...overrides,
} as APIProfile;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Test factories omit required fields, relying on as casts to bypass type checking.

createOAuthProfile omits createdAt (required Date on ClaudeProfile) and createAPIProfile omits createdAt/updatedAt (required number on APIProfile). The as casts suppress compiler errors but if getBestAvailableUnifiedAccount ever accesses those fields, tests would pass at compile time yet produce undefined at runtime, masking real bugs.

Consider adding sensible defaults:

♻️ Proposed fix
 function createOAuthProfile(overrides: Partial<ClaudeProfile> = {}): ClaudeProfile {
   return {
     id: 'profile-1',
     name: 'Account 1',
     isDefault: false,
     configDir: '/tmp/config',
     oauthToken: 'fake-token-for-testing',
     usage: { weeklyUsagePercent: 50, sessionUsagePercent: 50 },
     rateLimitEvents: [],
+    createdAt: new Date(),
     ...overrides,
   } as ClaudeProfile;
 }

 function createAPIProfile(overrides: Partial<APIProfile> = {}): APIProfile {
   return {
     id: 'glm-1',
     name: 'GLM API',
     baseUrl: 'https://api.z.ai/api/anthropic',
     apiKey: 'sk-glm-key',
+    createdAt: Date.now(),
+    updatedAt: Date.now(),
     ...overrides,
   } as APIProfile;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function createOAuthProfile(overrides: Partial<ClaudeProfile> = {}): ClaudeProfile {
return {
id: 'profile-1',
name: 'Account 1',
isDefault: false,
configDir: '/tmp/config',
oauthToken: 'fake-token-for-testing',
usage: { weeklyUsagePercent: 50, sessionUsagePercent: 50 },
rateLimitEvents: [],
...overrides,
} as ClaudeProfile;
}
function createAPIProfile(overrides: Partial<APIProfile> = {}): APIProfile {
return {
id: 'glm-1',
name: 'GLM API',
baseUrl: 'https://api.z.ai/api/anthropic',
apiKey: 'sk-glm-key',
...overrides,
} as APIProfile;
}
function createOAuthProfile(overrides: Partial<ClaudeProfile> = {}): ClaudeProfile {
return {
id: 'profile-1',
name: 'Account 1',
isDefault: false,
configDir: '/tmp/config',
oauthToken: 'fake-token-for-testing',
usage: { weeklyUsagePercent: 50, sessionUsagePercent: 50 },
rateLimitEvents: [],
createdAt: new Date(),
...overrides,
} as ClaudeProfile;
}
function createAPIProfile(overrides: Partial<APIProfile> = {}): APIProfile {
return {
id: 'glm-1',
name: 'GLM API',
baseUrl: 'https://api.z.ai/api/anthropic',
apiKey: 'sk-glm-key',
createdAt: Date.now(),
updatedAt: Date.now(),
...overrides,
} as APIProfile;
}
🤖 Prompt for AI Agents
In
`@apps/frontend/src/main/claude-profile/__tests__/unified-account-selection.test.ts`
around lines 12 - 33, The test factories createOAuthProfile and createAPIProfile
omit required timestamp fields and rely on `as` casts, so update these factories
to include sensible defaults for the missing fields (e.g., set
ClaudeProfile.createdAt to a new Date() and APIProfile.createdAt/updatedAt to
Date.now() or fixed numbers) while preserving the ability to override via the
overrides param; ensure the added fields match the types in ClaudeProfile and
APIProfile so getBestAvailableUnifiedAccount won't encounter undefined
timestamps at runtime.

Comment on lines +77 to +95
it('returns available OAuth profile over API when OAuth is not rate-limited', () => {
const healthyOAuth = createOAuthProfile({
id: 'oauth-1',
name: 'Healthy Account',
});

const glmProfile = createAPIProfile();

const result = getBestAvailableUnifiedAccount(
[healthyOAuth],
[glmProfile],
defaultSettings,
{ excludeAccountId: 'some-other-id' }
);

expect(result).not.toBeNull();
// Both are available; result depends on priority order
expect(result!.isAvailable).toBe(true);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Test assertion is too weak to catch regressions.

This test only asserts isAvailable is true but doesn't assert which account type was selected. If scoring logic were to accidentally prefer API over healthy OAuth, this test would still pass. Consider asserting the expected type:

♻️ Proposed fix
     expect(result).not.toBeNull();
-    // Both are available; result depends on priority order
-    expect(result!.isAvailable).toBe(true);
+    // Healthy OAuth should be preferred over API by default priority
+    expect(result!.isAvailable).toBe(true);
+    expect(result!.type).toBe('oauth');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('returns available OAuth profile over API when OAuth is not rate-limited', () => {
const healthyOAuth = createOAuthProfile({
id: 'oauth-1',
name: 'Healthy Account',
});
const glmProfile = createAPIProfile();
const result = getBestAvailableUnifiedAccount(
[healthyOAuth],
[glmProfile],
defaultSettings,
{ excludeAccountId: 'some-other-id' }
);
expect(result).not.toBeNull();
// Both are available; result depends on priority order
expect(result!.isAvailable).toBe(true);
});
it('returns available OAuth profile over API when OAuth is not rate-limited', () => {
const healthyOAuth = createOAuthProfile({
id: 'oauth-1',
name: 'Healthy Account',
});
const glmProfile = createAPIProfile();
const result = getBestAvailableUnifiedAccount(
[healthyOAuth],
[glmProfile],
defaultSettings,
{ excludeAccountId: 'some-other-id' }
);
expect(result).not.toBeNull();
// Healthy OAuth should be preferred over API by default priority
expect(result!.isAvailable).toBe(true);
expect(result!.type).toBe('oauth');
});
🤖 Prompt for AI Agents
In
`@apps/frontend/src/main/claude-profile/__tests__/unified-account-selection.test.ts`
around lines 77 - 95, The test for getBestAvailableUnifiedAccount is too
weak—after calling getBestAvailableUnifiedAccount with healthyOAuth (id
'oauth-1') and glmProfile, add assertions that the chosen account is the OAuth
profile (e.g., assert result!.type is the OAuth type and/or result!.id ===
'oauth-1') so the test fails if scoring ever prefers the API profile; update the
test in unified-account-selection.test.ts to explicitly check the selected
account's type/identifier in addition to isAvailable.

Comment on lines +408 to +429
function sendRateLimitIpc(
getWindow: WindowGetter,
terminalId: string,
resetTime: string,
profileId: string,
autoSwitchEnabled: boolean,
bestAccount?: { id: string; name: string; type: string } | null
): void {
const win = getWindow();
if (win) {
win.webContents.send(IPC_CHANNELS.TERMINAL_RATE_LIMIT, {
terminalId,
resetTime,
detectedAt: new Date().toISOString(),
profileId,
suggestedProfileId: bestAccount?.id,
suggestedProfileName: bestAccount?.name,
suggestedAccountType: bestAccount?.type,
autoSwitchEnabled,
} as RateLimitEvent);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

bestAccount.type is typed as string but assigned to a 'oauth' | 'api' field.

The parameter bestAccount declares type: string, but it's assigned to RateLimitEvent.suggestedAccountType which is 'oauth' | 'api'. The as RateLimitEvent cast on line 427 silently masks this mismatch. If a caller ever passes an unexpected type string, it would flow through unchecked.

♻️ Proposed fix — tighten the parameter type
 function sendRateLimitIpc(
   getWindow: WindowGetter,
   terminalId: string,
   resetTime: string,
   profileId: string,
   autoSwitchEnabled: boolean,
-  bestAccount?: { id: string; name: string; type: string } | null
+  bestAccount?: { id: string; name: string; type: 'oauth' | 'api' } | null
 ): void {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function sendRateLimitIpc(
getWindow: WindowGetter,
terminalId: string,
resetTime: string,
profileId: string,
autoSwitchEnabled: boolean,
bestAccount?: { id: string; name: string; type: string } | null
): void {
const win = getWindow();
if (win) {
win.webContents.send(IPC_CHANNELS.TERMINAL_RATE_LIMIT, {
terminalId,
resetTime,
detectedAt: new Date().toISOString(),
profileId,
suggestedProfileId: bestAccount?.id,
suggestedProfileName: bestAccount?.name,
suggestedAccountType: bestAccount?.type,
autoSwitchEnabled,
} as RateLimitEvent);
}
}
function sendRateLimitIpc(
getWindow: WindowGetter,
terminalId: string,
resetTime: string,
profileId: string,
autoSwitchEnabled: boolean,
bestAccount?: { id: string; name: string; type: 'oauth' | 'api' } | null
): void {
const win = getWindow();
if (win) {
win.webContents.send(IPC_CHANNELS.TERMINAL_RATE_LIMIT, {
terminalId,
resetTime,
detectedAt: new Date().toISOString(),
profileId,
suggestedProfileId: bestAccount?.id,
suggestedProfileName: bestAccount?.name,
suggestedAccountType: bestAccount?.type,
autoSwitchEnabled,
} as RateLimitEvent);
}
}
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/terminal/claude-integration-handler.ts` around lines
408 - 429, The bestAccount parameter in sendRateLimitIpc is declared with type:
string but is assigned to RateLimitEvent.suggestedAccountType which expects
'oauth' | 'api', so tighten the parameter type to match RateLimitEvent (e.g.,
change bestAccount?: { id: string; name: string; type: 'oauth' | 'api' } | null
or reference the RateLimitEvent type for suggestedAccountType) and remove/avoid
relying on the broad "as RateLimitEvent" mask; update any callers to pass the
narrowed union type if needed so the type system enforces only 'oauth'|'api'
values for bestAccount.type instead of an arbitrary string.

Comment on lines +437 to +447
async function switchToUnifiedAccount(
profileManager: ReturnType<typeof getClaudeProfileManager>,
account: { id: string; type: string }
): Promise<void> {
if (account.type === 'api') {
const { setActiveAPIProfile } = await import('../services/profile/profile-manager');
await setActiveAPIProfile(account.id);
} else {
profileManager.setActiveProfile(account.id);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Same type: string looseness — and the else branch silently treats any non-'api' value as OAuth.

If account.type is ever anything other than 'api', it falls into the else branch and calls profileManager.setActiveProfile. Tightening the type and adding an exhaustive check would make this safer.

♻️ Proposed fix
 async function switchToUnifiedAccount(
   profileManager: ReturnType<typeof getClaudeProfileManager>,
-  account: { id: string; type: string }
+  account: { id: string; type: 'oauth' | 'api' }
 ): Promise<void> {
   if (account.type === 'api') {
     const { setActiveAPIProfile } = await import('../services/profile/profile-manager');
     await setActiveAPIProfile(account.id);
   } else {
     profileManager.setActiveProfile(account.id);
   }
 }
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/terminal/claude-integration-handler.ts` around lines
437 - 447, The function switchToUnifiedAccount currently accepts account: { id:
string; type: string } and treats any non-'api' value as OAuth; change the
account type to a discriminated union (e.g., { id: string; type: 'api' } | { id:
string; type: 'oauth' }) or an enum so callers are constrained, then replace the
current if/else with explicit branches for 'api' (call
setActiveAPIProfile(account.id)) and 'oauth' (call
profileManager.setActiveProfile(account.id)), and add an exhaustive default that
throws an error for unknown types so unexpected values cannot silently be
treated as OAuth; update the signature of switchToUnifiedAccount and any callers
to match the tighter type.

Comment on lines +515 to +545
// Use unified account selection (OAuth + API) instead of OAuth-only
// Pass a properly-prefixed unified ID so the exclude filter works correctly
profileManager.getBestAvailableUnifiedAccount(toOAuthUnifiedId(currentProfileId)).then(bestAccount => {
sendRateLimitIpc(getWindow, terminal.id, resetTime, currentProfileId, autoSwitchSettings.autoSwitchOnRateLimit, bestAccount);

if (autoSwitchSettings.enabled && autoSwitchSettings.autoSwitchOnRateLimit && bestAccount) {
console.warn('[ClaudeIntegration] Auto-switching to account:', bestAccount.name, '(type:', bestAccount.type, ')');

if (bestAccount.type === 'api') {
// API profile: persist globally via setActiveAPIProfile
import('../services/profile/profile-manager').then(({ setActiveAPIProfile }) => {
return setActiveAPIProfile(bestAccount.id);
}).then(() => {
console.warn('[ClaudeIntegration] API profile auto-switch completed:', bestAccount.name);
}).catch(err => {
console.error('[ClaudeIntegration] API profile auto-switch failed:', err);
});
} else {
// OAuth profile: use existing callback (restarts terminal, not just setActiveProfile)
switchProfileCallback(terminal.id, bestAccount.id).then(() => {
console.warn('[ClaudeIntegration] OAuth profile auto-switch completed');
}).catch(err => {
console.error('[ClaudeIntegration] OAuth profile auto-switch failed:', err);
});
}
}
}).catch(err => {
// Fallback: send IPC event without suggested profile
console.error('[ClaudeIntegration] Unified account selection failed:', err);
sendRateLimitIpc(getWindow, terminal.id, resetTime, currentProfileId, autoSwitchSettings.autoSwitchOnRateLimit);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Duplicated API-switch logic between handleRateLimit and switchToUnifiedAccount.

Lines 523–531 inline the same import → setActiveAPIProfile pattern already encapsulated in switchToUnifiedAccount (lines 437–447). The OAuth path intentionally differs (uses switchProfileCallback to restart the terminal), but the API path could delegate to the shared helper:

♻️ Reduce duplication for the API branch
       if (bestAccount.type === 'api') {
-        // API profile: persist globally via setActiveAPIProfile
-        import('../services/profile/profile-manager').then(({ setActiveAPIProfile }) => {
-          return setActiveAPIProfile(bestAccount.id);
-        }).then(() => {
+        switchToUnifiedAccount(profileManager, bestAccount).then(() => {
           console.warn('[ClaudeIntegration] API profile auto-switch completed:', bestAccount.name);
         }).catch(err => {
           console.error('[ClaudeIntegration] API profile auto-switch failed:', err);
         });
       } else {
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/terminal/claude-integration-handler.ts` around lines
515 - 545, Replace the duplicated inline API-switch logic in the API branch (the
dynamic import and setActiveAPIProfile calls inside the bestAccount.type ===
'api' block) with a call to the existing helper switchToUnifiedAccount to reuse
its encapsulated import/setActiveAPIProfile behavior; call
switchToUnifiedAccount(bestAccount) (or bestAccount.id if the helper expects an
id), chain the same then() and catch() handlers to preserve the success/error
console messages, and remove the now-redundant import → setActiveAPIProfile
block.

Comment on lines +1179 to +1183
// Proactive swap only when no explicit profile was requested (i.e. not a manual switch).
// When profileId is set, the user explicitly chose a profile — respect that choice.
if (!profileId) {
ensureBestProfileActive().catch(() => {/* handled internally */});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fire-and-forget ensureBestProfileActive() creates a race — proactive swap cannot take effect before invocation.

ensureBestProfileActive() is fully async (awaits getBestAvailableUnifiedAccount and switchToUnifiedAccount), but here it's launched without await. The synchronous code on lines 1188–1191 reads profileManager.getActiveProfile() immediately, before the swap can complete. The proactive swap therefore has no effect on this invocation — the old (potentially rate-limited) profile is used, and the swap silently completes in the background (affecting only the next invocation).

In contrast, the async path at line 1380 correctly awaits the swap.

Options:

  1. Convert invokeClaude to return a Promise (may require upstream changes).
  2. Remove the fire-and-forget call from the sync path entirely and rely on the async path and reactive handling (clearer contract, no hidden background mutation).
  3. Document this as intentional "best-effort for next invocation" behavior (but this contradicts the PR description).
Option 2: Remove fire-and-forget to avoid silent no-op
-    // Proactive swap only when no explicit profile was requested (i.e. not a manual switch).
-    // When profileId is set, the user explicitly chose a profile — respect that choice.
-    if (!profileId) {
-      ensureBestProfileActive().catch(() => {/* handled internally */});
-    }
-
     const startTime = Date.now();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Proactive swap only when no explicit profile was requested (i.e. not a manual switch).
// When profileId is set, the user explicitly chose a profile — respect that choice.
if (!profileId) {
ensureBestProfileActive().catch(() => {/* handled internally */});
}
const startTime = Date.now();
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/terminal/claude-integration-handler.ts` around lines
1179 - 1183, The fire-and-forget call to ensureBestProfileActive() in the
synchronous invokeClaude path causes a race because
profileManager.getActiveProfile() is read before the async swap can complete;
remove the proactive ensureBestProfileActive().catch(...) invocation from the
sync path (leave the awaited call in the async path that already uses await
ensureBestProfileActive()), and add a brief comment explaining that proactive
swaps are handled only in the async/await path so the sync path will not mutate
active profile mid-invocation; do not change invokeClaude's return type.

Add CodeQL suppression comments for js/weak-crypto-hashing rule on two lines
that hash filesystem paths for credential storage identifiers (not passwords).
These are legitimate uses of SHA256 for creating deterministic identifiers
from config directory paths.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* @returns The 8-character hex hash suffix
*/
export function calculateConfigDirHash(configDir: string): string {
// CodeQL[js/weak-crypto-hashing] suppress False positive: hashing filesystem path for identifier, not password

Check failure

Code scanning / CodeQL

Use of password hash with insufficient computational effort High

Password from
an access to rateLimitedOAuth
is hashed insecurely.
Password from
an access to healthyOAuth
is hashed insecurely.
Password from
an access to atCapacityOAuth
is hashed insecurely.
Password from
an access to oauth
is hashed insecurely.
Password from
an access to healthyOAuth
is hashed insecurely.
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.

1 participant