Skip to content

Commit 4484152

Browse files
gsquared94Yuna Seol
authored andcommitted
fix: loadcodeassist eligible tiers getting ignored for unlicensed users (regression) (#17581)
1 parent 36a16ea commit 4484152

File tree

2 files changed

+161
-9
lines changed

2 files changed

+161
-9
lines changed

packages/core/src/code_assist/setup.test.ts

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,86 @@ describe('setupUser validation', () => {
332332
vi.unstubAllEnvs();
333333
});
334334

335-
it('should throw error if LoadCodeAssist returns ineligible tiers and no current tier', async () => {
335+
it('should throw ineligible tier error when currentTier exists but no project ID available', async () => {
336+
vi.stubEnv('GOOGLE_CLOUD_PROJECT', '');
337+
mockLoad.mockResolvedValue({
338+
currentTier: mockPaidTier,
339+
cloudaicompanionProject: undefined,
340+
ineligibleTiers: [
341+
{
342+
reasonMessage: 'User is not eligible',
343+
reasonCode: 'INELIGIBLE_ACCOUNT',
344+
tierId: 'free-tier',
345+
tierName: 'free',
346+
},
347+
],
348+
});
349+
350+
await expect(setupUser({} as OAuth2Client)).rejects.toThrow(
351+
'User is not eligible',
352+
);
353+
});
354+
355+
it('should continue if LoadCodeAssist returns ineligible tiers but has allowed tiers', async () => {
356+
const mockOnboardUser = vi.fn().mockResolvedValue({
357+
done: true,
358+
response: {
359+
cloudaicompanionProject: {
360+
id: 'server-project',
361+
},
362+
},
363+
});
364+
vi.mocked(CodeAssistServer).mockImplementation(
365+
() =>
366+
({
367+
loadCodeAssist: mockLoad,
368+
onboardUser: mockOnboardUser,
369+
}) as unknown as CodeAssistServer,
370+
);
371+
372+
mockLoad.mockResolvedValue({
373+
currentTier: null,
374+
allowedTiers: [mockPaidTier],
375+
ineligibleTiers: [
376+
{
377+
reasonMessage: 'Not eligible for free tier',
378+
reasonCode: 'INELIGIBLE_ACCOUNT',
379+
tierId: 'free-tier',
380+
tierName: 'free',
381+
},
382+
],
383+
});
384+
385+
// Should not throw - should proceed to onboarding with the allowed tier
386+
const result = await setupUser({} as OAuth2Client);
387+
expect(result).toEqual({
388+
projectId: 'server-project',
389+
userTier: 'standard-tier',
390+
userTierName: 'paid',
391+
});
392+
expect(mockOnboardUser).toHaveBeenCalled();
393+
});
394+
395+
it('should proceed to onboarding with LEGACY tier when no currentTier and no allowedTiers', async () => {
396+
const mockOnboardUser = vi.fn().mockResolvedValue({
397+
done: true,
398+
response: {
399+
cloudaicompanionProject: {
400+
id: 'server-project',
401+
},
402+
},
403+
});
404+
vi.mocked(CodeAssistServer).mockImplementation(
405+
() =>
406+
({
407+
loadCodeAssist: mockLoad,
408+
onboardUser: mockOnboardUser,
409+
}) as unknown as CodeAssistServer,
410+
);
411+
336412
mockLoad.mockResolvedValue({
337413
currentTier: null,
414+
allowedTiers: undefined,
338415
ineligibleTiers: [
339416
{
340417
reasonMessage: 'User is not eligible',
@@ -345,8 +422,63 @@ describe('setupUser validation', () => {
345422
],
346423
});
347424

425+
// Should proceed to onboarding with LEGACY tier, ignoring ineligible tier errors
426+
const result = await setupUser({} as OAuth2Client);
427+
expect(result).toEqual({
428+
projectId: 'server-project',
429+
userTier: 'legacy-tier',
430+
userTierName: '',
431+
});
432+
expect(mockOnboardUser).toHaveBeenCalledWith(
433+
expect.objectContaining({
434+
tierId: 'legacy-tier',
435+
}),
436+
);
437+
});
438+
439+
it('should throw ValidationRequiredError even if allowed tiers exist', async () => {
440+
mockLoad.mockResolvedValue({
441+
currentTier: null,
442+
allowedTiers: [mockPaidTier],
443+
ineligibleTiers: [
444+
{
445+
reasonMessage: 'Please verify your account',
446+
reasonCode: 'VALIDATION_REQUIRED',
447+
tierId: 'free-tier',
448+
tierName: 'free',
449+
validationUrl: 'https://example.com/verify',
450+
},
451+
],
452+
});
453+
454+
await expect(setupUser({} as OAuth2Client)).rejects.toThrow(
455+
ValidationRequiredError,
456+
);
457+
});
458+
459+
it('should combine multiple ineligible tier messages when currentTier exists but no project ID', async () => {
460+
vi.stubEnv('GOOGLE_CLOUD_PROJECT', '');
461+
mockLoad.mockResolvedValue({
462+
currentTier: mockPaidTier,
463+
cloudaicompanionProject: undefined,
464+
ineligibleTiers: [
465+
{
466+
reasonMessage: 'Not eligible for standard',
467+
reasonCode: 'INELIGIBLE_ACCOUNT',
468+
tierId: 'standard-tier',
469+
tierName: 'standard',
470+
},
471+
{
472+
reasonMessage: 'Not eligible for free',
473+
reasonCode: 'INELIGIBLE_ACCOUNT',
474+
tierId: 'free-tier',
475+
tierName: 'free',
476+
},
477+
],
478+
});
479+
348480
await expect(setupUser({} as OAuth2Client)).rejects.toThrow(
349-
'User is not eligible',
481+
'Not eligible for standard, Not eligible for free',
350482
);
351483
});
352484

packages/core/src/code_assist/setup.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,26 @@ export interface UserData {
4242
}
4343

4444
/**
45+
* Sets up the user by loading their Code Assist configuration and onboarding if needed.
4546
*
46-
* @param projectId the user's project id, if any
47-
* @returns the user's actual project id
47+
* Tier eligibility:
48+
* - FREE tier: Eligibility is determined by the Code Assist server response.
49+
* - STANDARD tier: User is always eligible if they have a valid project ID.
50+
*
51+
* If no valid project ID is available (from env var or server response):
52+
* - Surfaces ineligibility reasons for the FREE tier from the server.
53+
* - Throws ProjectIdRequiredError if no ineligibility reasons are available.
54+
*
55+
* Handles VALIDATION_REQUIRED via the optional validation handler, allowing
56+
* retry, auth change, or cancellation.
57+
*
58+
* @param client - The authenticated client to use for API calls
59+
* @param validationHandler - Optional handler for account validation flow
60+
* @returns The user's project ID, tier ID, and tier name
61+
* @throws {ValidationRequiredError} If account validation is required
62+
* @throws {ProjectIdRequiredError} If no project ID is available and required
63+
* @throws {ValidationCancelledError} If user cancels validation
64+
* @throws {ChangeAuthRequestedError} If user requests to change auth method
4865
*/
4966
export async function setupUser(
5067
client: AuthClient,
@@ -108,6 +125,14 @@ export async function setupUser(
108125
userTierName: loadRes.currentTier.name,
109126
};
110127
}
128+
129+
// If user is not setup for standard tier, inform them about all other tiers they are ineligible for.
130+
if (loadRes.ineligibleTiers && loadRes.ineligibleTiers.length > 0) {
131+
const reasons = loadRes.ineligibleTiers
132+
.map((t) => t.reasonMessage)
133+
.join(', ');
134+
throw new Error(reasons);
135+
}
111136
throw new ProjectIdRequiredError();
112137
}
113138
return {
@@ -188,7 +213,6 @@ function validateLoadCodeAssistResponse(res: LoadCodeAssistResponse): void {
188213
res.ineligibleTiers &&
189214
res.ineligibleTiers.length > 0
190215
) {
191-
// Check for VALIDATION_REQUIRED first - this is a recoverable state
192216
const validationTier = res.ineligibleTiers.find(
193217
(t) =>
194218
t.validationUrl &&
@@ -203,9 +227,5 @@ function validateLoadCodeAssistResponse(res: LoadCodeAssistResponse): void {
203227
validationTier.reasonMessage,
204228
);
205229
}
206-
207-
// For other ineligibility reasons, throw a generic error
208-
const reasons = res.ineligibleTiers.map((t) => t.reasonMessage).join(', ');
209-
throw new Error(reasons);
210230
}
211231
}

0 commit comments

Comments
 (0)