Skip to content

Commit 8d962d9

Browse files
committed
fix: loadcodeassist eligible tiers getting ignored for unlicensed users (regression)
1 parent d75dc88 commit 8d962d9

File tree

2 files changed

+141
-7
lines changed

2 files changed

+141
-7
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: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ export async function setupUser(
108108
userTierName: loadRes.currentTier.name,
109109
};
110110
}
111+
112+
if (loadRes.ineligibleTiers && loadRes.ineligibleTiers.length > 0) {
113+
const reasons = loadRes.ineligibleTiers
114+
.map((t) => t.reasonMessage)
115+
.join(', ');
116+
throw new Error(reasons);
117+
}
111118
throw new ProjectIdRequiredError();
112119
}
113120
return {
@@ -188,7 +195,6 @@ function validateLoadCodeAssistResponse(res: LoadCodeAssistResponse): void {
188195
res.ineligibleTiers &&
189196
res.ineligibleTiers.length > 0
190197
) {
191-
// Check for VALIDATION_REQUIRED first - this is a recoverable state
192198
const validationTier = res.ineligibleTiers.find(
193199
(t) =>
194200
t.validationUrl &&
@@ -203,9 +209,5 @@ function validateLoadCodeAssistResponse(res: LoadCodeAssistResponse): void {
203209
validationTier.reasonMessage,
204210
);
205211
}
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);
210212
}
211213
}

0 commit comments

Comments
 (0)