package web import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "keel/internal/settings" ) // fakeApplier records the last settings it was asked to apply and can be told to // reject with ErrInvalidBackend. type fakeApplier struct { called int last settings.Settings reject bool } func (f *fakeApplier) apply(s settings.Settings) error { if f.reject { return settings.ErrInvalidBackend } f.called++ f.last = s return nil } func TestGetSettingsReturnsCurrent(t *testing.T) { s, _ := newTestServer(t) cur := settings.Settings{AIBackend: "claude", MarvinCmd: "am", KnowledgePath: "/tmp/k.md"} fa := &fakeApplier{} s.SetSettings(filepath.Join(t.TempDir(), "settings.json"), cur, fa.apply) r := s.Router() req := httptest.NewRequest(http.MethodGet, "/settings", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200", w.Code) } var got settings.Settings if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode: %v", err) } if got != cur { t.Errorf("got %+v, want %+v", got, cur) } } func TestPostSettingsValidAppliesAndSaves(t *testing.T) { s, _ := newTestServer(t) path := filepath.Join(t.TempDir(), "settings.json") fa := &fakeApplier{} s.SetSettings(path, settings.Settings{}, fa.apply) r := s.Router() body := `{"ai_backend":"codex","marvin_cmd":"uv run am","knowledge_path":"/tmp/k.md"}` 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) } if fa.last.AIBackend != "codex" { t.Errorf("applied backend = %q, want codex", fa.last.AIBackend) } saved, err := settings.Load(path) if err != nil { t.Fatalf("settings file not loadable: %v", err) } if saved.AIBackend != "codex" || saved.MarvinCmd != "uv run am" || saved.KnowledgePath != "/tmp/k.md" { t.Errorf("saved settings = %+v, want codex/uv run am//tmp/k.md", saved) } } func TestPostSettingsInvalidBackendIs400AndNoSave(t *testing.T) { s, _ := newTestServer(t) path := filepath.Join(t.TempDir(), "settings.json") fa := &fakeApplier{reject: true} s.SetSettings(path, settings.Settings{}, fa.apply) r := s.Router() w := post(t, r, "/settings", `{"ai_backend":"bogus"}`) if w.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", w.Code) } if _, err := os.Stat(path); !os.IsNotExist(err) { t.Errorf("settings file should not exist after rejected save, stat err = %v", err) } if fa.called != 0 { t.Errorf("applier recorded %d successful applies, want 0", fa.called) } } func TestPostSettingsInvalidJSONIs400(t *testing.T) { s, _ := newTestServer(t) fa := &fakeApplier{} s.SetSettings(filepath.Join(t.TempDir(), "settings.json"), settings.Settings{}, fa.apply) r := s.Router() w := post(t, r, "/settings", `{not json`) if w.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", w.Code) } if fa.called != 0 { t.Errorf("applier called on invalid json; want 0, got %d", fa.called) } } 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 { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "profile.md"), []byte("x"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("x"), 0o644); err != nil { t.Fatal(err) } s, _ := newTestServer(t) s.SetSettings(filepath.Join(t.TempDir(), "settings.json"), settings.Settings{}, func(settings.Settings) error { return nil }) r := s.Router() req := httptest.NewRequest(http.MethodGet, "/fs/browse?dir="+dir, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200", w.Code) } var resp struct { Dir string `json:"dir"` Parent string `json:"parent"` Entries []struct { Name string `json:"name"` Path string `json:"path"` IsDir bool `json:"is_dir"` } `json:"entries"` } if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("decode: %v", err) } if resp.Parent != filepath.Dir(dir) { t.Errorf("parent = %q, want %q", resp.Parent, filepath.Dir(dir)) } if resp.Dir != dir { t.Errorf("dir = %q, want %q", resp.Dir, dir) } var names []string hasSub, hasMd, hasTxt := false, false, false for _, e := range resp.Entries { names = append(names, e.Name) switch e.Name { case "sub": hasSub = e.IsDir if e.Path != filepath.Join(dir, e.Name) { t.Errorf("sub path = %q, want %q", e.Path, filepath.Join(dir, e.Name)) } case "profile.md": hasMd = !e.IsDir if e.Path != filepath.Join(dir, e.Name) { t.Errorf("profile.md path = %q, want %q", e.Path, filepath.Join(dir, e.Name)) } case "notes.txt": hasTxt = true } } if !hasSub || !hasMd { t.Errorf("missing dir or .md entry; names = %v", names) } if hasTxt { t.Errorf(".txt should be filtered out; names = %v", names) } } func TestBrowseBadDirIs400(t *testing.T) { s, _ := newTestServer(t) s.SetSettings(filepath.Join(t.TempDir(), "settings.json"), settings.Settings{}, func(settings.Settings) error { return nil }) r := s.Router() req := httptest.NewRequest(http.MethodGet, "/fs/browse?dir=/no/such/dir/xyz", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", w.Code) } }