@@ -4,21 +4,40 @@ import path from "node:path";
44import type { Model } from "@earendil-works/pi-ai" ;
55import { afterAll , afterEach , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
66
7- const { buildGuardedModelFetchMock, guardedFetchMock } = vi . hoisted ( ( ) => ( {
8- buildGuardedModelFetchMock : vi . fn ( ) ,
9- guardedFetchMock : vi . fn ( ) ,
10- } ) ) ;
7+ const {
8+ buildGuardedModelFetchMock,
9+ guardedFetchMock,
10+ googleAuthGetAccessTokenMock,
11+ googleAuthMock,
12+ } = vi . hoisted ( ( ) => {
13+ const googleAuthGetAccessTokenMock = vi . fn ( ) ;
14+ return {
15+ buildGuardedModelFetchMock : vi . fn ( ) ,
16+ guardedFetchMock : vi . fn ( ) ,
17+ googleAuthGetAccessTokenMock,
18+ googleAuthMock : vi . fn ( function GoogleAuthMock ( ) {
19+ return {
20+ getAccessToken : googleAuthGetAccessTokenMock ,
21+ } ;
22+ } ) ,
23+ } ;
24+ } ) ;
1125
1226vi . mock ( "openclaw/plugin-sdk/provider-transport-runtime" , async ( importOriginal ) => ( {
1327 ...( await importOriginal ( ) ) ,
1428 buildGuardedModelFetch : buildGuardedModelFetchMock ,
1529} ) ) ;
1630
31+ vi . mock ( "google-auth-library" , ( ) => ( {
32+ GoogleAuth : googleAuthMock ,
33+ } ) ) ;
34+
1735let buildGoogleGenerativeAiParams : typeof import ( "./transport-stream.js" ) . buildGoogleGenerativeAiParams ;
1836let buildGoogleGemini3FirstResponseRetryParams : typeof import ( "./transport-stream.js" ) . buildGoogleGemini3FirstResponseRetryParams ;
1937let createGoogleGenerativeAiTransportStreamFn : typeof import ( "./transport-stream.js" ) . createGoogleGenerativeAiTransportStreamFn ;
2038let createGoogleVertexTransportStreamFn : typeof import ( "./transport-stream.js" ) . createGoogleVertexTransportStreamFn ;
2139let hasGoogleVertexAuthorizedUserAdcSync : typeof import ( "./vertex-adc.js" ) . hasGoogleVertexAuthorizedUserAdcSync ;
40+ let resolveGoogleVertexAuthorizedUserHeaders : typeof import ( "./vertex-adc.js" ) . resolveGoogleVertexAuthorizedUserHeaders ;
2241let resetGoogleVertexAuthorizedUserTokenCacheForTest : typeof import ( "./vertex-adc.js" ) . resetGoogleVertexAuthorizedUserTokenCacheForTest ;
2342
2443const MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL = Symbol . for (
@@ -254,13 +273,18 @@ describe("google transport stream", () => {
254273 createGoogleGenerativeAiTransportStreamFn,
255274 createGoogleVertexTransportStreamFn,
256275 } = await import ( "./transport-stream.js" ) ) ;
257- ( { hasGoogleVertexAuthorizedUserAdcSync, resetGoogleVertexAuthorizedUserTokenCacheForTest } =
258- await import ( "./vertex-adc.js" ) ) ;
276+ ( {
277+ hasGoogleVertexAuthorizedUserAdcSync,
278+ resolveGoogleVertexAuthorizedUserHeaders,
279+ resetGoogleVertexAuthorizedUserTokenCacheForTest,
280+ } = await import ( "./vertex-adc.js" ) ) ;
259281 } ) ;
260282
261283 beforeEach ( ( ) => {
262284 buildGuardedModelFetchMock . mockReset ( ) ;
263285 guardedFetchMock . mockReset ( ) ;
286+ googleAuthGetAccessTokenMock . mockReset ( ) ;
287+ googleAuthMock . mockClear ( ) ;
264288 buildGuardedModelFetchMock . mockReturnValue ( guardedFetchMock ) ;
265289 resetGoogleVertexAuthorizedUserTokenCacheForTest ( ) ;
266290 } ) ;
@@ -271,6 +295,7 @@ describe("google transport stream", () => {
271295
272296 afterAll ( ( ) => {
273297 vi . doUnmock ( "openclaw/plugin-sdk/provider-transport-runtime" ) ;
298+ vi . doUnmock ( "google-auth-library" ) ;
274299 vi . resetModules ( ) ;
275300 } ) ;
276301
@@ -695,6 +720,89 @@ describe("google transport stream", () => {
695720 } ) ;
696721 } ) ;
697722
723+ it ( "detects supported Vertex ADC sources synchronously" , async ( ) => {
724+ const tempDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-google-vertex-adc-detect-" ) ) ;
725+ for ( const type of [ "authorized_user" , "external_account" , "service_account" ] ) {
726+ const credentialsPath = path . join ( tempDir , `${ type } .json` ) ;
727+ await writeFile ( credentialsPath , JSON . stringify ( { type } ) , "utf8" ) ;
728+
729+ expect (
730+ hasGoogleVertexAuthorizedUserAdcSync ( {
731+ GOOGLE_APPLICATION_CREDENTIALS : credentialsPath ,
732+ } ) ,
733+ ) . toBe ( true ) ;
734+ }
735+
736+ expect (
737+ hasGoogleVertexAuthorizedUserAdcSync ( {
738+ HOME : path . join ( tempDir , "empty-home" ) ,
739+ KUBERNETES_SERVICE_HOST : "10.0.0.1" ,
740+ } ) ,
741+ ) . toBe ( false ) ;
742+ } ) ;
743+
744+ it ( "resolves non-file Vertex ADC through google-auth-library without OAuth refresh fetch" , async ( ) => {
745+ const tempDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-google-vertex-authlib-" ) ) ;
746+ vi . stubEnv ( "GOOGLE_APPLICATION_CREDENTIALS" , "" ) ;
747+ vi . stubEnv ( "HOME" , path . join ( tempDir , "home" ) ) ;
748+ vi . stubEnv ( "APPDATA" , "" ) ;
749+ googleAuthGetAccessTokenMock . mockResolvedValueOnce ( "ya29.google-auth-token" ) ;
750+ const tokenFetchMock = vi . fn ( ) ;
751+
752+ await expect ( resolveGoogleVertexAuthorizedUserHeaders ( tokenFetchMock ) ) . resolves . toEqual ( {
753+ Authorization : "Bearer ya29.google-auth-token" ,
754+ } ) ;
755+
756+ expect ( googleAuthMock ) . toHaveBeenCalledWith ( {
757+ scopes : [ "https://www.googleapis.com/auth/cloud-platform" ] ,
758+ } ) ;
759+ expect ( googleAuthGetAccessTokenMock ) . toHaveBeenCalledTimes ( 1 ) ;
760+ expect ( tokenFetchMock ) . not . toHaveBeenCalled ( ) ;
761+ } ) ;
762+
763+ it ( "uses google-auth-library bearer auth for Google Vertex credential marker requests" , async ( ) => {
764+ const tempDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-google-vertex-authlib-stream-" ) ) ;
765+ vi . stubEnv ( "GOOGLE_APPLICATION_CREDENTIALS" , "" ) ;
766+ vi . stubEnv ( "HOME" , path . join ( tempDir , "home" ) ) ;
767+ vi . stubEnv ( "APPDATA" , "" ) ;
768+ vi . stubEnv ( "GOOGLE_CLOUD_PROJECT" , "vertex-project" ) ;
769+ vi . stubEnv ( "GOOGLE_CLOUD_LOCATION" , "us-central1" ) ;
770+ googleAuthGetAccessTokenMock . mockResolvedValueOnce ( "ya29.transport-token" ) ;
771+ const tokenFetchMock = vi . fn ( ) ;
772+ guardedFetchMock . mockResolvedValueOnce (
773+ buildSseResponse ( [
774+ {
775+ candidates : [ { content : { parts : [ { text : "ok" } ] } , finishReason : "STOP" } ] ,
776+ } ,
777+ ] ) ,
778+ ) ;
779+
780+ const streamFn = createGoogleVertexTransportStreamFn ( ) ;
781+ const stream = await Promise . resolve (
782+ streamFn (
783+ buildGoogleVertexModel ( ) ,
784+ {
785+ messages : [ { role : "user" , content : "hello" , timestamp : 0 } ] ,
786+ } as Parameters < typeof streamFn > [ 1 ] ,
787+ {
788+ apiKey : "gcp-vertex-credentials" ,
789+ fetch : tokenFetchMock ,
790+ } as Parameters < typeof streamFn > [ 2 ] ,
791+ ) ,
792+ ) ;
793+ await stream . result ( ) ;
794+
795+ expect ( tokenFetchMock ) . not . toHaveBeenCalled ( ) ;
796+ const guardedCall = requireMockCall ( guardedFetchMock , 0 , "guarded fetch" ) ;
797+ const guardedInit = requireRequestInit ( guardedCall , "guarded fetch" ) ;
798+ expectHeaders ( guardedInit , {
799+ Authorization : "Bearer ya29.transport-token" ,
800+ "Content-Type" : "application/json" ,
801+ accept : "text/event-stream" ,
802+ } ) ;
803+ expect ( new Headers ( guardedInit . headers ) . has ( "x-goog-api-key" ) ) . toBe ( false ) ;
804+ } ) ;
805+
698806 it ( "refreshes authorized_user ADC before Google Vertex requests" , async ( ) => {
699807 const tempDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-google-vertex-adc-" ) ) ;
700808 const credentialsPath = path . join ( tempDir , "application_default_credentials.json" ) ;
0 commit comments