claudish/tests/gemini-adapter.test.ts

394 lines
13 KiB
TypeScript
Raw Normal View History

/**
* Unit tests for GeminiAdapter
*
* Tests thought signature extraction from OpenRouter's reasoning_details format,
* based on real API responses captured during manual testing.
*/
import { describe, it, expect, beforeEach } from "bun:test";
import { GeminiAdapter } from "../src/adapters/gemini-adapter";
describe("GeminiAdapter", () => {
let adapter: GeminiAdapter;
beforeEach(() => {
adapter = new GeminiAdapter("google/gemini-3-pro-preview");
});
describe("Model Detection", () => {
it("should handle google/gemini-3-pro-preview model", () => {
expect(adapter.shouldHandle("google/gemini-3-pro-preview")).toBe(true);
});
it("should handle google/gemini-2.5-flash model", () => {
expect(adapter.shouldHandle("google/gemini-2.5-flash")).toBe(true);
});
it("should handle gemini models without google/ prefix", () => {
expect(adapter.shouldHandle("gemini-3-pro")).toBe(true);
});
it("should NOT handle non-Gemini models", () => {
expect(adapter.shouldHandle("x-ai/grok-code-fast-1")).toBe(false);
expect(adapter.shouldHandle("openai/gpt-5")).toBe(false);
expect(adapter.shouldHandle("claude-sonnet")).toBe(false);
});
it("should return correct adapter name", () => {
expect(adapter.getName()).toBe("GeminiAdapter");
});
});
describe("Thought Signature Extraction", () => {
it("should extract signatures from reasoning_details with encrypted type", () => {
// Real response data from OpenRouter test
const reasoningDetails = [
{
id: "tool_Bash_ZOJxtsiJqi9njkBUmCeV",
type: "reasoning.encrypted",
data: "CiQB4/H/XsukhAagMavyI3vfZtzB0lQLRD5TIh1OQyfMar/wzqoKaQHj8f9e7azlSwPXjAxZ3Vy+SA3Lozr6JjvJah7yLoz34Z44orOB9T5IM3acsExG0w2M+LdYDxSm3WfUqbUJTvs4EmG098y5FWCKWhMG1aVaHNGuQ5uytp+21m8BOw0Qw+Q9mEqd7TYK7gpjAePx/16yxZM4eAE4YppB66hLqV6qjWd6vEJ9lGIMbmqi+t5t4Se/HkBPizrcgbdaOd3Fje5GXRfb1vqv+nhuxWwOx+hAFczJWwtd8d6H/YloE38JqTSNt98sb0odCShJcNnVCjgB4/H/XoJS5Xrj4j5jSsnUSG+rvZi6NKV+La8QIur8jKEeBF0DbTnO+ZNiYzz9GokbPHjkIRKePA==",
format: "google-gemini-v1",
index: 0
}
];
const extracted = adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
expect(extracted.size).toBe(1);
expect(extracted.has("tool_Bash_ZOJxtsiJqi9njkBUmCeV")).toBe(true);
expect(extracted.get("tool_Bash_ZOJxtsiJqi9njkBUmCeV")).toBe(reasoningDetails[0].data);
});
it("should extract from multiple reasoning_details", () => {
const reasoningDetails = [
{
id: "tool_1",
type: "reasoning.encrypted",
data: "signature-data-1",
format: "google-gemini-v1",
index: 0
},
{
id: "tool_2",
type: "reasoning.encrypted",
data: "signature-data-2",
format: "google-gemini-v1",
index: 1
}
];
const extracted = adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
expect(extracted.size).toBe(2);
expect(extracted.get("tool_1")).toBe("signature-data-1");
expect(extracted.get("tool_2")).toBe("signature-data-2");
});
it("should skip reasoning_details without encrypted type", () => {
const reasoningDetails = [
{
id: "tool_1",
type: "reasoning.encrypted",
data: "signature-data-1",
format: "google-gemini-v1",
index: 0
},
{
id: "tool_2",
type: "reasoning.text", // Not encrypted, should skip
text: "thinking process...",
format: "google-gemini-v1",
index: 1
}
];
const extracted = adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
expect(extracted.size).toBe(1);
expect(extracted.has("tool_1")).toBe(true);
expect(extracted.has("tool_2")).toBe(false);
});
it("should skip reasoning_details without id", () => {
const reasoningDetails = [
{
type: "reasoning.encrypted",
data: "signature-data-1",
format: "google-gemini-v1",
index: 0
// Missing id
}
];
const extracted = adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
expect(extracted.size).toBe(0);
});
it("should skip reasoning_details without data", () => {
const reasoningDetails = [
{
id: "tool_1",
type: "reasoning.encrypted",
format: "google-gemini-v1",
index: 0
// Missing data
}
];
const extracted = adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
expect(extracted.size).toBe(0);
});
it("should handle empty reasoning_details array", () => {
const extracted = adapter.extractThoughtSignaturesFromReasoningDetails([]);
expect(extracted.size).toBe(0);
});
it("should handle undefined reasoning_details", () => {
const extracted = adapter.extractThoughtSignaturesFromReasoningDetails(undefined as any);
expect(extracted.size).toBe(0);
});
});
describe("Signature Storage", () => {
it("should store extracted signatures internally", () => {
const reasoningDetails = [
{
id: "tool_123",
type: "reasoning.encrypted",
data: "encrypted-signature-data",
format: "google-gemini-v1",
index: 0
}
];
adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
expect(adapter.hasThoughtSignature("tool_123")).toBe(true);
expect(adapter.getThoughtSignature("tool_123")).toBe("encrypted-signature-data");
});
it("should retrieve stored signatures", () => {
const reasoningDetails = [
{
id: "tool_abc",
type: "reasoning.encrypted",
data: "test-signature-xyz",
format: "google-gemini-v1",
index: 0
}
];
adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
const signature = adapter.getThoughtSignature("tool_abc");
expect(signature).toBe("test-signature-xyz");
});
it("should return undefined for unknown tool call IDs", () => {
const signature = adapter.getThoughtSignature("non-existent-tool");
expect(signature).toBeUndefined();
});
it("should return false for hasThoughtSignature on unknown IDs", () => {
expect(adapter.hasThoughtSignature("unknown-tool")).toBe(false);
});
it("should store multiple signatures", () => {
const reasoningDetails1 = [
{
id: "tool_1",
type: "reasoning.encrypted",
data: "sig-1",
format: "google-gemini-v1",
index: 0
}
];
const reasoningDetails2 = [
{
id: "tool_2",
type: "reasoning.encrypted",
data: "sig-2",
format: "google-gemini-v1",
index: 0
}
];
adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails1);
adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails2);
expect(adapter.hasThoughtSignature("tool_1")).toBe(true);
expect(adapter.hasThoughtSignature("tool_2")).toBe(true);
expect(adapter.getThoughtSignature("tool_1")).toBe("sig-1");
expect(adapter.getThoughtSignature("tool_2")).toBe("sig-2");
});
it("should override existing signatures with same tool_call_id", () => {
const reasoningDetails1 = [
{
id: "tool_1",
type: "reasoning.encrypted",
data: "original-signature",
format: "google-gemini-v1",
index: 0
}
];
const reasoningDetails2 = [
{
id: "tool_1",
type: "reasoning.encrypted",
data: "updated-signature",
format: "google-gemini-v1",
index: 0
}
];
adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails1);
adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails2);
expect(adapter.getThoughtSignature("tool_1")).toBe("updated-signature");
});
});
describe("Reset Functionality", () => {
it("should clear all stored signatures on reset", () => {
const reasoningDetails = [
{
id: "tool_1",
type: "reasoning.encrypted",
data: "sig-1",
format: "google-gemini-v1",
index: 0
},
{
id: "tool_2",
type: "reasoning.encrypted",
data: "sig-2",
format: "google-gemini-v1",
index: 1
}
];
adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
expect(adapter.getAllThoughtSignatures().size).toBe(2);
adapter.reset();
expect(adapter.getAllThoughtSignatures().size).toBe(0);
expect(adapter.hasThoughtSignature("tool_1")).toBe(false);
expect(adapter.hasThoughtSignature("tool_2")).toBe(false);
});
});
describe("Get All Signatures", () => {
it("should return copy of all signatures", () => {
const reasoningDetails = [
{
id: "tool_1",
type: "reasoning.encrypted",
data: "sig-1",
format: "google-gemini-v1",
index: 0
},
{
id: "tool_2",
type: "reasoning.encrypted",
data: "sig-2",
format: "google-gemini-v1",
index: 1
}
];
adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
const allSignatures = adapter.getAllThoughtSignatures();
expect(allSignatures.size).toBe(2);
expect(allSignatures.get("tool_1")).toBe("sig-1");
expect(allSignatures.get("tool_2")).toBe("sig-2");
// Should be a copy (modifying doesn't affect internal state)
allSignatures.set("tool_3", "sig-3");
expect(adapter.getAllThoughtSignatures().size).toBe(2);
});
it("should return empty Map when no signatures stored", () => {
const allSignatures = adapter.getAllThoughtSignatures();
expect(allSignatures.size).toBe(0);
});
});
describe("OpenRouter Real Data Test", () => {
it("should extract from real OpenRouter streaming response structure", () => {
// This is actual data from OpenRouter test (test-gemini-thought-signature.ts)
const reasoningDetails = [
{
id: "tool_Bash_ZOJxtsiJqi9njkBUmCeV",
type: "reasoning.encrypted",
data: "CiQB4/H/XsukhAagMavyI3vfZtzB0lQLRD5TIh1OQyfMar/wzqoKaQHj8f9e7azlSwPXjAxZ3Vy+SA3Lozr6JjvJah7yLoz34Z44orOB9T5IM3acsExG0w2M+LdYDxSm3WfUqbUJTvs4EmG098y5FWCKWhMG1aVaHNGuQ5uytp+21m8BOw0Qw+Q9mEqd7TYK7gpjAePx/16yxZM4eAE4YppB66hLqV6qjWd6vEJ9lGIMbmqi+t5t4Se/HkBPizrcgbdaOd3Fje5GXRfb1vqv+nhuxWwOx+hAFczJWwtd8d6H/YloE38JqTSNt98sb0odCShJcNnVCjgB4/H/XoJS5Xrj4j5jSsnUSG+rvZi6NKV+La8QIur8jKEeBF0DbTnO+ZNiYzz9GokbPHjkIRKePA==",
format: "google-gemini-v1",
index: 0
}
];
const extracted = adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
expect(extracted.size).toBe(1);
expect(extracted.has("tool_Bash_ZOJxtsiJqi9njkBUmCeV")).toBe(true);
// Verify the signature data matches exactly
const signature = extracted.get("tool_Bash_ZOJxtsiJqi9njkBUmCeV");
expect(signature).toBe(reasoningDetails[0].data);
expect(signature).toContain("CiQB4/H/X"); // Should start with encrypted prefix
expect(signature.length).toBeGreaterThan(100); // Should be long (encrypted)
});
it("should handle OpenRouter's non-streaming response format", () => {
// From test-non-streaming.ts - shows full response structure
const reasoningDetails = [
{
format: "google-gemini-v1",
index: 0,
type: "reasoning.text",
text: "**Analyzing Command Execution**\n\nI've decided on the Bash tool..."
},
{
id: "tool_Bash_xCnVDMy3yKKLMmubLViZ",
format: "google-gemini-v1",
index: 0,
type: "reasoning.encrypted",
data: "CiQB4/H/Xpq6W/zfkirEV83BJOnpNRAEsRj3j95YOEooIPrBh1cKZgHj8f9eJ8A0IFVGYoG0HDJXG0MuH41sRRpJkvtF2vmnl36y0KOrmiKGnoKerQlRKodqdQBh1N04iwI8+9ULLbnnk/4YSpAi2/uh2xqOHnt2jluPJbnpZOJ1Cd+zHf7/VZqj1WZbEgpaAePx/158Zpu4rKl4VbaLLmuJfwoLFE58SrhoOqhpu52Fsw3JeEl4ezcOlxYkA91fFNVDcVaE9J3sdfeUUsP7c6EPNwKX0Roj4xGAn6R4THYoZaLRdBoaTt7bClEB4/H/Xm1hmM8Qwyj4XqSLOH1e4lbgYwYYECa0060K6z8YTS+wKaKkAWrk7WpDDovNzrTihw1aMvBy5oY0kVjhvKe0s48QiStQx/KBrwU3xfY="
}
];
const extracted = adapter.extractThoughtSignaturesFromReasoningDetails(reasoningDetails);
// Should only extract the encrypted one, not the text one
expect(extracted.size).toBe(1);
expect(extracted.has("tool_Bash_xCnVDMy3yKKLMmubLViZ")).toBe(true);
expect(extracted.has(undefined as any)).toBe(false);
});
});
describe("processTextContent", () => {
it("should pass through text unchanged (Gemini doesn't use special text formats)", () => {
const result = adapter.processTextContent("Hello world", "");
expect(result.cleanedText).toBe("Hello world");
expect(result.extractedToolCalls).toEqual([]);
expect(result.wasTransformed).toBe(false);
});
it("should handle empty text", () => {
const result = adapter.processTextContent("", "");
expect(result.cleanedText).toBe("");
expect(result.extractedToolCalls).toEqual([]);
expect(result.wasTransformed).toBe(false);
});
});
});