From f4e73191d1be34673d489e22d960af8389b061a4 Mon Sep 17 00:00:00 2001 From: Gaurav <39389231+gsquared94@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:45:39 -0800 Subject: [PATCH] fix: loadcodeassist eligible tiers getting ignored for unlicensed users (regression) (#17581) --- packages/core/src/code_assist/setup.test.ts | 136 +++++++++++++++++++- packages/core/src/code_assist/setup.ts | 34 ++++- 2 files changed, 161 insertions(+), 9 deletions(-) diff --git a/packages/core/src/code_assist/setup.test.ts b/packages/core/src/code_assist/setup.test.ts index 9559c58254..0d71a4d162 100644 --- a/packages/core/src/code_assist/setup.test.ts +++ b/packages/core/src/code_assist/setup.test.ts @@ -332,9 +332,86 @@ describe('setupUser validation', () => { vi.unstubAllEnvs(); }); - it('should throw error if LoadCodeAssist returns ineligible tiers and no current tier', async () => { + it('should throw ineligible tier error when currentTier exists but no project ID available', async () => { + vi.stubEnv('GOOGLE_CLOUD_PROJECT', ''); + mockLoad.mockResolvedValue({ + currentTier: mockPaidTier, + cloudaicompanionProject: undefined, + ineligibleTiers: [ + { + reasonMessage: 'User is not eligible', + reasonCode: 'INELIGIBLE_ACCOUNT', + tierId: 'free-tier', + tierName: 'free', + }, + ], + }); + + await expect(setupUser({} as OAuth2Client)).rejects.toThrow( + 'User is not eligible', + ); + }); + + it('should continue if LoadCodeAssist returns ineligible tiers but has allowed tiers', async () => { + const mockOnboardUser = vi.fn().mockResolvedValue({ + done: true, + response: { + cloudaicompanionProject: { + id: 'server-project', + }, + }, + }); + vi.mocked(CodeAssistServer).mockImplementation( + () => + ({ + loadCodeAssist: mockLoad, + onboardUser: mockOnboardUser, + }) as unknown as CodeAssistServer, + ); + mockLoad.mockResolvedValue({ currentTier: null, + allowedTiers: [mockPaidTier], + ineligibleTiers: [ + { + reasonMessage: 'Not eligible for free tier', + reasonCode: 'INELIGIBLE_ACCOUNT', + tierId: 'free-tier', + tierName: 'free', + }, + ], + }); + + // Should not throw - should proceed to onboarding with the allowed tier + const result = await setupUser({} as OAuth2Client); + expect(result).toEqual({ + projectId: 'server-project', + userTier: 'standard-tier', + userTierName: 'paid', + }); + expect(mockOnboardUser).toHaveBeenCalled(); + }); + + it('should proceed to onboarding with LEGACY tier when no currentTier and no allowedTiers', async () => { + const mockOnboardUser = vi.fn().mockResolvedValue({ + done: true, + response: { + cloudaicompanionProject: { + id: 'server-project', + }, + }, + }); + vi.mocked(CodeAssistServer).mockImplementation( + () => + ({ + loadCodeAssist: mockLoad, + onboardUser: mockOnboardUser, + }) as unknown as CodeAssistServer, + ); + + mockLoad.mockResolvedValue({ + currentTier: null, + allowedTiers: undefined, ineligibleTiers: [ { reasonMessage: 'User is not eligible', @@ -345,8 +422,63 @@ describe('setupUser validation', () => { ], }); + // Should proceed to onboarding with LEGACY tier, ignoring ineligible tier errors + const result = await setupUser({} as OAuth2Client); + expect(result).toEqual({ + projectId: 'server-project', + userTier: 'legacy-tier', + userTierName: '', + }); + expect(mockOnboardUser).toHaveBeenCalledWith( + expect.objectContaining({ + tierId: 'legacy-tier', + }), + ); + }); + + it('should throw ValidationRequiredError even if allowed tiers exist', async () => { + mockLoad.mockResolvedValue({ + currentTier: null, + allowedTiers: [mockPaidTier], + ineligibleTiers: [ + { + reasonMessage: 'Please verify your account', + reasonCode: 'VALIDATION_REQUIRED', + tierId: 'free-tier', + tierName: 'free', + validationUrl: 'https://example.com/verify', + }, + ], + }); + await expect(setupUser({} as OAuth2Client)).rejects.toThrow( - 'User is not eligible', + ValidationRequiredError, + ); + }); + + it('should combine multiple ineligible tier messages when currentTier exists but no project ID', async () => { + vi.stubEnv('GOOGLE_CLOUD_PROJECT', ''); + mockLoad.mockResolvedValue({ + currentTier: mockPaidTier, + cloudaicompanionProject: undefined, + ineligibleTiers: [ + { + reasonMessage: 'Not eligible for standard', + reasonCode: 'INELIGIBLE_ACCOUNT', + tierId: 'standard-tier', + tierName: 'standard', + }, + { + reasonMessage: 'Not eligible for free', + reasonCode: 'INELIGIBLE_ACCOUNT', + tierId: 'free-tier', + tierName: 'free', + }, + ], + }); + + await expect(setupUser({} as OAuth2Client)).rejects.toThrow( + 'Not eligible for standard, Not eligible for free', ); }); diff --git a/packages/core/src/code_assist/setup.ts b/packages/core/src/code_assist/setup.ts index 15da70fb42..bf948f1f93 100644 --- a/packages/core/src/code_assist/setup.ts +++ b/packages/core/src/code_assist/setup.ts @@ -42,9 +42,26 @@ export interface UserData { } /** + * Sets up the user by loading their Code Assist configuration and onboarding if needed. * - * @param projectId the user's project id, if any - * @returns the user's actual project id + * Tier eligibility: + * - FREE tier: Eligibility is determined by the Code Assist server response. + * - STANDARD tier: User is always eligible if they have a valid project ID. + * + * If no valid project ID is available (from env var or server response): + * - Surfaces ineligibility reasons for the FREE tier from the server. + * - Throws ProjectIdRequiredError if no ineligibility reasons are available. + * + * Handles VALIDATION_REQUIRED via the optional validation handler, allowing + * retry, auth change, or cancellation. + * + * @param client - The authenticated client to use for API calls + * @param validationHandler - Optional handler for account validation flow + * @returns The user's project ID, tier ID, and tier name + * @throws {ValidationRequiredError} If account validation is required + * @throws {ProjectIdRequiredError} If no project ID is available and required + * @throws {ValidationCancelledError} If user cancels validation + * @throws {ChangeAuthRequestedError} If user requests to change auth method */ export async function setupUser( client: AuthClient, @@ -108,6 +125,14 @@ export async function setupUser( userTierName: loadRes.currentTier.name, }; } + + // If user is not setup for standard tier, inform them about all other tiers they are ineligible for. + if (loadRes.ineligibleTiers && loadRes.ineligibleTiers.length > 0) { + const reasons = loadRes.ineligibleTiers + .map((t) => t.reasonMessage) + .join(', '); + throw new Error(reasons); + } throw new ProjectIdRequiredError(); } return { @@ -188,7 +213,6 @@ function validateLoadCodeAssistResponse(res: LoadCodeAssistResponse): void { res.ineligibleTiers && res.ineligibleTiers.length > 0 ) { - // Check for VALIDATION_REQUIRED first - this is a recoverable state const validationTier = res.ineligibleTiers.find( (t) => t.validationUrl && @@ -203,9 +227,5 @@ function validateLoadCodeAssistResponse(res: LoadCodeAssistResponse): void { validationTier.reasonMessage, ); } - - // For other ineligibility reasons, throw a generic error - const reasons = res.ineligibleTiers.map((t) => t.reasonMessage).join(', '); - throw new Error(reasons); } }