@@ -52,6 +52,43 @@ vi.mock("../config/runtime-schema.js", () => ({
5252 readBestEffortRuntimeConfigSchema : ( ) => mockReadBestEffortRuntimeConfigSchema ( ) ,
5353} ) ) ;
5454
55+ vi . mock ( "../gateway/config-reload-plan.js" , ( ) => ( {
56+ buildGatewayReloadPlan : ( changedPaths : string [ ] ) => {
57+ const restartReasons = changedPaths . filter (
58+ ( changedPath ) =>
59+ changedPath . startsWith ( "models.pricing." ) || changedPath . startsWith ( "plugins.load." ) ,
60+ ) ;
61+ const hotReasons = changedPaths . filter (
62+ ( changedPath ) =>
63+ ! restartReasons . includes ( changedPath ) &&
64+ ( changedPath . startsWith ( "agents.list." ) ||
65+ changedPath . startsWith ( "agents.defaults.models." ) ||
66+ changedPath . startsWith ( "models." ) ||
67+ changedPath . startsWith ( "plugins." ) ) ,
68+ ) ;
69+ restartReasons . push (
70+ ...changedPaths . filter (
71+ ( changedPath ) => ! hotReasons . includes ( changedPath ) && ! restartReasons . includes ( changedPath ) ,
72+ ) ,
73+ ) ;
74+ return {
75+ changedPaths,
76+ restartGateway : restartReasons . length > 0 ,
77+ restartReasons,
78+ hotReasons,
79+ reloadHooks : false ,
80+ restartGmailWatcher : false ,
81+ restartCron : false ,
82+ restartHeartbeat : hotReasons . length > 0 ,
83+ restartHealthMonitor : false ,
84+ reloadPlugins : false ,
85+ restartChannels : new Set ( ) ,
86+ disposeMcpRuntimes : false ,
87+ noopPaths : [ ] ,
88+ } ;
89+ } ,
90+ } ) ) ;
91+
5592vi . mock ( "../plugins/plugin-metadata-snapshot.js" , async ( importOriginal ) => {
5693 const actual = await importOriginal < typeof import ( "../plugins/plugin-metadata-snapshot.js" ) > ( ) ;
5794 return {
@@ -3425,6 +3462,291 @@ describe("config cli", () => {
34253462 } ) ;
34263463 } ) ;
34273464
3465+ describe ( "config apply hints - issue #80722" , ( ) => {
3466+ it ( "prints a hot-reload hint for agents.list model changes" , async ( ) => {
3467+ const resolved : OpenClawConfig = {
3468+ agents : {
3469+ list : [
3470+ { id : "main" } ,
3471+ { id : "mason-vale" , model : { primary : "ollama/qwen3-coder-next" } } ,
3472+ ] ,
3473+ } ,
3474+ } ;
3475+ setSnapshot ( resolved , withRuntimeDefaults ( resolved ) ) ;
3476+
3477+ await runConfigCommand ( [
3478+ "config" ,
3479+ "set" ,
3480+ "agents.list[1].model.primary" ,
3481+ '"ollama/kimi-k2.6"' ,
3482+ "--strict-json" ,
3483+ ] ) ;
3484+
3485+ expectLogIncludes ( "Updated agents.list.1.model.primary" ) ;
3486+ expectLogIncludes ( "Change will apply without restarting the gateway." ) ;
3487+ expectLogExcludes ( "Restart the gateway to apply." ) ;
3488+ } ) ;
3489+
3490+ it ( "does not treat legacy per-agent agentRuntime as restart-required" , async ( ) => {
3491+ const resolved : OpenClawConfig = {
3492+ agents : {
3493+ list : [
3494+ {
3495+ id : "codex-legacy" ,
3496+ agentRuntime : { id : "codex" } ,
3497+ model : { primary : "openai/gpt-5.5" } ,
3498+ } ,
3499+ ] ,
3500+ } ,
3501+ } as unknown as OpenClawConfig ;
3502+ setSnapshot ( resolved , withRuntimeDefaults ( resolved ) ) ;
3503+
3504+ await runConfigCommand ( [
3505+ "config" ,
3506+ "set" ,
3507+ "agents.list[0].model.primary" ,
3508+ '"openai/gpt-5.4-mini"' ,
3509+ "--strict-json" ,
3510+ ] ) ;
3511+
3512+ expectLogIncludes ( "Change will apply without restarting the gateway." ) ;
3513+ expectLogExcludes ( "Restart the gateway to apply." ) ;
3514+ } ) ;
3515+
3516+ it ( "keeps the restart hint for hot-path edits when reload mode is off" , async ( ) => {
3517+ const resolved : OpenClawConfig = {
3518+ agents : {
3519+ list : [ { id : "main" , model : { primary : "openai/gpt-5.4" } } ] ,
3520+ } ,
3521+ gateway : {
3522+ reload : { mode : "off" } ,
3523+ } ,
3524+ } ;
3525+ setSnapshot ( resolved , withRuntimeDefaults ( resolved ) ) ;
3526+
3527+ await runConfigCommand ( [
3528+ "config" ,
3529+ "set" ,
3530+ "agents.list[0].model.primary" ,
3531+ '"openai/gpt-5.5"' ,
3532+ "--strict-json" ,
3533+ ] ) ;
3534+
3535+ expectLogIncludes ( "Updated agents.list.0.model.primary" ) ;
3536+ expectLogIncludes ( "Restart the gateway to apply." ) ;
3537+ expectLogExcludes ( "Change will apply without restarting the gateway." ) ;
3538+ } ) ;
3539+
3540+ it ( "keeps the restart hint for hot-path edits when reload mode is restart" , async ( ) => {
3541+ const resolved : OpenClawConfig = {
3542+ agents : {
3543+ list : [ { id : "main" , model : { primary : "openai/gpt-5.4" } } ] ,
3544+ } ,
3545+ gateway : {
3546+ reload : { mode : "restart" } ,
3547+ } ,
3548+ } ;
3549+ setSnapshot ( resolved , withRuntimeDefaults ( resolved ) ) ;
3550+
3551+ await runConfigCommand ( [
3552+ "config" ,
3553+ "set" ,
3554+ "agents.list[0].model.primary" ,
3555+ '"openai/gpt-5.5"' ,
3556+ "--strict-json" ,
3557+ ] ) ;
3558+
3559+ expectLogIncludes ( "Updated agents.list.0.model.primary" ) ;
3560+ expectLogIncludes ( "Restart the gateway to apply." ) ;
3561+ expectLogExcludes ( "Change will apply without restarting the gateway." ) ;
3562+ } ) ;
3563+
3564+ it ( "prints a hot-reload hint when removing legacy per-agent agentRuntime" , async ( ) => {
3565+ const resolved : OpenClawConfig = {
3566+ agents : {
3567+ list : [
3568+ {
3569+ id : "codex-legacy" ,
3570+ agentRuntime : { id : "codex" } ,
3571+ } ,
3572+ ] ,
3573+ } ,
3574+ } as unknown as OpenClawConfig ;
3575+ setSnapshot ( resolved , withRuntimeDefaults ( resolved ) ) ;
3576+
3577+ await runConfigCommand ( [ "config" , "unset" , "agents.list[0].agentRuntime" ] ) ;
3578+
3579+ expectLogIncludes ( "Removed agents.list[0].agentRuntime" ) ;
3580+ expectLogIncludes ( "Change will apply without restarting the gateway." ) ;
3581+ expectLogExcludes ( "Restart the gateway to apply." ) ;
3582+ } ) ;
3583+
3584+ it ( "prints a hot-reload hint for provider runtime policy changes" , async ( ) => {
3585+ const resolved : OpenClawConfig = {
3586+ models : {
3587+ providers : {
3588+ openai : { } ,
3589+ } ,
3590+ } ,
3591+ } as unknown as OpenClawConfig ;
3592+ setSnapshot ( resolved , resolved ) ;
3593+
3594+ await runConfigCommand ( [
3595+ "config" ,
3596+ "set" ,
3597+ "models.providers.openai.agentRuntime.id" ,
3598+ '"pi"' ,
3599+ "--strict-json" ,
3600+ ] ) ;
3601+
3602+ expectLogIncludes ( "Updated models.providers.openai.agentRuntime.id" ) ;
3603+ expectLogIncludes ( "Change will apply without restarting the gateway." ) ;
3604+ expectLogExcludes ( "Restart the gateway to apply." ) ;
3605+ } ) ;
3606+
3607+ it ( "keeps the restart hint for broad models writes that change pricing bootstrap" , async ( ) => {
3608+ const resolved : OpenClawConfig = {
3609+ models : {
3610+ pricing : {
3611+ enabled : false ,
3612+ } ,
3613+ providers : {
3614+ openai : {
3615+ agentRuntime : { id : "node" } ,
3616+ } ,
3617+ } ,
3618+ } ,
3619+ } as unknown as OpenClawConfig ;
3620+ setSnapshot ( resolved , resolved ) ;
3621+
3622+ await runConfigCommand ( [
3623+ "config" ,
3624+ "set" ,
3625+ "models" ,
3626+ '{"pricing":{"enabled":true},"providers":{"openai":{"agentRuntime":{"id":"node"}}}}' ,
3627+ "--strict-json" ,
3628+ "--replace" ,
3629+ ] ) ;
3630+
3631+ expectLogIncludes ( "Updated models. Restart the gateway to apply." ) ;
3632+ expectLogExcludes ( "Change will apply without restarting the gateway." ) ;
3633+ } ) ;
3634+
3635+ it ( "keeps the restart hint for broad plugins writes that change load paths" , async ( ) => {
3636+ const resolved : OpenClawConfig = {
3637+ plugins : {
3638+ load : {
3639+ paths : [ "/tmp/openclaw-plugins-a" ] ,
3640+ } ,
3641+ entries : {
3642+ canvas : { enabled : true } ,
3643+ } ,
3644+ } ,
3645+ } as unknown as OpenClawConfig ;
3646+ setSnapshot ( resolved , resolved ) ;
3647+
3648+ await runConfigCommand ( [
3649+ "config" ,
3650+ "set" ,
3651+ "plugins" ,
3652+ '{"load":{"paths":["/tmp/openclaw-plugins-b"]},"entries":{"canvas":{"enabled":true}}}' ,
3653+ "--strict-json" ,
3654+ "--replace" ,
3655+ ] ) ;
3656+
3657+ expectLogIncludes ( "Updated plugins. Restart the gateway to apply." ) ;
3658+ expectLogExcludes ( "Change will apply without restarting the gateway." ) ;
3659+ } ) ;
3660+
3661+ it ( "keeps the restart hint for broad models unsets that remove pricing bootstrap" , async ( ) => {
3662+ const resolved : OpenClawConfig = {
3663+ models : {
3664+ pricing : {
3665+ enabled : false ,
3666+ } ,
3667+ providers : {
3668+ openai : {
3669+ agentRuntime : { id : "node" } ,
3670+ } ,
3671+ } ,
3672+ } ,
3673+ } as unknown as OpenClawConfig ;
3674+ setSnapshot ( resolved , resolved ) ;
3675+
3676+ await runConfigCommand ( [ "config" , "unset" , "models" ] ) ;
3677+
3678+ expectLogIncludes ( "Removed models. Restart the gateway to apply." ) ;
3679+ expectLogExcludes ( "Change will apply without restarting the gateway." ) ;
3680+ } ) ;
3681+
3682+ it ( "keeps the restart hint for broad plugins unsets that remove load paths" , async ( ) => {
3683+ const resolved : OpenClawConfig = {
3684+ plugins : {
3685+ load : {
3686+ paths : [ "/tmp/openclaw-plugins-a" ] ,
3687+ } ,
3688+ entries : {
3689+ canvas : { enabled : true } ,
3690+ } ,
3691+ } ,
3692+ } as unknown as OpenClawConfig ;
3693+ setSnapshot ( resolved , resolved ) ;
3694+
3695+ await runConfigCommand ( [ "config" , "unset" , "plugins" ] ) ;
3696+
3697+ expectLogIncludes ( "Removed plugins. Restart the gateway to apply." ) ;
3698+ expectLogExcludes ( "Change will apply without restarting the gateway." ) ;
3699+ } ) ;
3700+
3701+ it ( "keeps the restart hint for restart-required config paths" , async ( ) => {
3702+ const resolved : OpenClawConfig = {
3703+ agents : { list : [ { id : "main" } ] } ,
3704+ gateway : { port : 18789 } ,
3705+ } ;
3706+ setSnapshot ( resolved , withRuntimeDefaults ( resolved ) ) ;
3707+
3708+ await runConfigCommand ( [ "config" , "set" , "gateway.auth.mode" , "token" ] ) ;
3709+
3710+ expectLogIncludes ( "Restart the gateway to apply." ) ;
3711+ expectLogExcludes ( "Change will apply without restarting the gateway." ) ;
3712+ } ) ;
3713+
3714+ it ( "keeps plugin entry config writes restart-backed when reload metadata is absent" , async ( ) => {
3715+ const resolved : OpenClawConfig = {
3716+ plugins : {
3717+ entries : {
3718+ canvas : { enabled : true } ,
3719+ } ,
3720+ } ,
3721+ } as unknown as OpenClawConfig ;
3722+ setSnapshot ( resolved , resolved ) ;
3723+
3724+ await runConfigCommand ( [ "config" , "set" , "plugins.entries.canvas.enabled" , "false" ] ) ;
3725+
3726+ expectLogIncludes ( "Updated plugins.entries.canvas.enabled" ) ;
3727+ expectLogIncludes ( "Restart the gateway to apply." ) ;
3728+ expectLogExcludes ( "Change will apply without restarting the gateway." ) ;
3729+ } ) ;
3730+
3731+ it ( "keeps the restart hint for mixed hot and restart batch updates" , async ( ) => {
3732+ const resolved : OpenClawConfig = {
3733+ agents : { list : [ { id : "main" , model : { primary : "openai/gpt-5.4" } } ] } ,
3734+ gateway : { port : 18789 } ,
3735+ } ;
3736+ setSnapshot ( resolved , withRuntimeDefaults ( resolved ) ) ;
3737+
3738+ await runConfigCommand ( [
3739+ "config" ,
3740+ "set" ,
3741+ "--batch-json" ,
3742+ '[{"path":"agents.list[0].model.primary","value":"openai/gpt-5.5"},{"path":"gateway.auth.mode","value":"token"}]' ,
3743+ ] ) ;
3744+
3745+ expectLogIncludes ( "Updated 2 config paths. Restart the gateway to apply." ) ;
3746+ expectLogExcludes ( "Change will apply without restarting the gateway." ) ;
3747+ } ) ;
3748+ } ) ;
3749+
34283750 describe ( "config file" , ( ) => {
34293751 it ( "prints the active config file path" , async ( ) => {
34303752 const resolved : OpenClawConfig = { gateway : { port : 18789 } } ;
0 commit comments