@@ -74,6 +74,39 @@ describe("resolveProviderAuths key normalization", () => {
7474 ) ;
7575 }
7676
77+ async function writeConfig ( home : string , config : Record < string , unknown > ) {
78+ const stateDir = path . join ( home , ".openclaw" ) ;
79+ await fs . mkdir ( stateDir , { recursive : true } ) ;
80+ await fs . writeFile (
81+ path . join ( stateDir , "openclaw.json" ) ,
82+ `${ JSON . stringify ( config , null , 2 ) } \n` ,
83+ "utf8" ,
84+ ) ;
85+ }
86+
87+ async function writeProfileOrder ( home : string , provider : string , profileIds : string [ ] ) {
88+ const agentDir = path . join ( home , ".openclaw" , "agents" , "main" , "agent" ) ;
89+ const parsed = JSON . parse (
90+ await fs . readFile ( path . join ( agentDir , "auth-profiles.json" ) , "utf8" ) ,
91+ ) as Record < string , unknown > ;
92+ const order = ( parsed . order && typeof parsed . order === "object" ? parsed . order : { } ) as Record <
93+ string ,
94+ unknown
95+ > ;
96+ order [ provider ] = profileIds ;
97+ parsed . order = order ;
98+ await fs . writeFile (
99+ path . join ( agentDir , "auth-profiles.json" ) ,
100+ `${ JSON . stringify ( parsed , null , 2 ) } \n` ,
101+ ) ;
102+ }
103+
104+ async function writeLegacyPiAuth ( home : string , raw : string ) {
105+ const legacyDir = path . join ( home , ".pi" , "agent" ) ;
106+ await fs . mkdir ( legacyDir , { recursive : true } ) ;
107+ await fs . writeFile ( path . join ( legacyDir , "auth.json" ) , raw , "utf8" ) ;
108+ }
109+
77110 it ( "strips embedded CR/LF from env keys" , async ( ) => {
78111 await withSuiteHome (
79112 async ( ) => {
@@ -144,12 +177,9 @@ describe("resolveProviderAuths key normalization", () => {
144177 it ( "falls back to legacy .pi auth file for zai keys" , async ( ) => {
145178 await withSuiteHome (
146179 async ( home ) => {
147- const legacyDir = path . join ( home , ".pi" , "agent" ) ;
148- await fs . mkdir ( legacyDir , { recursive : true } ) ;
149- await fs . writeFile (
150- path . join ( legacyDir , "auth.json" ) ,
180+ await writeLegacyPiAuth (
181+ home ,
151182 `${ JSON . stringify ( { "z-ai" : { access : "legacy-zai-key" } } , null , 2 ) } \n` ,
152- "utf8" ,
153183 ) ;
154184
155185 const auths = await resolveProviderAuths ( {
@@ -180,4 +210,195 @@ describe("resolveProviderAuths key normalization", () => {
180210 expect ( auths ) . toEqual ( [ { provider : "google-gemini-cli" , token : "google-oauth-token" } ] ) ;
181211 } , { } ) ;
182212 } ) ;
213+
214+ it ( "keeps raw google token when token payload is not JSON" , async ( ) => {
215+ await withSuiteHome ( async ( home ) => {
216+ await writeAuthProfiles ( home , {
217+ "google-antigravity:default" : {
218+ type : "token" ,
219+ provider : "google-antigravity" ,
220+ token : "plain-google-token" ,
221+ } ,
222+ } ) ;
223+
224+ const auths = await resolveProviderAuths ( {
225+ providers : [ "google-antigravity" ] ,
226+ } ) ;
227+ expect ( auths ) . toEqual ( [ { provider : "google-antigravity" , token : "plain-google-token" } ] ) ;
228+ } , { } ) ;
229+ } ) ;
230+
231+ it ( "uses config api keys when env and profiles are missing" , async ( ) => {
232+ await withSuiteHome (
233+ async ( home ) => {
234+ const modelDef = {
235+ id : "test-model" ,
236+ name : "Test Model" ,
237+ reasoning : false ,
238+ input : [ "text" ] ,
239+ cost : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 } ,
240+ contextWindow : 1024 ,
241+ maxTokens : 256 ,
242+ } ;
243+ await writeConfig ( home , {
244+ models : {
245+ providers : {
246+ zai : {
247+ baseUrl : "https://api.z.ai" ,
248+ models : [ modelDef ] ,
249+ apiKey : "cfg-zai-key" ,
250+ } ,
251+ minimax : {
252+ baseUrl : "https://api.minimaxi.com" ,
253+ models : [ modelDef ] ,
254+ apiKey : "cfg-minimax-key" ,
255+ } ,
256+ xiaomi : {
257+ baseUrl : "https://api.xiaomi.example" ,
258+ models : [ modelDef ] ,
259+ apiKey : "cfg-xiaomi-key" ,
260+ } ,
261+ } ,
262+ } ,
263+ } ) ;
264+
265+ const auths = await resolveProviderAuths ( {
266+ providers : [ "zai" , "minimax" , "xiaomi" ] ,
267+ } ) ;
268+ expect ( auths ) . toEqual ( [
269+ { provider : "zai" , token : "cfg-zai-key" } ,
270+ { provider : "minimax" , token : "cfg-minimax-key" } ,
271+ { provider : "xiaomi" , token : "cfg-xiaomi-key" } ,
272+ ] ) ;
273+ } ,
274+ {
275+ ZAI_API_KEY : undefined ,
276+ Z_AI_API_KEY : undefined ,
277+ MINIMAX_API_KEY : undefined ,
278+ MINIMAX_CODE_PLAN_KEY : undefined ,
279+ XIAOMI_API_KEY : undefined ,
280+ } ,
281+ ) ;
282+ } ) ;
283+
284+ it ( "returns no auth when providers have no configured credentials" , async ( ) => {
285+ await withSuiteHome (
286+ async ( ) => {
287+ const auths = await resolveProviderAuths ( {
288+ providers : [ "zai" , "minimax" , "xiaomi" ] ,
289+ } ) ;
290+ expect ( auths ) . toEqual ( [ ] ) ;
291+ } ,
292+ {
293+ ZAI_API_KEY : undefined ,
294+ Z_AI_API_KEY : undefined ,
295+ MINIMAX_API_KEY : undefined ,
296+ MINIMAX_CODE_PLAN_KEY : undefined ,
297+ XIAOMI_API_KEY : undefined ,
298+ } ,
299+ ) ;
300+ } ) ;
301+
302+ it ( "uses zai api_key auth profiles when env and config are missing" , async ( ) => {
303+ await withSuiteHome (
304+ async ( home ) => {
305+ await writeAuthProfiles ( home , {
306+ "zai:default" : { type : "api_key" , provider : "zai" , key : "profile-zai-key" } ,
307+ } ) ;
308+
309+ const auths = await resolveProviderAuths ( {
310+ providers : [ "zai" ] ,
311+ } ) ;
312+ expect ( auths ) . toEqual ( [ { provider : "zai" , token : "profile-zai-key" } ] ) ;
313+ } ,
314+ {
315+ ZAI_API_KEY : undefined ,
316+ Z_AI_API_KEY : undefined ,
317+ } ,
318+ ) ;
319+ } ) ;
320+
321+ it ( "ignores invalid legacy z-ai auth files" , async ( ) => {
322+ await withSuiteHome (
323+ async ( home ) => {
324+ await writeLegacyPiAuth ( home , "{not-json" ) ;
325+ const auths = await resolveProviderAuths ( {
326+ providers : [ "zai" ] ,
327+ } ) ;
328+ expect ( auths ) . toEqual ( [ ] ) ;
329+ } ,
330+ {
331+ ZAI_API_KEY : undefined ,
332+ Z_AI_API_KEY : undefined ,
333+ } ,
334+ ) ;
335+ } ) ;
336+
337+ it ( "discovers oauth provider from config but skips mismatched profile providers" , async ( ) => {
338+ await withSuiteHome ( async ( home ) => {
339+ await writeConfig ( home , {
340+ auth : {
341+ profiles : {
342+ "anthropic:default" : { provider : "anthropic" , mode : "token" } ,
343+ } ,
344+ } ,
345+ } ) ;
346+ await writeAuthProfiles ( home , {
347+ "anthropic:default" : {
348+ type : "token" ,
349+ provider : "zai" ,
350+ token : "mismatched-provider-token" ,
351+ } ,
352+ } ) ;
353+
354+ const auths = await resolveProviderAuths ( {
355+ providers : [ "anthropic" ] ,
356+ } ) ;
357+ expect ( auths ) . toEqual ( [ ] ) ;
358+ } , { } ) ;
359+ } ) ;
360+
361+ it ( "skips providers without oauth-compatible profiles" , async ( ) => {
362+ await withSuiteHome ( async ( ) => {
363+ const auths = await resolveProviderAuths ( {
364+ providers : [ "anthropic" ] ,
365+ } ) ;
366+ expect ( auths ) . toEqual ( [ ] ) ;
367+ } , { } ) ;
368+ } ) ;
369+
370+ it ( "skips oauth profiles that resolve without an api key and uses later profiles" , async ( ) => {
371+ await withSuiteHome ( async ( home ) => {
372+ await writeAuthProfiles ( home , {
373+ "anthropic:empty" : {
374+ type : "token" ,
375+ provider : "anthropic" ,
376+ token : "expired-token" ,
377+ expires : Date . now ( ) - 60_000 ,
378+ } ,
379+ "anthropic:valid" : { type : "token" , provider : "anthropic" , token : "anthropic-token" } ,
380+ } ) ;
381+ await writeProfileOrder ( home , "anthropic" , [ "anthropic:empty" , "anthropic:valid" ] ) ;
382+
383+ const auths = await resolveProviderAuths ( {
384+ providers : [ "anthropic" ] ,
385+ } ) ;
386+ expect ( auths ) . toEqual ( [ { provider : "anthropic" , token : "anthropic-token" } ] ) ;
387+ } , { } ) ;
388+ } ) ;
389+
390+ it ( "skips api_key entries in oauth token resolution order" , async ( ) => {
391+ await withSuiteHome ( async ( home ) => {
392+ await writeAuthProfiles ( home , {
393+ "anthropic:api" : { type : "api_key" , provider : "anthropic" , key : "api-key-1" } ,
394+ "anthropic:token" : { type : "token" , provider : "anthropic" , token : "token-1" } ,
395+ } ) ;
396+ await writeProfileOrder ( home , "anthropic" , [ "anthropic:api" , "anthropic:token" ] ) ;
397+
398+ const auths = await resolveProviderAuths ( {
399+ providers : [ "anthropic" ] ,
400+ } ) ;
401+ expect ( auths ) . toEqual ( [ { provider : "anthropic" , token : "token-1" } ] ) ;
402+ } , { } ) ;
403+ } ) ;
183404} ) ;
0 commit comments