@@ -806,4 +806,123 @@ describe('classifyGoogleError', () => {
806806 const result = classifyGoogleError ( new Error ( ) ) ;
807807 expect ( result ) . toBeInstanceOf ( ValidationRequiredError ) ;
808808 } ) ;
809+
810+ it ( 'should return TerminalQuotaError when limit is 0 even if message contains "Please retry in Xs"' , ( ) => {
811+ const complexError = {
812+ error : {
813+ message :
814+ '{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0\\nPlease retry in 59.906331105s.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \\n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0\\nPlease retry in 59.906331105s."}]}}' ,
815+ code : 429 ,
816+ status : 'Too Many Requests' ,
817+ } ,
818+ } ;
819+ const rawError = new Error ( JSON . stringify ( complexError ) ) as Error & {
820+ status ?: number ;
821+ } ;
822+ rawError . status = 429 ;
823+ vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( null ) ;
824+
825+ const result = classifyGoogleError ( rawError ) ;
826+
827+ expect ( result ) . toBeInstanceOf ( TerminalQuotaError ) ;
828+ } ) ;
829+
830+ it ( 'should return TerminalQuotaError when limit is 0 even if structured RetryInfo is present' , ( ) => {
831+ const apiError : GoogleApiError = {
832+ code : 429 ,
833+ message : 'Quota exceeded for limit: 0' ,
834+ details : [
835+ {
836+ '@type' : 'type.googleapis.com/google.rpc.RetryInfo' ,
837+ retryDelay : '59s' ,
838+ } ,
839+ ] ,
840+ } ;
841+ vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( apiError ) ;
842+ const result = classifyGoogleError (
843+ new Error ( 'Quota exceeded for limit: 0' ) ,
844+ ) ;
845+ expect ( result ) . toBeInstanceOf ( TerminalQuotaError ) ;
846+ } ) ;
847+
848+ it ( 'should return TerminalQuotaError when limit is 0 and message contains actual newlines' , ( ) => {
849+ const apiError : GoogleApiError = {
850+ code : 429 ,
851+ message : 'Quota exceeded for metric: ...\nlimit: 0, model: gemini-3-pro' ,
852+ details : [ ] ,
853+ } ;
854+ vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( apiError ) ;
855+ const result = classifyGoogleError (
856+ new Error (
857+ 'Quota exceeded for metric: ...\nlimit: 0, model: gemini-3-pro' ,
858+ ) ,
859+ ) ;
860+ expect ( result ) . toBeInstanceOf ( TerminalQuotaError ) ;
861+ } ) ;
862+
863+ it ( 'should return TerminalQuotaError when limit is 0 followed by a period' , ( ) => {
864+ const apiError : GoogleApiError = {
865+ code : 429 ,
866+ message : 'Quota exceeded for metric: ...\nlimit: 0. Please retry in 59s.' ,
867+ details : [ ] ,
868+ } ;
869+ vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( apiError ) ;
870+ const result = classifyGoogleError (
871+ new Error (
872+ 'Quota exceeded for metric: ...\nlimit: 0. Please retry in 59s.' ,
873+ ) ,
874+ ) ;
875+ expect ( result ) . toBeInstanceOf ( TerminalQuotaError ) ;
876+ } ) ;
877+
878+ it ( 'should return RetryableQuotaError when limit is fractional (e.g., 0.5)' , ( ) => {
879+ const apiError : GoogleApiError = {
880+ code : 429 ,
881+ message :
882+ 'Quota exceeded for metric: ...\nlimit: 0.5. Please retry in 59s.' ,
883+ details : [ ] ,
884+ } ;
885+ vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( apiError ) ;
886+ const result = classifyGoogleError (
887+ new Error (
888+ 'Quota exceeded for metric: ...\nlimit: 0.5. Please retry in 59s.' ,
889+ ) ,
890+ ) ;
891+ expect ( result ) . toBeInstanceOf ( RetryableQuotaError ) ;
892+ } ) ;
893+
894+ it ( 'should fall back to "Model not found" for 404 error with plain object' , ( ) => {
895+ vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( null ) ;
896+ const result = classifyGoogleError ( { status : 404 } ) ;
897+ expect ( result ) . toBeInstanceOf ( ModelNotFoundError ) ;
898+ expect ( ( result as ModelNotFoundError ) . message ) . toBe ( 'Model not found' ) ;
899+ } ) ;
900+
901+ it ( 'should parse custom 404 message from plain object correctly' , ( ) => {
902+ vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( null ) ;
903+ const result = classifyGoogleError ( {
904+ status : 404 ,
905+ message : 'Custom 404 message' ,
906+ } ) ;
907+ expect ( result ) . toBeInstanceOf ( ModelNotFoundError ) ;
908+ expect ( ( result as ModelNotFoundError ) . message ) . toBe ( 'Custom 404 message' ) ;
909+ } ) ;
910+
911+ it ( 'should classify plain object with limit: 0 message as TerminalQuotaError correctly' , ( ) => {
912+ vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( null ) ;
913+ const result = classifyGoogleError ( {
914+ status : 429 ,
915+ message : 'Quota exceeded, limit: 0' ,
916+ } ) ;
917+ expect ( result ) . toBeInstanceOf ( TerminalQuotaError ) ;
918+ } ) ;
919+
920+ it ( 'should handle Error instances with undefined message gracefully' , ( ) => {
921+ const malformedError = new Error ( ) ;
922+ delete ( malformedError as { message ?: string } ) . message ;
923+ vi . spyOn ( errorParser , 'parseGoogleApiError' ) . mockReturnValue ( null ) ;
924+
925+ const result = classifyGoogleError ( malformedError ) ;
926+ expect ( result ) . toBe ( malformedError ) ; // Should return the original error without crashing
927+ } ) ;
809928} ) ;
0 commit comments