@@ -82,7 +82,7 @@ function minimalEnv(): Record<string, string | undefined> {
8282}
8383
8484describe ( 'generateBaseConfig' , ( ) => {
85- it ( 'generates config with gateway and exec defaults, no kilocode provider entry' , ( ) => {
85+ it ( 'generates config with gateway, exec defaults, and a kilocode provider entry that triggers live discovery ' , ( ) => {
8686 const { deps } = fakeDeps ( ) ;
8787 const config = generateBaseConfig ( minimalEnv ( ) , '/tmp/openclaw.json' , deps ) ;
8888
@@ -93,8 +93,11 @@ describe('generateBaseConfig', () => {
9393 expect ( config . gateway . auth . token ) . toBe ( 'test-gw-token' ) ;
9494 expect ( config . gateway . controlUi . allowInsecureAuth ) . toBe ( true ) ;
9595
96- // No kilocode provider entry in production — built-in provider takes over
97- expect ( config . models ) . toBeUndefined ( ) ;
96+ // The bundled kilocode plugin only loads when this entry is present.
97+ // Empty `models` lets live gateway discovery own the catalog.
98+ expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://api.kilo.ai/api/gateway/' ) ;
99+ expect ( config . models . providers . kilocode . api ) . toBe ( 'openai-completions' ) ;
100+ expect ( config . models . providers . kilocode . models ) . toEqual ( [ ] ) ;
98101
99102 // No default model override when env var not set, and no memorySearch
100103 // schema introduced when the feature is off and absent from existing config.
@@ -311,7 +314,7 @@ describe('generateBaseConfig', () => {
311314 expect ( config . gateway . port ) . toBe ( 3001 ) ;
312315 } ) ;
313316
314- it ( 'removes stale kilocode provider with /api/openrouter/ baseUrl ' , ( ) => {
317+ it ( 'removes stale kilocode openrouter entry and rebuilds it pointed at the production gateway ' , ( ) => {
315318 const existing = JSON . stringify ( {
316319 models : {
317320 providers : {
@@ -327,16 +330,70 @@ describe('generateBaseConfig', () => {
327330 const { deps } = fakeDeps ( existing ) ;
328331 const config = generateBaseConfig ( minimalEnv ( ) , '/tmp/openclaw.json' , deps ) ;
329332
330- // Stale provider deleted, models object cleaned up
331- expect ( config . models ) . toBeUndefined ( ) ;
333+ // Stale entry replaced — old apiKey and models dropped, baseUrl pointed
334+ // at the production gateway so the bundled plugin can load.
335+ expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://api.kilo.ai/api/gateway/' ) ;
336+ expect ( config . models . providers . kilocode . api ) . toBe ( 'openai-completions' ) ;
337+ expect ( config . models . providers . kilocode . models ) . toEqual ( [ ] ) ;
338+ expect ( config . models . providers . kilocode . apiKey ) . toBeUndefined ( ) ;
339+ } ) ;
340+
341+ // Regression: an earlier migration deleted the kilocode provider entry on
342+ // personal (non-org) instances, expecting the bundled openclaw kilocode
343+ // plugin to auto-activate from KILOCODE_API_KEY alone. It does not — the
344+ // plugin only loads when an explicit provider entry is present, so without
345+ // it `kilo-auto/balanced` and the rest of the dynamic catalog were never
346+ // discovered and the agent failed with "Unknown model".
347+ it ( 'keeps kilocode provider entry on personal instances (no KILOCODE_ORGANIZATION_ID) so the bundled plugin loads' , ( ) => {
348+ const { deps } = fakeDeps ( ) ;
349+ const env = {
350+ ...minimalEnv ( ) ,
351+ KILOCODE_DEFAULT_MODEL : 'kilocode/kilo-auto/balanced' ,
352+ } ;
353+ const config = generateBaseConfig ( env , '/tmp/openclaw.json' , deps ) ;
354+
355+ expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://api.kilo.ai/api/gateway/' ) ;
356+ expect ( config . models . providers . kilocode . api ) . toBe ( 'openai-completions' ) ;
357+ expect ( config . models . providers . kilocode . models ) . toEqual ( [ ] ) ;
358+ expect ( config . models . providers . kilocode . headers ?. [ 'X-KiloCode-OrganizationId' ] ) . toBeUndefined ( ) ;
359+ expect ( config . agents . defaults . model . primary ) . toBe ( 'kilocode/kilo-auto/balanced' ) ;
360+ } ) ;
361+
362+ it ( 'preserves kilocode provider with production /api/gateway/ baseUrl and clears stale models' , ( ) => {
363+ const existing = JSON . stringify ( {
364+ models : {
365+ providers : {
366+ kilocode : {
367+ baseUrl : 'https://api.kilo.ai/api/gateway/' ,
368+ api : 'openai-completions' ,
369+ models : [ { id : 'kilo/auto' , name : 'Kilo Auto' } ] ,
370+ } ,
371+ } ,
372+ } ,
373+ } ) ;
374+ const { deps } = fakeDeps ( existing ) ;
375+ const config = generateBaseConfig ( minimalEnv ( ) , '/tmp/openclaw.json' , deps ) ;
376+
377+ // Entry preserved (so the plugin loads), stale onboard-written models
378+ // cleared so live discovery owns the catalog.
379+ expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://api.kilo.ai/api/gateway/' ) ;
380+ expect ( config . models . providers . kilocode . api ) . toBe ( 'openai-completions' ) ;
381+ expect ( config . models . providers . kilocode . models ) . toEqual ( [ ] ) ;
332382 } ) ;
333383
334- it ( 'removes stale kilocode provider with production /api/gateway/ baseUrl' , ( ) => {
384+ // Auth must come from `KILOCODE_API_KEY` env, never from a literal `apiKey`
385+ // field on disk. The previous deletion-based migration was incidentally
386+ // scrubbing the field; this test pins that the new normalization keeps that
387+ // scrub so a stale plaintext credential from a legacy onboard run cannot
388+ // survive across boots.
389+ it ( 'scrubs a stale plaintext apiKey from the kilocode provider entry' , ( ) => {
335390 const existing = JSON . stringify ( {
336391 models : {
337392 providers : {
338393 kilocode : {
339394 baseUrl : 'https://api.kilo.ai/api/gateway/' ,
395+ api : 'openai-completions' ,
396+ apiKey : 'sk-stale-plaintext' ,
340397 models : [ ] ,
341398 } ,
342399 } ,
@@ -345,7 +402,8 @@ describe('generateBaseConfig', () => {
345402 const { deps } = fakeDeps ( existing ) ;
346403 const config = generateBaseConfig ( minimalEnv ( ) , '/tmp/openclaw.json' , deps ) ;
347404
348- expect ( config . models ) . toBeUndefined ( ) ;
405+ expect ( config . models . providers . kilocode . apiKey ) . toBeUndefined ( ) ;
406+ expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://api.kilo.ai/api/gateway/' ) ;
349407 } ) ;
350408
351409 it ( 'keeps gateway provider for org-scoped instances but clears static models' , ( ) => {
@@ -396,7 +454,7 @@ describe('generateBaseConfig', () => {
396454 expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://api.kilo.ai/api/gateway/' ) ;
397455 } ) ;
398456
399- it ( 'preserves non-kilocode providers when removing stale kilocode entry' , ( ) => {
457+ it ( 'preserves non-kilocode providers when rebuilding stale kilocode openrouter entry' , ( ) => {
400458 const existing = JSON . stringify ( {
401459 models : {
402460 providers : {
@@ -414,9 +472,9 @@ describe('generateBaseConfig', () => {
414472 const { deps } = fakeDeps ( existing ) ;
415473 const config = generateBaseConfig ( minimalEnv ( ) , '/tmp/openclaw.json' , deps ) ;
416474
417- // kilocode removed, openai preserved
418- expect ( config . models . providers . kilocode ) . toBeUndefined ( ) ;
475+ // openai preserved, kilocode rebuilt with production gateway URL
419476 expect ( config . models . providers . openai . baseUrl ) . toBe ( 'https://api.openai.com/v1' ) ;
477+ expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://api.kilo.ai/api/gateway/' ) ;
420478 } ) ;
421479
422480 it ( 'creates kilocode provider with baseUrl and models: [] when KILOCODE_API_BASE_URL is set' , ( ) => {
@@ -428,7 +486,7 @@ describe('generateBaseConfig', () => {
428486 expect ( config . models . providers . kilocode . models ) . toEqual ( [ ] ) ;
429487 } ) ;
430488
431- it ( 'preserves existing models array when overriding baseUrl' , ( ) => {
489+ it ( 'clears stale models when overriding baseUrl, since live discovery owns the catalog ' , ( ) => {
432490 const existing = JSON . stringify ( {
433491 models : {
434492 providers : {
@@ -443,9 +501,10 @@ describe('generateBaseConfig', () => {
443501 const env = { ...minimalEnv ( ) , KILOCODE_API_BASE_URL : 'https://new-tunnel.example.com/' } ;
444502 const config = generateBaseConfig ( env , '/tmp/openclaw.json' , deps ) ;
445503
446- // baseUrl updated, existing models preserved
504+ // baseUrl updated, stale onboard-written models cleared so live
505+ // discovery populates the catalog from the new endpoint.
447506 expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://new-tunnel.example.com/' ) ;
448- expect ( config . models . providers . kilocode . models ) . toEqual ( [ { id : 'kept/model' , name : 'Kept' } ] ) ;
507+ expect ( config . models . providers . kilocode . models ) . toEqual ( [ ] ) ;
449508 } ) ;
450509
451510 it ( 'sets X-KiloCode-OrganizationId header when KILOCODE_ORGANIZATION_ID is set' , ( ) => {
@@ -465,8 +524,10 @@ describe('generateBaseConfig', () => {
465524 const { deps } = fakeDeps ( ) ;
466525 const config = generateBaseConfig ( minimalEnv ( ) , '/tmp/openclaw.json' , deps ) ;
467526
468- // No kilocode provider entry created when neither baseUrl nor orgId is set
469- expect ( config . models ) . toBeUndefined ( ) ;
527+ // Personal instance: kilocode entry still present (the bundled plugin
528+ // requires it to load), but no org header attached.
529+ expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://api.kilo.ai/api/gateway/' ) ;
530+ expect ( config . models . providers . kilocode . headers ?. [ 'X-KiloCode-OrganizationId' ] ) . toBeUndefined ( ) ;
470531 } ) ;
471532
472533 it ( 'preserves existing kilocode baseUrl and headers when adding org header' , ( ) => {
@@ -517,7 +578,8 @@ describe('generateBaseConfig', () => {
517578 // Other headers and config preserved
518579 expect ( config . models . providers . kilocode . headers [ 'X-Custom' ] ) . toBe ( 'preserved' ) ;
519580 expect ( config . models . providers . kilocode . baseUrl ) . toBe ( 'https://tunnel.example.com/' ) ;
520- expect ( config . models . providers . kilocode . models ) . toEqual ( [ { id : 'kept/model' , name : 'Kept' } ] ) ;
581+ // models cleared so live discovery from the (preserved) baseUrl owns the catalog
582+ expect ( config . models . providers . kilocode . models ) . toEqual ( [ ] ) ;
521583 } ) ;
522584
523585 it ( 'removes agents.defaults.models allowlist left by openclaw onboard' , ( ) => {
0 commit comments