11import { describe , it , expect } from "vitest" ;
22import {
33 prepareAntigravityRequest ,
4+ transformAntigravityResponse ,
45 getPluginSessionId ,
56 isGenerativeLanguageRequest ,
67 __testExports ,
@@ -517,6 +518,31 @@ describe("request.ts", () => {
517518 } ) ;
518519
519520 describe ( "prepareAntigravityRequest" , ( ) => {
521+ it ( "copies thoughtSignature to all functionCall parts in the same turn" , ( ) => {
522+ const mockPayload = {
523+ contents : [
524+ {
525+ role : "model" ,
526+ parts : [
527+ { functionCall : { name : "foo" , args : { } } , thoughtSignature : "signature-123" } ,
528+ { functionCall : { name : "bar" , args : { } } }
529+ ]
530+ }
531+ ]
532+ } ;
533+
534+ const result = prepareAntigravityRequest (
535+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro:generateContent" ,
536+ { method : "POST" , body : JSON . stringify ( mockPayload ) } ,
537+ "test-token" ,
538+ "test-project"
539+ ) ;
540+
541+ const parsedBody = JSON . parse ( result . init . body as string ) ;
542+ expect ( parsedBody . request . contents [ 0 ] . parts [ 0 ] . thoughtSignature ) . toBe ( "signature-123" ) ;
543+ expect ( parsedBody . request . contents [ 0 ] . parts [ 1 ] . thoughtSignature ) . toBe ( "signature-123" ) ;
544+ } ) ;
545+
520546 const mockAccessToken = "test-token" ;
521547 const mockProjectId = "test-project" ;
522548
@@ -731,6 +757,61 @@ it("removes x-api-key header", () => {
731757 expect ( result . streaming ) . toBe ( false ) ;
732758 } ) ;
733759
760+ it ( "removes contents entries with empty or invalid parts" , ( ) => {
761+ const result = prepareAntigravityRequest (
762+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent" ,
763+ {
764+ method : "POST" ,
765+ body : JSON . stringify ( {
766+ contents : [
767+ { role : "user" , parts : [ ] } ,
768+ { role : "model" , parts : [ null , { text : "kept" } ] } ,
769+ { role : "user" , parts : null } ,
770+ ] ,
771+ systemInstruction : {
772+ role : "user" ,
773+ parts : [ null , { text : "system kept" } ] ,
774+ } ,
775+ } ) ,
776+ } ,
777+ mockAccessToken ,
778+ mockProjectId ,
779+ undefined ,
780+ "gemini-cli" ,
781+ ) ;
782+
783+ const wrapped = JSON . parse ( result . init . body as string ) ;
784+ expect ( wrapped . request . contents ) . toHaveLength ( 1 ) ;
785+ expect ( wrapped . request . contents [ 0 ] ) . toEqual ( {
786+ role : "model" ,
787+ parts : [ { text : "kept" } ] ,
788+ } ) ;
789+ expect ( wrapped . request . systemInstruction . parts ) . toEqual ( [ { text : "system kept" } ] ) ;
790+ } ) ;
791+
792+ it ( "drops systemInstruction when all parts are invalid" , ( ) => {
793+ const result = prepareAntigravityRequest (
794+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent" ,
795+ {
796+ method : "POST" ,
797+ body : JSON . stringify ( {
798+ contents : [ { role : "user" , parts : [ { text : "hi" } ] } ] ,
799+ systemInstruction : {
800+ role : "user" ,
801+ parts : [ null ] ,
802+ } ,
803+ } ) ,
804+ } ,
805+ mockAccessToken ,
806+ mockProjectId ,
807+ undefined ,
808+ "gemini-cli" ,
809+ ) ;
810+
811+ const wrapped = JSON . parse ( result . init . body as string ) ;
812+ expect ( wrapped . request . systemInstruction ) . toBeUndefined ( ) ;
813+ } ) ;
814+
734815 it ( "preserves headerStyle in response" , ( ) => {
735816 const result = prepareAntigravityRequest (
736817 "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent" ,
@@ -768,6 +849,30 @@ it("removes x-api-key header", () => {
768849 expect ( result . effectiveModel ) . toBe ( "gemini-3-pro-low" ) ;
769850 } ) ;
770851
852+ it ( "transforms gemini-3.1-pro-preview to gemini-3.1-pro-low for antigravity headerStyle" , ( ) => {
853+ const result = prepareAntigravityRequest (
854+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview:generateContent" ,
855+ { method : "POST" , body : JSON . stringify ( { contents : [ ] } ) } ,
856+ mockAccessToken ,
857+ mockProjectId ,
858+ undefined ,
859+ "antigravity"
860+ ) ;
861+ expect ( result . effectiveModel ) . toBe ( "gemini-3.1-pro-low" ) ;
862+ } ) ;
863+
864+ it ( "transforms gemini-3.1-pro-preview-customtools to gemini-3.1-pro-low for antigravity headerStyle" , ( ) => {
865+ const result = prepareAntigravityRequest (
866+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview-customtools:generateContent" ,
867+ { method : "POST" , body : JSON . stringify ( { contents : [ ] } ) } ,
868+ mockAccessToken ,
869+ mockProjectId ,
870+ undefined ,
871+ "antigravity"
872+ ) ;
873+ expect ( result . effectiveModel ) . toBe ( "gemini-3.1-pro-low" ) ;
874+ } ) ;
875+
771876 it ( "transforms gemini-3-flash to gemini-3-flash-preview for gemini-cli headerStyle" , ( ) => {
772877 const result = prepareAntigravityRequest (
773878 "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash:generateContent" ,
@@ -792,6 +897,30 @@ it("removes x-api-key header", () => {
792897 expect ( result . effectiveModel ) . toBe ( "gemini-3-pro-preview" ) ;
793898 } ) ;
794899
900+ it ( "transforms gemini-3.1-pro-low to gemini-3.1-pro-preview for gemini-cli headerStyle" , ( ) => {
901+ const result = prepareAntigravityRequest (
902+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-low:generateContent" ,
903+ { method : "POST" , body : JSON . stringify ( { contents : [ ] } ) } ,
904+ mockAccessToken ,
905+ mockProjectId ,
906+ undefined ,
907+ "gemini-cli"
908+ ) ;
909+ expect ( result . effectiveModel ) . toBe ( "gemini-3.1-pro-preview" ) ;
910+ } ) ;
911+
912+ it ( "keeps gemini-3.1-pro-preview-customtools unchanged for gemini-cli headerStyle" , ( ) => {
913+ const result = prepareAntigravityRequest (
914+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview-customtools:generateContent" ,
915+ { method : "POST" , body : JSON . stringify ( { contents : [ ] } ) } ,
916+ mockAccessToken ,
917+ mockProjectId ,
918+ undefined ,
919+ "gemini-cli"
920+ ) ;
921+ expect ( result . effectiveModel ) . toBe ( "gemini-3.1-pro-preview-customtools" ) ;
922+ } ) ;
923+
795924 it ( "keeps non-Gemini-3 models unchanged regardless of headerStyle" , ( ) => {
796925 const result = prepareAntigravityRequest (
797926 "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent" ,
@@ -805,4 +934,66 @@ it("removes x-api-key header", () => {
805934 } ) ;
806935 } ) ;
807936 } ) ;
937+
938+ describe ( "transformAntigravityResponse" , ( ) => {
939+ it ( "does not misclassify generic INVALID_ARGUMENT as thinking recovery from debug metadata" , async ( ) => {
940+ const response = new Response (
941+ JSON . stringify ( {
942+ error : {
943+ code : 400 ,
944+ message : "Request contains an invalid argument." ,
945+ status : "INVALID_ARGUMENT" ,
946+ } ,
947+ } ) ,
948+ {
949+ status : 400 ,
950+ headers : { "content-type" : "application/json" } ,
951+ } ,
952+ ) ;
953+
954+ const transformed = await transformAntigravityResponse (
955+ response ,
956+ true ,
957+ undefined ,
958+ "antigravity-claude-opus-4-6-thinking" ,
959+ "test-project" ,
960+ "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:streamGenerateContent?alt=sse" ,
961+ "claude-opus-4-6-thinking" ,
962+ "session-1" ,
963+ 0 ,
964+ "expected=1 found=0" ,
965+ ) ;
966+
967+ await expect ( transformed . text ( ) ) . resolves . toContain ( "Request contains an invalid argument." ) ;
968+ } ) ;
969+
970+ it ( "rethrows THINKING_RECOVERY_NEEDED for outer retry handling" , async ( ) => {
971+ const response = new Response (
972+ JSON . stringify ( {
973+ error : {
974+ code : 400 ,
975+ message : "Thinking must start with a thinking block before tool use." ,
976+ status : "INVALID_ARGUMENT" ,
977+ } ,
978+ } ) ,
979+ {
980+ status : 400 ,
981+ headers : { "content-type" : "application/json" } ,
982+ } ,
983+ ) ;
984+
985+ await expect (
986+ transformAntigravityResponse (
987+ response ,
988+ true ,
989+ undefined ,
990+ "antigravity-claude-opus-4-6-thinking" ,
991+ "test-project" ,
992+ "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:streamGenerateContent?alt=sse" ,
993+ "claude-opus-4-6-thinking" ,
994+ "session-1" ,
995+ ) ,
996+ ) . rejects . toMatchObject ( { message : "THINKING_RECOVERY_NEEDED" } ) ;
997+ } ) ;
998+ } ) ;
808999} ) ;
0 commit comments