@@ -97,8 +97,20 @@ vi.mock("../config/io.js", () => ({
9797 } ,
9898} ) ) ;
9999
100+ const capturedReplaceConfigFileCalls : Array < {
101+ nextConfig : OpenClawConfig ;
102+ writeOptions ?: { allowConfigSizeDrop ?: boolean } ;
103+ } > = [ ] ;
104+
100105vi . mock ( "../config/config.js" , ( ) => ( {
101- replaceConfigFile : async ( { nextConfig } : { nextConfig : OpenClawConfig } ) => {
106+ replaceConfigFile : async ( {
107+ nextConfig,
108+ writeOptions,
109+ } : {
110+ nextConfig : OpenClawConfig ;
111+ writeOptions ?: { allowConfigSizeDrop ?: boolean } ;
112+ } ) => {
113+ capturedReplaceConfigFileCalls . push ( { nextConfig, ...( writeOptions ? { writeOptions } : { } ) } ) ;
102114 testConfigStore . set ( resolveTestConfigPath ( ) , nextConfig ) ;
103115 } ,
104116 resolveGatewayPort : ( cfg : OpenClawConfig ) => cfg . gateway ?. port ?? 18789 ,
@@ -375,6 +387,7 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
375387 afterEach ( ( ) => {
376388 waitForGatewayReachableMock = undefined ;
377389 testConfigStore . clear ( ) ;
390+ capturedReplaceConfigFileCalls . length = 0 ;
378391 ensureWorkspaceAndSessionsMock . mockClear ( ) ;
379392 installGatewayDaemonNonInteractiveMock . mockClear ( ) ;
380393 createPreMigrationBackupMock . mockClear ( ) ;
@@ -386,6 +399,62 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
386399 readLastGatewayErrorLineMock . mockClear ( ) ;
387400 } ) ;
388401
402+ it ( "preserves existing agents.list and bindings on onboard rerun (openclaw#84692)" , async ( ) => {
403+ await withStateDir ( "state-preserve-agents-" , async ( stateDir ) => {
404+ const workspace = path . join ( stateDir , "openclaw" ) ;
405+ const seededAgents = [
406+ { id : "alpha" , model : "anthropic/claude-3-5-sonnet" } ,
407+ { id : "beta" , model : "openai/gpt-4o" } ,
408+ ] ;
409+ const seededBindings = [
410+ {
411+ type : "route" as const ,
412+ agentId : "alpha" ,
413+ match : {
414+ channel : "discord" ,
415+ peer : { kind : "direct" as const , id : "user-1" } ,
416+ } ,
417+ } ,
418+ {
419+ type : "route" as const ,
420+ agentId : "beta" ,
421+ match : {
422+ channel : "discord" ,
423+ peer : { kind : "direct" as const , id : "user-2" } ,
424+ } ,
425+ } ,
426+ ] ;
427+ testConfigStore . set ( resolveTestConfigPath ( ) , {
428+ agents : { list : seededAgents , defaults : { workspace } } ,
429+ bindings : seededBindings ,
430+ gateway : { mode : "local" , port : 18789 , auth : { mode : "token" , token : "seed_tok" } } ,
431+ } as OpenClawConfig ) ;
432+
433+ await runNonInteractiveSetup (
434+ {
435+ nonInteractive : true ,
436+ mode : "local" ,
437+ workspace,
438+ authChoice : "skip" ,
439+ skipSkills : true ,
440+ skipHealth : true ,
441+ installDaemon : false ,
442+ gatewayBind : "loopback" ,
443+ gatewayAuth : "token" ,
444+ gatewayToken : "seed_tok" ,
445+ } ,
446+ runtime ,
447+ ) ;
448+
449+ const cfg = readTestConfig ( ) ;
450+ expect ( cfg . agents ?. list ?. map ( ( a ) => a . id ) ) . toEqual ( [ "alpha" , "beta" ] ) ;
451+ expect ( cfg . bindings ) . toEqual ( seededBindings ) ;
452+
453+ const onboardWrite = capturedReplaceConfigFileCalls . at ( - 1 ) ;
454+ expect ( onboardWrite ?. writeOptions ?. allowConfigSizeDrop ) . toBe ( false ) ;
455+ } ) ;
456+ } , 60_000 ) ;
457+
389458 it ( "writes gateway token auth into config" , async ( ) => {
390459 await withStateDir ( "state-noninteractive-" , async ( stateDir ) => {
391460 const token = "tok_test_123" ;
@@ -558,6 +627,54 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
558627 } ) ;
559628 } , 60_000 ) ;
560629
630+ it ( "preserves existing agents.list and bindings on remote onboard rerun (openclaw#84692)" , async ( ) => {
631+ await withStateDir ( "state-remote-preserve-agents-" , async ( _stateDir ) => {
632+ const port = getPseudoPort ( 30_000 ) ;
633+ const token = "tok_remote_seed" ;
634+ const seededAgents = [
635+ { id : "alpha" , model : "anthropic/claude-3-5-sonnet" } ,
636+ { id : "beta" , model : "openai/gpt-4o" } ,
637+ ] ;
638+ const seededBindings = [
639+ {
640+ type : "route" as const ,
641+ agentId : "alpha" ,
642+ match : {
643+ channel : "discord" ,
644+ peer : { kind : "direct" as const , id : "user-1" } ,
645+ } ,
646+ } ,
647+ ] ;
648+ testConfigStore . set ( resolveTestConfigPath ( ) , {
649+ agents : { list : seededAgents } ,
650+ bindings : seededBindings ,
651+ gateway : {
652+ mode : "remote" ,
653+ remote : { url : `ws://127.0.0.1:${ port } ` , token } ,
654+ } ,
655+ } as OpenClawConfig ) ;
656+
657+ await runNonInteractiveSetup (
658+ {
659+ nonInteractive : true ,
660+ mode : "remote" ,
661+ remoteUrl : `ws://127.0.0.1:${ port } ` ,
662+ remoteToken : token ,
663+ authChoice : "skip" ,
664+ json : true ,
665+ } ,
666+ runtime ,
667+ ) ;
668+
669+ const cfg = readTestConfig ( ) ;
670+ expect ( cfg . agents ?. list ?. map ( ( a ) => a . id ) ) . toEqual ( [ "alpha" , "beta" ] ) ;
671+ expect ( cfg . bindings ) . toEqual ( seededBindings ) ;
672+
673+ const remoteWrite = capturedReplaceConfigFileCalls . at ( - 1 ) ;
674+ expect ( remoteWrite ?. writeOptions ?. allowConfigSizeDrop ) . toBe ( false ) ;
675+ } ) ;
676+ } , 60_000 ) ;
677+
561678 it ( "explains local health failure when no daemon was requested" , async ( ) => {
562679 await withStateDir ( "state-local-health-hint-" , async ( stateDir ) => {
563680 waitForGatewayReachableMock = vi . fn ( async ( ) => ( {
0 commit comments