Skip to content

Commit 484b295

Browse files
authored
Merge pull request #87 from smart-mcp-proxy/bugfix/config-port
Fix: Save config to disk when restart required (fixes listen address changes)
2 parents bb356e5 + a5a3ce8 commit 484b295

File tree

2 files changed

+166
-14
lines changed

2 files changed

+166
-14
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package runtime
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"go.uber.org/zap"
9+
10+
"mcpproxy-go/internal/config"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// TestApplyConfig_ListenAddressChange tests that listen address changes are saved to disk
17+
// even though they require a restart
18+
func TestApplyConfig_ListenAddressChange(t *testing.T) {
19+
// Create temp directory and config file
20+
tmpDir := t.TempDir()
21+
cfgPath := filepath.Join(tmpDir, "config.json")
22+
23+
// Initial config with port 8080
24+
initialCfg := config.DefaultConfig()
25+
initialCfg.Listen = "127.0.0.1:8080"
26+
initialCfg.DataDir = tmpDir
27+
28+
// Save initial config
29+
err := config.SaveConfig(initialCfg, cfgPath)
30+
require.NoError(t, err)
31+
32+
// Create runtime with initial config
33+
rt, err := New(initialCfg, cfgPath, zap.NewNop())
34+
require.NoError(t, err)
35+
defer func() {
36+
_ = rt.Close()
37+
}()
38+
39+
// Create new config with different listen address
40+
newCfg := config.DefaultConfig()
41+
newCfg.Listen = "127.0.0.1:30080" // Changed port
42+
newCfg.DataDir = tmpDir
43+
44+
// Apply the new config
45+
result, err := rt.ApplyConfig(newCfg, cfgPath)
46+
require.NoError(t, err)
47+
require.NotNil(t, result)
48+
49+
// Verify that restart is required (listen address changes require restart)
50+
assert.True(t, result.RequiresRestart, "Listen address change should require restart")
51+
assert.Contains(t, result.ChangedFields, "listen", "Should detect listen address change")
52+
53+
// The critical test: verify config was saved to disk despite requiring restart
54+
savedCfg, err := config.LoadFromFile(cfgPath)
55+
require.NoError(t, err)
56+
57+
assert.Equal(t, "127.0.0.1:30080", savedCfg.Listen,
58+
"Config file should be updated with new listen address even though restart is required")
59+
}
60+
61+
// TestApplyConfig_HotReloadableChange tests that hot-reloadable changes work correctly
62+
func TestApplyConfig_HotReloadableChange(t *testing.T) {
63+
// Create temp directory and config file
64+
tmpDir := t.TempDir()
65+
cfgPath := filepath.Join(tmpDir, "config.json")
66+
67+
// Initial config
68+
initialCfg := config.DefaultConfig()
69+
initialCfg.Listen = "127.0.0.1:8080"
70+
initialCfg.DataDir = tmpDir
71+
initialCfg.TopK = 5
72+
73+
// Save initial config
74+
err := config.SaveConfig(initialCfg, cfgPath)
75+
require.NoError(t, err)
76+
77+
// Create runtime
78+
rt, err := New(initialCfg, cfgPath, zap.NewNop())
79+
require.NoError(t, err)
80+
defer func() {
81+
_ = rt.Close()
82+
}()
83+
84+
// Create new config with different TopK (hot-reloadable)
85+
newCfg := config.DefaultConfig()
86+
newCfg.Listen = "127.0.0.1:8080" // Same listen address
87+
newCfg.DataDir = tmpDir
88+
newCfg.TopK = 10 // Changed TopK
89+
90+
// Apply the new config
91+
result, err := rt.ApplyConfig(newCfg, cfgPath)
92+
require.NoError(t, err)
93+
require.NotNil(t, result)
94+
95+
// Verify that restart is NOT required (TopK is hot-reloadable)
96+
assert.False(t, result.RequiresRestart, "TopK change should not require restart")
97+
assert.True(t, result.AppliedImmediately, "TopK change should be applied immediately")
98+
99+
// Verify config was saved to disk
100+
savedCfg, err := config.LoadFromFile(cfgPath)
101+
require.NoError(t, err)
102+
103+
assert.Equal(t, 10, savedCfg.TopK, "Config file should be updated with new TopK value")
104+
}
105+
106+
// TestApplyConfig_SaveFailure tests handling of save errors
107+
func TestApplyConfig_SaveFailure(t *testing.T) {
108+
// Create temp directory and config file
109+
tmpDir := t.TempDir()
110+
cfgPath := filepath.Join(tmpDir, "config.json")
111+
112+
// Initial config
113+
initialCfg := config.DefaultConfig()
114+
initialCfg.Listen = "127.0.0.1:8080"
115+
initialCfg.DataDir = tmpDir
116+
117+
// Save initial config
118+
err := config.SaveConfig(initialCfg, cfgPath)
119+
require.NoError(t, err)
120+
121+
// Create runtime
122+
rt, err := New(initialCfg, cfgPath, zap.NewNop())
123+
require.NoError(t, err)
124+
defer func() {
125+
_ = rt.Close()
126+
}()
127+
128+
// Make config file read-only to force save failure
129+
err = os.Chmod(cfgPath, 0444)
130+
require.NoError(t, err)
131+
defer func() {
132+
_ = os.Chmod(cfgPath, 0644)
133+
}()
134+
135+
// Create new config
136+
newCfg := config.DefaultConfig()
137+
newCfg.Listen = "127.0.0.1:30080"
138+
newCfg.DataDir = tmpDir
139+
140+
// Apply should fail because config can't be saved
141+
result, err := rt.ApplyConfig(newCfg, cfgPath)
142+
assert.Error(t, err, "Should fail when config cannot be saved")
143+
assert.NotNil(t, result)
144+
assert.False(t, result.Success, "Result should indicate failure")
145+
}

