Skip to content
This repository was archived by the owner on Jan 29, 2026. It is now read-only.

Commit 58c6c0a

Browse files
CodeRabbit Generated Unit Tests: Add extensive unit tests across adapters, streaming, utils, services (#26)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 6bdd481 commit 58c6c0a

File tree

7 files changed

+1619
-1
lines changed

7 files changed

+1619
-1
lines changed

tests/unit/adapters/gemini-adapter.test.ts

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/(maxTokens|maxOutputTokens)\"\s*:\s*1234/);
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(/TIMEOUT|REQUEST_TIMEOUT/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(/function/i);
1175+
expect(payload).toMatch(/sum/);
1176+
expect(payload).toMatch(/lookup/);
1177+
expect(payload).toMatch(/parameters?/i);
1178+
});
9501179
});

0 commit comments

Comments
 (0)