394 lines
13 KiB
TypeScript
394 lines
13 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|