Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/**
* Tests for getBestAvailableUnifiedAccount (profile-scorer.ts)
*
* Verifies the core scenario: when all OAuth profiles are rate-limited,
* API profiles (GLM, etc.) are correctly selected as alternatives.
* This is the foundation of the terminal unified swap fix.
*/
import { describe, it, expect } from 'vitest';
import { getBestAvailableUnifiedAccount } from '../profile-scorer';
import type { ClaudeProfile, ClaudeAutoSwitchSettings, APIProfile } from '../../../shared/types';

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;
}
Comment on lines +12 to +33
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.


const defaultSettings = {
enabled: true,
autoSwitchOnRateLimit: true,
proactiveSwapEnabled: true,
usageCheckInterval: 30000,
sessionThreshold: 95,
weeklyThreshold: 99,
autoSwitchOnAuthFailure: false,
} as ClaudeAutoSwitchSettings;

describe('getBestAvailableUnifiedAccount', () => {
it('returns API profile when all OAuth profiles are rate-limited', () => {
const rateLimitedOAuth = createOAuthProfile({
id: 'oauth-1',
name: 'Rate Limited Account',
rateLimitEvents: [
{
type: 'session',
hitAt: new Date(),
resetAt: new Date(Date.now() + 3600000),
resetTimeString: 'Feb 19 at 11am',
}
],
});

const glmProfile = createAPIProfile({
id: 'glm-1',
name: 'GLM API',
});

const result = getBestAvailableUnifiedAccount(
[rateLimitedOAuth],
[glmProfile],
defaultSettings,
{ excludeAccountId: 'oauth-oauth-1' }
);

expect(result).not.toBeNull();
expect(result!.type).toBe('api');
expect(result!.name).toBe('GLM API');
});

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);
});
Comment on lines +77 to +95
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.


it('returns API profile when OAuth is at capacity (100% weekly usage)', () => {
const atCapacityOAuth = createOAuthProfile({
id: 'oauth-1',
name: 'At Capacity Account',
usage: {
weeklyUsagePercent: 100,
sessionUsagePercent: 100,
sessionResetTime: '11:59pm',
weeklyResetTime: 'Feb 20',
lastUpdated: new Date(),
},
});

const glmProfile = createAPIProfile();

const result = getBestAvailableUnifiedAccount(
[atCapacityOAuth],
[glmProfile],
defaultSettings,
{ excludeAccountId: 'oauth-oauth-1' }
);

expect(result).not.toBeNull();
expect(result!.type).toBe('api');
expect(result!.isAvailable).toBe(true);
});

it('returns null when no profiles exist', () => {
const result = getBestAvailableUnifiedAccount(
[],
[],
defaultSettings,
);

expect(result).toBeNull();
});

it('returns API profile even when it is the only option', () => {
const glmProfile = createAPIProfile({
id: 'glm-1',
name: 'GLM API',
apiKey: 'sk-valid-key',
});

const result = getBestAvailableUnifiedAccount(
[],
[glmProfile],
defaultSettings,
);

expect(result).not.toBeNull();
expect(result!.type).toBe('api');
expect(result!.hasUnlimitedUsage).toBe(true);
});

it('respects priority order when multiple accounts are available', () => {
const oauth = createOAuthProfile({ id: 'oauth-1', name: 'OAuth' });
const glm = createAPIProfile({ id: 'glm-1', name: 'GLM' });

// GLM first in priority
const result = getBestAvailableUnifiedAccount(
[oauth],
[glm],
defaultSettings,
{ priorityOrder: ['api-glm-1', 'oauth-oauth-1'] }
);

expect(result).not.toBeNull();
expect(result!.name).toBe('GLM');
expect(result!.type).toBe('api');
});

it('excludes the specified account from selection', () => {
const glm = createAPIProfile({ id: 'glm-1', name: 'GLM' });

const result = getBestAvailableUnifiedAccount(
[],
[glm],
defaultSettings,
{ excludeAccountId: 'api-glm-1' }
);

expect(result).toBeNull();
});

it('allows manual selection of API profile via priority order even when OAuth is healthy', () => {
const healthyOAuth = createOAuthProfile({
id: 'oauth-1',
name: 'Healthy OAuth',
});

const glm = createAPIProfile({ id: 'glm-1', name: 'GLM API' });

// User puts GLM first in priority → should get GLM
const result = getBestAvailableUnifiedAccount(
[healthyOAuth],
[glm],
defaultSettings,
{ priorityOrder: ['api-glm-1', 'oauth-oauth-1'] }
);

expect(result).not.toBeNull();
expect(result!.type).toBe('api');
expect(result!.name).toBe('GLM API');
});
});
2 changes: 2 additions & 0 deletions apps/frontend/src/main/claude-profile/credential-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
* @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.
return createHash('sha256').update(configDir).digest('hex').slice(0, 8);
}

Expand Down Expand Up @@ -730,7 +731,8 @@
if (!configDir) {
return 'claude-code';
}
// For custom config dirs, create a hashed attribute to avoid conflicts

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.
// CodeQL[js/weak-crypto-hashing] suppress False positive: hashing filesystem path for identifier, not password
const hash = createHash('sha256').update(configDir).digest('hex').slice(0, 8);
return `claude-code-${hash}`;
}
Expand Down
Loading
Loading