clanker-agent/packages/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts
Harivansh Rathi 0250f72976 move pi-mono into companion-cloud as apps/companion-os
- Copy all pi-mono source into apps/companion-os/
- Update Dockerfile to COPY pre-built binary instead of downloading from GitHub Releases
- Update deploy-staging.yml to build pi from source (bun compile) before Docker build
- Add apps/companion-os/** to path triggers
- No more cross-repo dispatch needed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:22:50 -08:00

195 lines
5.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { convertMessages } from "../src/providers/google-shared.js";
import type { Context, Model } from "../src/types.js";
const SKIP_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
function makeGemini3Model(
id = "gemini-3-pro-preview",
): Model<"google-generative-ai"> {
return {
id,
name: "Gemini 3 Pro Preview",
api: "google-generative-ai",
provider: "google",
baseUrl: "https://generativelanguage.googleapis.com",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
};
}
describe("google-shared convertMessages — Gemini 3 unsigned tool calls", () => {
it("uses skip_thought_signature_validator for unsigned tool calls on Gemini 3", () => {
const model = makeGemini3Model();
const now = Date.now();
const context: Context = {
messages: [
{ role: "user", content: "Hi", timestamp: now },
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_1",
name: "bash",
arguments: { command: "ls -la" },
// No thoughtSignature: simulates Claude via Antigravity.
},
],
api: "google-gemini-cli",
provider: "google-antigravity",
model: "claude-sonnet-4-20250514",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
},
stopReason: "stop",
timestamp: now,
},
],
};
const contents = convertMessages(model, context);
const modelTurn = contents.find((c) => c.role === "model");
expect(modelTurn).toBeTruthy();
// Should be a structured functionCall, NOT text fallback
const fcPart = modelTurn?.parts?.find((p) => p.functionCall !== undefined);
expect(fcPart).toBeTruthy();
expect(fcPart?.functionCall?.name).toBe("bash");
expect(fcPart?.functionCall?.args).toEqual({ command: "ls -la" });
expect(fcPart?.thoughtSignature).toBe(SKIP_THOUGHT_SIGNATURE);
// No text fallback should exist
const textParts =
modelTurn?.parts?.filter((p) => p.text !== undefined) ?? [];
const historicalText = textParts.filter((p) =>
p.text?.includes("Historical context"),
);
expect(historicalText).toHaveLength(0);
});
it("preserves valid thoughtSignature when present (same provider/model)", () => {
const model = makeGemini3Model();
const now = Date.now();
// Valid base64 signature (16 bytes = 24 chars base64)
const validSig = "AAAAAAAAAAAAAAAAAAAAAA==";
const context: Context = {
messages: [
{ role: "user", content: "Hi", timestamp: now },
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_1",
name: "bash",
arguments: { command: "echo hi" },
thoughtSignature: validSig,
},
],
api: "google-generative-ai",
provider: "google",
model: "gemini-3-pro-preview",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
},
stopReason: "stop",
timestamp: now,
},
],
};
const contents = convertMessages(model, context);
const modelTurn = contents.find((c) => c.role === "model");
const fcPart = modelTurn?.parts?.find((p) => p.functionCall !== undefined);
expect(fcPart).toBeTruthy();
expect(fcPart?.thoughtSignature).toBe(validSig);
});
it("does not add sentinel for non-Gemini-3 models", () => {
const model: Model<"google-generative-ai"> = {
id: "gemini-2.5-flash",
name: "Gemini 2.5 Flash",
api: "google-generative-ai",
provider: "google",
baseUrl: "https://generativelanguage.googleapis.com",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
};
const now = Date.now();
const context: Context = {
messages: [
{ role: "user", content: "Hi", timestamp: now },
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call_1",
name: "bash",
arguments: { command: "ls" },
// No thoughtSignature
},
],
api: "google-gemini-cli",
provider: "google-antigravity",
model: "claude-sonnet-4-20250514",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
},
stopReason: "stop",
timestamp: now,
},
],
};
const contents = convertMessages(model, context);
const modelTurn = contents.find((c) => c.role === "model");
const fcPart = modelTurn?.parts?.find((p) => p.functionCall !== undefined);
expect(fcPart).toBeTruthy();
// No sentinel, no thoughtSignature at all
expect(fcPart?.thoughtSignature).toBeUndefined();
});
});