From c855731c3980498056e7b592c359ac75fea3f478 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Fri, 5 Jun 2026 20:05:59 -0400 Subject: [PATCH] fix(web): preserve aw_url when a settings POST omits it A UI save was silently blanking the configured AW URL because handlePostSettings bound the whole Settings from the request and saved it as-is. Now reads s.settings.AWURL under the existing settingsMu lock and carries it forward when req.AWURL is empty, so fields not exposed in the form are never erased. --- internal/web/settings_handlers.go | 5 +++++ internal/web/settings_handlers_test.go | 31 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/internal/web/settings_handlers.go b/internal/web/settings_handlers.go index f3b8d66..a4a5f4b 100644 --- a/internal/web/settings_handlers.go +++ b/internal/web/settings_handlers.go @@ -45,6 +45,11 @@ func (s *Server) handlePostSettings(c *gin.Context) { s.settingsMu.Lock() apply := s.applyFn path := s.settingsPath + // aw_url is not exposed in the settings form; preserve the persisted value + // so a UI save never blanks a configured AW URL. + if req.AWURL == "" { + req.AWURL = s.settings.AWURL + } s.settingsMu.Unlock() if apply == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "settings not wired"}) diff --git a/internal/web/settings_handlers_test.go b/internal/web/settings_handlers_test.go index 4506361..3b078ff 100644 --- a/internal/web/settings_handlers_test.go +++ b/internal/web/settings_handlers_test.go @@ -111,6 +111,37 @@ func TestPostSettingsInvalidJSONIs400(t *testing.T) { } } +func TestPostSettingsPreservesAWURL(t *testing.T) { + s, _ := newTestServer(t) + path := filepath.Join(t.TempDir(), "settings.json") + fa := &fakeApplier{} + cur := settings.Settings{AIBackend: "claude", AWURL: "http://aw.remote:9999"} + s.SetSettings(path, cur, fa.apply) + r := s.Router() + + // POST a body that omits aw_url (mirrors what the real UI form sends). + body := `{"ai_backend":"claude","marvin_cmd":"","knowledge_path":""}` + w := post(t, r, "/settings", body) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200 (body %s)", w.Code, w.Body.String()) + } + if fa.called != 1 { + t.Fatalf("applier called %d times, want 1", fa.called) + } + // (a) applyFn must have received the preserved AWURL. + if fa.last.AWURL != "http://aw.remote:9999" { + t.Errorf("applied AWURL = %q, want http://aw.remote:9999", fa.last.AWURL) + } + // (b) persisted file must also preserve the AWURL. + saved, err := settings.Load(path) + if err != nil { + t.Fatalf("settings file not loadable: %v", err) + } + if saved.AWURL != "http://aw.remote:9999" { + t.Errorf("saved AWURL = %q, want http://aw.remote:9999", saved.AWURL) + } +} + func TestBrowseListsDirsAndMarkdown(t *testing.T) { dir := t.TempDir() if err := os.Mkdir(filepath.Join(dir, "sub"), 0o755); err != nil {