Files
oh-my-pi/packages/coding-agent/test/tools/web-search-gemini.test.ts
T
can1357 7d0807ec36 feat(ai,coding-agent): Google fingerprint hardening
providers/google-gemini-cli:
- parseGeminiCliCredentials() handles legacy, alias (project_id/refresh/expires),
  and enriched credential JSON formats
- shouldRefreshGeminiCliCredentials() + refreshGeminiCliCredentialsIfNeeded()
  proactively refresh OAuth tokens 60s before expiry for both providers
- normalizeAntigravityTools() converts parametersJsonSchema -> parameters
  in function declarations for Antigravity compatibility
- VALIDATED tool calling config applied for Antigravity + Claude model combos
- maxOutputTokens removed from generation config for Antigravity non-Claude models
- Antigravity system instruction injection scoped to Claude + gemini-3-pro-high models
- Antigravity session ID: signed decimal int63 derived from SHA-256 of first user
  message (or random bounded int63), replacing truncated hex hash
- Antigravity requestId uses agent-{uuid}; non-Antigravity requests omit
  requestId/userAgent/requestType from payload
- ANTIGRAVITY_DAILY_ENDPOINT corrected to daily-cloudcode-pa.googleapis.com;
  sandbox kept as fallback
- ANTIGRAVITY_SYSTEM_INSTRUCTION exported

oauth/google-antigravity:
- PKCE removed from OAuth flow (no code_challenge)
- loadCodeAssist metadata ideType changed to ANTIGRAVITY
- discoverProject uses single production endpoint; falls back to onboardUser LRO
  (up to 5 retries, 2s interval) instead of hardcoded default project ID
- ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA exported

oauth/google-gemini-cli:
- PKCE removed from OAuth flow

oauth/index:
- getOAuthApiKey includes refreshToken, expiresAt, email, accountId in
  Gemini/Antigravity JSON payload for proactive refresh support

discovery/antigravity:
- Tries production daily endpoint first, sandbox as fallback
- Removed recommended/agentModelSorts filter; applies denylist instead
- ANTIGRAVITY_DISCOVERY_DENYLIST filters low-quality/internal models
- Request body no longer includes project field

coding-agent:
- gemini_image: corrected responseModalities to uppercase IMAGE/TEXT
- Gemini web search: endpoint fallback (daily->sandbox) with retry on 429/5xx,
  aligned Antigravity request metadata, ANTIGRAVITY_SYSTEM_INSTRUCTION injection
- buildGeminiRequestTools() helper for composable googleSearch/codeExecution/urlContext
- Web search schema: expose max_tokens, temperature, num_search_results params
- Web search: explicit provider falls back to auto chain when unavailable

Tests: google-antigravity-auth, google-gemini-cli-alignment, web-search-gemini
2026-02-28 21:17:57 +01:00

84 lines
2.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, mock, vi } from "bun:test";
import { AgentStorage } from "../../src/session/agent-storage";
import { searchGemini } from "../../src/web/search/providers/gemini";
type CapturedRequest = {
body: Record<string, unknown> | null;
};
const SSE_RESPONSE =
'data: {"response":{"candidates":[{"content":{"role":"model","parts":[{"text":"Gemini answer"}]}}],"modelVersion":"gemini-2.5-flash"}}\n\n';
describe("searchGemini tools serialization", () => {
const originalFetch = globalThis.fetch;
let capturedRequest: CapturedRequest | null = null;
beforeEach(() => {
capturedRequest = null;
vi.spyOn(AgentStorage, "open").mockResolvedValue({
listAuthCredentials: () => [
{
id: 1,
credential: {
type: "oauth",
access: "test-access-token",
expires: Date.now() + 600_000,
projectId: "test-project",
},
},
],
updateAuthCredential: () => undefined,
} as unknown as AgentStorage);
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
capturedRequest = {
body: init?.body ? (JSON.parse(init.body as string) as Record<string, unknown>) : null,
};
return new Response(SSE_RESPONSE, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
}) as unknown as typeof fetch;
});
afterEach(() => {
vi.restoreAllMocks();
globalThis.fetch = originalFetch;
capturedRequest = null;
});
it("sends default googleSearch tool when no passthrough payloads are provided", async () => {
await searchGemini({ query: "default tools" });
expect(capturedRequest).not.toBeNull();
expect(capturedRequest?.body?.request).toMatchObject({
tools: [{ googleSearch: {} }],
});
});
it("passes through google_search payload into googleSearch tool", async () => {
await searchGemini({
query: "google payload",
google_search: { dynamicRetrievalConfig: { mode: "MODE_DYNAMIC" } },
});
expect(capturedRequest).not.toBeNull();
expect(capturedRequest?.body?.request).toMatchObject({
tools: [{ googleSearch: { dynamicRetrievalConfig: { mode: "MODE_DYNAMIC" } } }],
});
});
it("includes codeExecution and urlContext tools when provided", async () => {
await searchGemini({
query: "extended tools",
code_execution: {},
url_context: { allowedDomains: ["example.com"] },
});
expect(capturedRequest).not.toBeNull();
expect(capturedRequest?.body?.request).toMatchObject({
tools: [{ googleSearch: {} }, { codeExecution: {} }, { urlContext: { allowedDomains: ["example.com"] } }],
});
});
});