@@ -947,4 +947,233 @@ describe('GeminiAdapter', () => {
947947 expect ( health . errors [ 0 ] ) . toContain ( 'Health check failed' ) ;
948948 } ) ;
949949 } ) ;
950+ } ) ;
951+ // ---------------- Additional tests appended by PR automation ----------------
952+
953+ describe ( 'GeminiAdapter – additional coverage' , ( ) => {
954+ let adapter : GeminiAdapter ;
955+
956+ const baseConfig : GeminiAdapterConfig = {
957+ modelName : 'gemini-2.0-flash' ,
958+ model : 'gemini-2.0-flash' ,
959+ apiKey : 'test-api-key' ,
960+ timeout : 200 , // make timeouts fast for tests we add
961+ retryAttempts : 3 ,
962+ streamingEnabled : true ,
963+ cachingEnabled : true
964+ } ;
965+
966+ beforeEach ( async ( ) => {
967+ jest . clearAllMocks ( ) ;
968+ process . env . GOOGLE_AI_API_KEY = undefined ;
969+ adapter = new GeminiAdapter ( baseConfig ) ;
970+ await adapter . initialize ( ) ;
971+ } ) ;
972+
973+ it ( 'should prefer request.parameters over config.generationConfig when both are provided' , async ( ) => {
974+ const GoogleGenerativeAI = require ( '@google/generative-ai' ) . GoogleGenerativeAI ;
975+ const genModel = GoogleGenerativeAI ( ) . getGenerativeModel ( ) ;
976+ const genSpy = jest . spyOn ( genModel , 'generateContent' ) ;
977+
978+ // Recreate adapter with a default generationConfig
979+ adapter = new GeminiAdapter ( {
980+ ...baseConfig ,
981+ generationConfig : {
982+ temperature : 0.1 ,
983+ topP : 0.2 ,
984+ topK : 10 ,
985+ maxOutputTokens : 256 ,
986+ stopSequences : [ 'STOP_FROM_CONFIG' ] ,
987+ }
988+ } ) ;
989+ await adapter . initialize ( ) ;
990+
991+ const request : ModelRequest = {
992+ prompt : 'param precedence' ,
993+ parameters : {
994+ temperature : 0.9 ,
995+ topP : 0.95 ,
996+ topK : 50 ,
997+ maxTokens : 1234 ,
998+ stopSequences : [ 'END_FROM_REQUEST' ]
999+ } ,
1000+ context : {
1001+ requestId : 'precedence-1' ,
1002+ priority : 'medium' ,
1003+ userTier : 'pro' ,
1004+ latencyTarget : 1000 ,
1005+ }
1006+ } ;
1007+
1008+ await adapter . generate ( request ) ;
1009+
1010+ // Ensure we called google sdk with request-level overrides
1011+ const lastCallArg = genSpy . mock . calls [ 0 ] ?. [ 0 ] ;
1012+ // The argument can be either a single object or array of parts depending on implementation.
1013+ // We just assert generationConfig-like fields are somewhere present via string match to reduce tight coupling.
1014+ expect ( JSON . stringify ( lastCallArg ) ) . toEqual ( expect . stringContaining ( '"temperature":0.9' ) ) ;
1015+ expect ( JSON . stringify ( lastCallArg ) ) . toEqual ( expect . stringContaining ( '"topP":0.95' ) ) ;
1016+ expect ( JSON . stringify ( lastCallArg ) ) . toEqual ( expect . stringContaining ( '"topK":50' ) ) ;
1017+ expect ( JSON . stringify ( lastCallArg ) ) . toMatch ( / ( m a x T o k e n s | m a x O u t p u t T o k e n s ) \" \s * : \s * 1 2 3 4 / ) ;
1018+ expect ( JSON . stringify ( lastCallArg ) ) . toEqual ( expect . stringContaining ( 'END_FROM_REQUEST' ) ) ;
1019+ } ) ;
1020+
1021+ it ( 'should retry transient errors and eventually succeed within retryAttempts' , async ( ) => {
1022+ jest . useFakeTimers ( { now : Date . now ( ) } ) ;
1023+ const GoogleGenerativeAI = require ( '@google/generative-ai' ) . GoogleGenerativeAI ;
1024+
1025+ let calls = 0 ;
1026+ GoogleGenerativeAI . mockImplementationOnce ( ( ) => ( {
1027+ getGenerativeModel : jest . fn ( ) . mockReturnValue ( {
1028+ generateContent : jest . fn ( ) . mockImplementation ( ( ) => {
1029+ calls += 1 ;
1030+ if ( calls < 3 ) {
1031+ // First two attempts fail with retryable 500
1032+ return Promise . reject ( { status : 500 , message : 'Transient' } ) ;
1033+ }
1034+ // Succeeds on 3rd
1035+ return Promise . resolve ( {
1036+ response : {
1037+ text : ( ) => 'Recovered after retries' ,
1038+ candidates : [ { finishReason : 'STOP' } ] ,
1039+ usageMetadata : { promptTokenCount : 1 , candidatesTokenCount : 1 , totalTokenCount : 2 }
1040+ }
1041+ } ) ;
1042+ } )
1043+ } )
1044+ } ) ) ;
1045+
1046+ adapter = new GeminiAdapter ( { ...baseConfig , retryAttempts : 3 } ) ;
1047+ await adapter . initialize ( ) ;
1048+
1049+ const p = adapter . generate ( {
1050+ prompt : 'retry please' ,
1051+ context : { requestId : 'retry-1' , priority : 'medium' , userTier : 'pro' , latencyTarget : 1000 }
1052+ } ) ;
1053+
1054+ // Advance timers in case adapter uses backoff via setTimeout
1055+ jest . runAllTimers ( ) ;
1056+
1057+ const res = await p ;
1058+ expect ( res . content ) . toBe ( 'Recovered after retries' ) ;
1059+ expect ( calls ) . toBe ( 3 ) ;
1060+ jest . useRealTimers ( ) ;
1061+ } ) ;
1062+
1063+ it ( 'should not use cache when cachingEnabled is false (subsequent identical calls hit provider again)' , async ( ) => {
1064+ const GoogleGenerativeAI = require ( '@google/generative-ai' ) . GoogleGenerativeAI ;
1065+ const genModel = GoogleGenerativeAI ( ) . getGenerativeModel ( ) ;
1066+ const genSpy = jest . spyOn ( genModel , 'generateContent' ) ;
1067+
1068+ const noCacheAdapter = new GeminiAdapter ( { ...baseConfig , cachingEnabled : false } ) ;
1069+ await noCacheAdapter . initialize ( ) ;
1070+
1071+ const req : ModelRequest = {
1072+ prompt : 'no-cache' ,
1073+ context : { requestId : 'nocache-1' , priority : 'medium' , userTier : 'pro' , latencyTarget : 1000 }
1074+ } ;
1075+
1076+ await noCacheAdapter . generate ( req ) ;
1077+ await noCacheAdapter . generate ( req ) ;
1078+
1079+ // Should call the underlying API twice (no caching)
1080+ expect ( genSpy ) . toHaveBeenCalledTimes ( 2 ) ;
1081+ } ) ;
1082+
1083+ it ( 'should timeout long-running generation requests according to config.timeout' , async ( ) => {
1084+ jest . useFakeTimers ( { now : Date . now ( ) } ) ;
1085+ const GoogleGenerativeAI = require ( '@google/generative-ai' ) . GoogleGenerativeAI ;
1086+
1087+ // Mock a never-resolving promise to trigger timeout
1088+ GoogleGenerativeAI . mockImplementationOnce ( ( ) => ( {
1089+ getGenerativeModel : jest . fn ( ) . mockReturnValue ( {
1090+ generateContent : jest . fn ( ) . mockImplementation (
1091+ ( ) => new Promise ( ( ) => { /* never resolve */ } )
1092+ )
1093+ } )
1094+ } ) ) ;
1095+
1096+ const shortTimeoutAdapter = new GeminiAdapter ( { ...baseConfig , timeout : 50 } ) ;
1097+ await shortTimeoutAdapter . initialize ( ) ;
1098+
1099+ const genPromise = shortTimeoutAdapter . generate ( {
1100+ prompt : 'hang' ,
1101+ context : { requestId : 'timeout-1' , priority : 'medium' , userTier : 'pro' , latencyTarget : 1000 }
1102+ } ) ;
1103+
1104+ jest . advanceTimersByTime ( 60 ) ;
1105+
1106+ await expect ( genPromise ) . rejects . toMatchObject ( {
1107+ code : expect . stringMatching ( / T I M E O U T | R E Q U E S T _ T I M E O U T / i)
1108+ } ) ;
1109+
1110+ jest . useRealTimers ( ) ;
1111+ } ) ;
1112+
1113+ it ( 'should include requestId in the response id for correlation' , async ( ) => {
1114+ const request : ModelRequest = {
1115+ prompt : 'ID correlation test' ,
1116+ context : {
1117+ requestId : 'my-req-123' ,
1118+ priority : 'medium' ,
1119+ userTier : 'pro' ,
1120+ latencyTarget : 1000
1121+ }
1122+ } ;
1123+
1124+ const response = await adapter . generate ( request ) ;
1125+ expect ( response . id ) . toEqual ( expect . stringContaining ( 'my-req-123' ) ) ;
1126+ } ) ;
1127+
1128+ it ( 'should accept audio/video multimodal inputs for models that support them' , async ( ) => {
1129+ const request : ModelRequest = {
1130+ prompt : 'Analyze av' ,
1131+ multimodal : {
1132+ audio : [ 'base64-audio' ] ,
1133+ video : [ 'base64-video' ]
1134+ } as any ,
1135+ context : {
1136+ requestId : 'av-1' ,
1137+ priority : 'medium' ,
1138+ userTier : 'pro' ,
1139+ latencyTarget : 1000
1140+ }
1141+ } ;
1142+
1143+ const isValid = await adapter . validateRequest ( request ) ;
1144+ expect ( isValid ) . toBe ( true ) ;
1145+
1146+ const response = await adapter . generate ( request ) ;
1147+ expect ( response . content ) . toBe ( 'Gemini test response' ) ;
1148+ } ) ;
1149+
1150+ it ( 'should map tools into Google function declarations shape when provided' , async ( ) => {
1151+ const GoogleGenerativeAI = require ( '@google/generative-ai' ) . GoogleGenerativeAI ;
1152+ const genModel = GoogleGenerativeAI ( ) . getGenerativeModel ( ) ;
1153+ const genSpy = jest . spyOn ( genModel , 'generateContent' ) ;
1154+
1155+ const request : ModelRequest = {
1156+ prompt : 'test tools mapping' ,
1157+ tools : [
1158+ { name : 'sum' , description : 'adds' , parameters : { a : { type : 'number' } , b : { type : 'number' } } } ,
1159+ { name : 'lookup' , description : 'search' , parameters : { q : { type : 'string' } } }
1160+ ] as any ,
1161+ context : {
1162+ requestId : 'tools-1' ,
1163+ priority : 'medium' ,
1164+ userTier : 'pro' ,
1165+ latencyTarget : 1000
1166+ }
1167+ } ;
1168+
1169+ await adapter . generate ( request ) ;
1170+
1171+ const lastCallArg = genSpy . mock . calls [ 0 ] ?. [ 0 ] ;
1172+ const payload = JSON . stringify ( lastCallArg ) ;
1173+ // Loosely assert that function declarations are present
1174+ expect ( payload ) . toMatch ( / f u n c t i o n / i) ;
1175+ expect ( payload ) . toMatch ( / s u m / ) ;
1176+ expect ( payload ) . toMatch ( / l o o k u p / ) ;
1177+ expect ( payload ) . toMatch ( / p a r a m e t e r s ? / i) ;
1178+ } ) ;
9501179} ) ;
0 commit comments