internal/runtime/runtime.go

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,27 @@ func (r *Runtime) ApplyConfig(newCfg *config.Config, cfgPath string) (*ConfigApp
813813
return result, fmt.Errorf("failed to detect config changes")
814814
}
815815

816-
// If restart is required, don't apply changes (let user restart)
816+
// Save configuration to disk BEFORE checking if restart is required
817+
// This ensures config changes that require restart are persisted and take effect on next startup
818+
savePath := cfgPath
819+
if savePath == "" {
820+
savePath = r.cfgPath
821+
}
822+
saveErr := config.SaveConfig(newCfg, savePath)
823+
if saveErr != nil {
824+
r.logger.Error("Failed to save configuration to disk",
825+
zap.String("path", savePath),
826+
zap.Error(saveErr))
827+
r.mu.Unlock() // Unlock before returning
828+
return &ConfigApplyResult{
829+
Success: false,
830+
}, fmt.Errorf("failed to save configuration: %w", saveErr)
831+
} else {
832+
r.logger.Info("Configuration successfully saved to disk",
833+
zap.String("path", savePath))
834+
}
835+
836+
// If restart is required, don't apply changes in-memory (let user restart)
817837
if result.RequiresRestart {
818838
r.logger.Warn("Configuration changes require restart",
819839
zap.String("reason", result.RestartReason),
@@ -829,19 +849,6 @@ func (r *Runtime) ApplyConfig(newCfg *config.Config, cfgPath string) (*ConfigApp
829849
r.cfgPath = cfgPath
830850
}
831851

832-
// Save configuration to disk
833-
saveErr := config.SaveConfig(newCfg, r.cfgPath)
834-
if saveErr != nil {
835-
r.logger.Error("Failed to save configuration to disk",
836-
zap.String("path", r.cfgPath),
837-
zap.Error(saveErr))
838-
// Don't fail the entire operation, but log the error
839-
// In-memory changes are still applied
840-
} else {
841-
r.logger.Info("Configuration successfully saved to disk",
842-
zap.String("path", r.cfgPath))
843-
}
844-
845852
// Apply configuration changes to components
846853
r.logger.Info("Applying configuration hot-reload",
847854
zap.Strings("changed_fields", result.ChangedFields))

0 commit comments

Comments
 (0)