package api import ( "bytes" "net/http" "net/http/httptest" "testing" "github.com/sipeed/picoclaw/pkg/config" ) func TestHandleUpdateConfig_PreservesExecAllowRemoteDefaultWhenOmitted(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() h := NewHandler(configPath) mux := http.NewServeMux() h.RegisterRoutes(mux) req := httptest.NewRequest(http.MethodPut, "/api/config", bytes.NewBufferString(`{ "agents": { "defaults": { "workspace": "~/.picoclaw/workspace" } }, "model_list": [ { "model_name": "custom-default", "model": "openai/gpt-4o", "api_key": "sk-default" } ] }`)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) } cfg, err := config.LoadConfig(configPath) if err != nil { t.Fatalf("LoadConfig() error = %v", err) } if !cfg.Tools.Exec.AllowRemote { t.Fatal("tools.exec.allow_remote should remain true when omitted from PUT /api/config") } } func TestHandleUpdateConfig_DoesNotInheritDefaultModelFields(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() h := NewHandler(configPath) mux := http.NewServeMux() h.RegisterRoutes(mux) req := httptest.NewRequest(http.MethodPut, "/api/config", bytes.NewBufferString(`{ "agents": { "defaults": { "workspace": "~/.picoclaw/workspace" } }, "model_list": [ { "model_name": "custom-default", "model": "openai/gpt-4o", "api_key": "sk-default" } ] }`)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) } cfg, err := config.LoadConfig(configPath) if err != nil { t.Fatalf("LoadConfig() error = %v", err) } if got := cfg.ModelList[0].APIBase; got != "" { t.Fatalf("model_list[0].api_base = %q, want empty string", got) } } func TestHandlePatchConfig_RejectsInvalidExecRegexPatterns(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() h := NewHandler(configPath) mux := http.NewServeMux() h.RegisterRoutes(mux) req := httptest.NewRequest(http.MethodPatch, "/api/config", bytes.NewBufferString(`{ "tools": { "exec": { "custom_deny_patterns": ["("] } } }`)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) } if !bytes.Contains(rec.Body.Bytes(), []byte("custom_deny_patterns")) { t.Fatalf("expected validation error mentioning custom_deny_patterns, body=%s", rec.Body.String()) } } func TestHandlePatchConfig_AllowsInvalidExecRegexPatternsWhenExecDisabled(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() h := NewHandler(configPath) mux := http.NewServeMux() h.RegisterRoutes(mux) req := httptest.NewRequest(http.MethodPatch, "/api/config", bytes.NewBufferString(`{ "tools": { "exec": { "enabled": false, "custom_deny_patterns": ["("], "custom_allow_patterns": ["("] } } }`)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) } } func TestHandlePatchConfig_AllowsInvalidDenyRegexPatternsWhenDenyPatternsDisabled(t *testing.T) { configPath, cleanup := setupOAuthTestEnv(t) defer cleanup() h := NewHandler(configPath) mux := http.NewServeMux() h.RegisterRoutes(mux) req := httptest.NewRequest(http.MethodPatch, "/api/config", bytes.NewBufferString(`{ "tools": { "exec": { "enabled": true, "enable_deny_patterns": false, "custom_deny_patterns": ["("] } } }`)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) } }