Add interactive ValidationDialog for handling 403 VALIDATION_REQUIRED errors. (#16231)

This commit is contained in:
Gaurav
2026-01-20 16:23:01 -08:00
committed by GitHub
parent aceb06a587
commit 3b626e7c61
18 changed files with 1060 additions and 12 deletions
@@ -498,4 +498,186 @@ To disable gemini-3-pro-preview, disable "Preview features" in /settings.`,
);
});
});
describe('Validation Handler', () => {
let setValidationHandlerSpy: SpyInstance;
beforeEach(() => {
setValidationHandlerSpy = vi.spyOn(mockConfig, 'setValidationHandler');
});
it('should register a validation handler on initialization', () => {
renderHook(() =>
useQuotaAndFallback({
config: mockConfig,
historyManager: mockHistoryManager,
userTier: UserTierId.FREE,
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
}),
);
expect(setValidationHandlerSpy).toHaveBeenCalledTimes(1);
expect(setValidationHandlerSpy.mock.calls[0][0]).toBeInstanceOf(Function);
});
it('should set a validation request when handler is called', async () => {
const { result } = renderHook(() =>
useQuotaAndFallback({
config: mockConfig,
historyManager: mockHistoryManager,
userTier: UserTierId.FREE,
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
}),
);
const handler = setValidationHandlerSpy.mock.calls[0][0] as (
validationLink?: string,
validationDescription?: string,
learnMoreUrl?: string,
) => Promise<'verify' | 'change_auth' | 'cancel'>;
let promise: Promise<'verify' | 'change_auth' | 'cancel'>;
act(() => {
promise = handler(
'https://example.com/verify',
'Please verify',
'https://example.com/help',
);
});
const request = result.current.validationRequest;
expect(request).not.toBeNull();
expect(request?.validationLink).toBe('https://example.com/verify');
expect(request?.validationDescription).toBe('Please verify');
expect(request?.learnMoreUrl).toBe('https://example.com/help');
// Simulate user choosing verify
act(() => {
result.current.handleValidationChoice('verify');
});
const intent = await promise!;
expect(intent).toBe('verify');
expect(result.current.validationRequest).toBeNull();
});
it('should handle race conditions by returning cancel for subsequent requests', async () => {
const { result } = renderHook(() =>
useQuotaAndFallback({
config: mockConfig,
historyManager: mockHistoryManager,
userTier: UserTierId.FREE,
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
}),
);
const handler = setValidationHandlerSpy.mock.calls[0][0] as (
validationLink?: string,
) => Promise<'verify' | 'change_auth' | 'cancel'>;
let promise1: Promise<'verify' | 'change_auth' | 'cancel'>;
act(() => {
promise1 = handler('https://example.com/verify1');
});
const firstRequest = result.current.validationRequest;
expect(firstRequest).not.toBeNull();
let result2: 'verify' | 'change_auth' | 'cancel';
await act(async () => {
result2 = await handler('https://example.com/verify2');
});
// The lock should have stopped the second request
expect(result2!).toBe('cancel');
expect(result.current.validationRequest).toBe(firstRequest);
// Complete the first request
act(() => {
result.current.handleValidationChoice('verify');
});
const intent1 = await promise1!;
expect(intent1).toBe('verify');
expect(result.current.validationRequest).toBeNull();
});
it('should add info message when change_auth is chosen', async () => {
const { result } = renderHook(() =>
useQuotaAndFallback({
config: mockConfig,
historyManager: mockHistoryManager,
userTier: UserTierId.FREE,
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
}),
);
const handler = setValidationHandlerSpy.mock.calls[0][0] as (
validationLink?: string,
) => Promise<'verify' | 'change_auth' | 'cancel'>;
let promise: Promise<'verify' | 'change_auth' | 'cancel'>;
act(() => {
promise = handler('https://example.com/verify');
});
act(() => {
result.current.handleValidationChoice('change_auth');
});
const intent = await promise!;
expect(intent).toBe('change_auth');
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[0][0];
expect(lastCall.type).toBe(MessageType.INFO);
expect(lastCall.text).toBe('Use /auth to change authentication method.');
});
it('should not add info message when cancel is chosen', async () => {
const { result } = renderHook(() =>
useQuotaAndFallback({
config: mockConfig,
historyManager: mockHistoryManager,
userTier: UserTierId.FREE,
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
}),
);
const handler = setValidationHandlerSpy.mock.calls[0][0] as (
validationLink?: string,
) => Promise<'verify' | 'change_auth' | 'cancel'>;
let promise: Promise<'verify' | 'change_auth' | 'cancel'>;
act(() => {
promise = handler('https://example.com/verify');
});
act(() => {
result.current.handleValidationChoice('cancel');
});
const intent = await promise!;
expect(intent).toBe('cancel');
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
});
it('should do nothing if handleValidationChoice is called without pending request', () => {
const { result } = renderHook(() =>
useQuotaAndFallback({
config: mockConfig,
historyManager: mockHistoryManager,
userTier: UserTierId.FREE,
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
}),
);
act(() => {
result.current.handleValidationChoice('verify');
});
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
});
});
});