@@ -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,95 @@ 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 ( true ) ;
742+ expect (
743+ hasGoogleVertexAuthorizedUserAdcSync ( {
744+ HOME : path . join ( tempDir , "empty-home" ) ,
745+ K_SERVICE : "cloud-run-service" ,
746+ } ) ,
747+ ) . toBe ( true ) ;
748+ } ) ;
749+
750+ it ( "resolves non-file Vertex ADC through google-auth-library without OAuth refresh fetch" , async ( ) => {
751+ const tempDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-google-vertex-authlib-" ) ) ;
752+ vi . stubEnv ( "GOOGLE_APPLICATION_CREDENTIALS" , "" ) ;
753+ vi . stubEnv ( "HOME" , path . join ( tempDir , "home" ) ) ;
754+ vi . stubEnv ( "APPDATA" , "" ) ;
755+ googleAuthGetAccessTokenMock . mockResolvedValueOnce ( "ya29.google-auth-token" ) ;
756+ const tokenFetchMock = vi . fn ( ) ;
757+
758+ await expect ( resolveGoogleVertexAuthorizedUserHeaders ( tokenFetchMock ) ) . resolves . toEqual ( {
759+ Authorization : "Bearer ya29.google-auth-token" ,
760+ } ) ;
761+
762+ expect ( googleAuthMock ) . toHaveBeenCalledWith ( {
763+ scopes : [ "https://www.googleapis.com/auth/cloud-platform" ] ,
764+ } ) ;
765+ expect ( googleAuthGetAccessTokenMock ) . toHaveBeenCalledTimes ( 1 ) ;
766+ expect ( tokenFetchMock ) . not . toHaveBeenCalled ( ) ;
767+ } ) ;
768+
769+ it ( "uses google-auth-library bearer auth for Google Vertex credential marker requests" , async ( ) => {
770+ const tempDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-google-vertex-authlib-stream-" ) ) ;
771+ vi . stubEnv ( "GOOGLE_APPLICATION_CREDENTIALS" , "" ) ;
772+ vi . stubEnv ( "HOME" , path . join ( tempDir , "home" ) ) ;
773+ vi . stubEnv ( "APPDATA" , "" ) ;
774+ vi . stubEnv ( "GOOGLE_CLOUD_PROJECT" , "vertex-project" ) ;
775+ vi . stubEnv ( "GOOGLE_CLOUD_LOCATION" , "us-central1" ) ;
776+ googleAuthGetAccessTokenMock . mockResolvedValueOnce ( "ya29.transport-token" ) ;
777+ const tokenFetchMock = vi . fn ( ) ;
778+ guardedFetchMock . mockResolvedValueOnce (
779+ buildSseResponse ( [
780+ {
781+ candidates : [ { content : { parts : [ { text : "ok" } ] } , finishReason : "STOP" } ] ,
782+ } ,
783+ ] ) ,
784+ ) ;
785+
786+ const streamFn = createGoogleVertexTransportStreamFn ( ) ;
787+ const stream = await Promise . resolve (
788+ streamFn (
789+ buildGoogleVertexModel ( ) ,
790+ {
791+ messages : [ { role : "user" , content : "hello" , timestamp : 0 } ] ,
792+ } as Parameters < typeof streamFn > [ 1 ] ,
793+ {
794+ apiKey : "gcp-vertex-credentials" ,
795+ fetch : tokenFetchMock ,
796+ } as Parameters < typeof streamFn > [ 2 ] ,
797+ ) ,
798+ ) ;
799+ await stream . result ( ) ;
800+
801+ expect ( tokenFetchMock ) . not . toHaveBeenCalled ( ) ;
802+ const guardedCall = requireMockCall ( guardedFetchMock , 0 , "guarded fetch" ) ;
803+ const guardedInit = requireRequestInit ( guardedCall , "guarded fetch" ) ;
804+ expectHeaders ( guardedInit , {
805+ Authorization : "Bearer ya29.transport-token" ,
806+ "Content-Type" : "application/json" ,
807+ accept : "text/event-stream" ,
808+ } ) ;
809+ expect ( new Headers ( guardedInit . headers ) . has ( "x-goog-api-key" ) ) . toBe ( false ) ;
810+ } ) ;
811+
698812 it ( "refreshes authorized_user ADC before Google Vertex requests" , async ( ) => {
699813 const tempDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-google-vertex-adc-" ) ) ;
700814 const credentialsPath = path . join ( tempDir , "application_default_credentials.json" ) ;
0 commit comments